본문 바로가기
개발

[spring] spring security 완전 정복

by 주주병 2024. 10. 9.
728x90
반응형

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를 통해서 인증을 해야합니다.

  1. AuthenticationManager.authenticate() 호출
  2. AuthenticationManager의 대표적인 구현체 ProviderManager 실행
  3. 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라는 기본 인증 제공자를 사용합니다.

DaoAuthenticationProviderUserDetailsService를 사용해 사용자를 조회하고 비밀번호를 검증합니다.

 

그래서 별도의 설정을 하지 않더라도 UserDetailsService 구현체가 빈으로 등록되어 있으면 이를 자동으로 사용합니다.

따라서 아래처럼 구현체를 만들고 @Service 컴포넌트를 달아주면 스프링이 autenticate를 할 때 알아서 가져다 씁니다.


      
@Service
@RequiredArgsConstructor
public class MyUserDetailsService implements UserDetailsService

 

retrieveUser() 메서드를 사용하여 UserDetailsService 구현체를 통해 데이터베이스에서 사용자 정보를 조회.

 

 

4. SecurityContext에 저장: 생성된 Authentication 객체는 SecurityContextHolder에 저장됩니다.

 

728x90

로그인 이후 필터에서의 과정

 

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; // 필터 체인 중단
}
}

 

그리고 이 과정에서 만약 아직 SecurityContextHolderAuthentication이 없는 경우에만 추가로 검사해줘야합니다.

 

그리고 jwt와 추출한 username을 userDetails로  비교해서 맞는 경우에 SecurityContextHolder에 Authentication을 넣어주면 됩니다.

728x90
반응형