spring security 완벽 정리

로그인 과정
1. HTTP Request 수신
모든 HTTP 요청은 FilterChain을 통과하며, 등록된 필터들이 차례대로 실행됩니다.
먼저 config파일을 작성해 줄 필요가 있습니다.
어떤 url을 제한할 건지 또한 권한은 어떻게 할 것인지 그리고 어떤 필터를 사용할 것인지.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/login", "/signup", "/swagger-ui/**").permitAll() // 인증이 필요 없는 경로 설정
.anyRequest().authenticated() // 나머지 요청은 인증 필요
.and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); // JWT 필터 추가
return http.build();
}
}
HttpSecurity 객체를 사용하여 필터 체인과 인가 규칙을 설정한다. 모든 HTTP 요청은 이 설정을 통과하며, 필터 체인에 정의된 대로 실행됩니다.
/login, /signup, js 및 css파일들은 인증 필터에서 제외해줘야 합니다.
당연하죠. 로그인을 해야 jwt 토큰을 받는데 이게 없는 상태에서 로그인 페이지를 못 들어가는 건 말이 안 되잖아요.
2. 유저 자격을 기반으로 인증토큰 생성
@Service
@RequiredArgsConstructor
public class LoginService {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
public String login(String username, String password) throws AuthenticationException {
// 1. 사용자 인증 (사용자 존재 여부 확인 포함)
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
// 2. 인증이 성공하면 SecurityContext에 설정
SecurityContextHolder.getContext().setAuthentication(authentication);
// 3. JWT 토큰 생성
return jwtUtil.generateToken(username);
}
}
이렇게 /login으로 맵핑된 경우,
authenticationManager를 사용하는 경우는 /login으로 요청이 왔을 경우 username과 password를 통해서 인증을 해야합니다.
- AuthenticationManager.authenticate() 호출
- AuthenticationManager의 대표적인 구현체 ProviderManager 실행
- ProviderManager가 등록된 AuthenticationProvider에게 위임
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
// 각 AuthenticationProvider 순회하며 인증 시도
for (AuthenticationProvider provider : getProviders()) {
if (provider.supports(toTest)) {
Authentication result = provider.authenticate(authentication);
if (result != null) {
return result;
}
}
}
throw new AuthenticationException("No suitable AuthenticationProvider found for " + toTest.getName());
}
위 코드에서 getProviders를 통해서 등록된 Provider를 가져옵니다.
AuthenticationProvider.authenticate() 메서드 실행
// DaoAuthenticationProvider의 인증 과정
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 입력된 사용자 이름과 비밀번호를 추출
String username = authentication.getName();
String password = (String) authentication.getCredentials();
// 데이터베이스에서 사용자 정보 조회
UserDetails user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
// 입력된 비밀번호와 데이터베이스의 비밀번호 비교
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("Invalid credentials");
}
// 인증 성공 시 Authentication 객체 생성
return createSuccessAuthentication(authentication, user);
}
Spring Security는 내부적으로 DaoAuthenticationProvider라는 기본 인증 제공자를 사용합니다.
이 DaoAuthenticationProvider는 UserDetailsService를 사용해 사용자를 조회하고 비밀번호를 검증합니다.
그래서 별도의 설정을 하지 않더라도 UserDetailsService 구현체가 빈으로 등록되어 있으면 이를 자동으로 사용합니다.
따라서 아래처럼 구현체를 만들고 @Service 컴포넌트를 달아주면 스프링이 autenticate를 할 때 알아서 가져다 씁니다.
@Service
@RequiredArgsConstructor
public class MyUserDetailsService implements UserDetailsService
retrieveUser() 메서드를 사용하여 UserDetailsService 구현체를 통해 데이터베이스에서 사용자 정보를 조회.
4. SecurityContext에 저장: 생성된 Authentication 객체는 SecurityContextHolder에 저장됩니다.

로그인 이후 필터에서의 과정
JWT 추출 및 검증
요청의 Authorization 헤더에서 JWT를 추출하고, 추출된 JWT가 유효한지 검증합니다.
이 과정에서는 AuthenticationManager가 필요하지 않습니다. 왜 why?
토큰을 뽑아서 username을 얻어서 검증하기만 하면 되니깐요.
// Authorization 헤더에서 JWT 토큰을 추출
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
try {
username = jwtUtil.extractUsername(jwt);
} catch (Exception e) {
// JWT 토큰 파싱 중 예외 발생 시 처리
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token");
return; // 필터 체인 중단
}
이 과정에서 jwt 토큰에서 username을 뽑습니다.
// 사용자 인증 정보가 없을 경우, JWT 검증
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 토큰이 유효한 경우, 사용자 인증을 설정
if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
} else {
// JWT 토큰이 유효하지 않은 경우
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token");
return; // 필터 체인 중단
}
}
그리고 이 과정에서 만약 아직 SecurityContextHolder에 Authentication이 없는 경우에만 추가로 검사해줘야합니다.
그리고 jwt와 추출한 username을 userDetails로 비교해서 맞는 경우에 SecurityContextHolder에 Authentication을 넣어주면 됩니다.
'개발' 카테고리의 다른 글
[java] JVM 개념 및 기능 : 왜 쓰는 걸까? (3) | 2024.10.10 |
---|---|
[배포] github actions, EC2, nginx를 통한 무중단 배포 (3) | 2024.10.09 |
[JAVA] 자바의 가비지컬렉션 개념과 동작원리 (0) | 2024.10.08 |
[spring mvc] 서블릿 컨테이너 너는 누구냐 (3) | 2024.10.06 |
[spring mvc] DispatcherServlet 핵심 정리 (0) | 2024.10.06 |