- 23.12.13) TIL2023년 12월 14일 01시 30분 22초에 업로드 된 글입니다.작성자: oneseel
1. SecurityFilterChain
- WebSecurityConfig 클래스에서 SecurityFilterChain을 Bean으로 등록한다.
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // CSRF 설정 http.csrf((csrf) -> csrf.disable()); // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정 http.sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ); http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests .requestMatchers(PathRequest.toStaticResources().atCommonLocations()) .permitAll() // resource 접근 허용 설정 .requestMatchers("/api/users/**").permitAll() // '/api/users/'로 시작하는 요청 모두 접근 허가 .anyRequest().authenticated() // 그 외 모든 요청 인증처리 ); // 필터 처리 http.addFilterBefore(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); }
2. JwtAuthorizationFilter
- OncePerRequestFilter를 상속받는다. (이 클래스는 모든 HTTP 요청에 대해 딱 한 번만 실행되도록 보장한다. 즉, 동일한 요청에 대해 필터가 여러 번 실행되지 않도록 하는 역할을 한다.)
@Slf4j(topic = "JWT 검증 및 인가") @RequiredArgsConstructor public class JwtAuthorizationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; private final ObjectMapper objectMapper; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
1) 토큰 추출
- JwtUtil의 resolveToken 메서드를 이용해 요청에 있는 토큰을 추출한다.
String token = jwtUtil.resolveToken(request);
- HTTP 헤더에서 "Authorization" 헤더의 값을 가져온다.
- 가져온 헤더 값이 StringUtils.hasText 메서드를 사용하여 null이 아니고, 비어 있지 않은지 확인한다.
- bearerToken.startsWith(BEARER_PREFIX)를 사용하여 헤더 값이 "Bearer "로 시작하는지 확인한다.
- 만약 위 두 가지 조건이 모두 참이면, "Bearer " 접두사를 제외하고 나머지 부분을 반환하고 만족하지 않으면 null을 반환한다.
public String resolveToken(HttpServletRequest request) { String bearerToken = request.getHeader(AUTHORIZATION_HEADER); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) { return bearerToken.substring(7); } return null; }
2) 토큰 유효성 검사
- 토큰이 null이 아닌지 확인하고 JwtUtil의 validationToken 메서드를 이용해 토큰의 유효성을 확인한다.
if (Objects.nonNull(token)) { // 토큰이 Null이 아니면 if (jwtUtil.validationToken(token)) { // 토큰을 validationToken 메소드로 검사
- global.exception에서 각 예외처리를 해준다.
public boolean validationToken(String token) { try { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); return true; } catch (SecurityException | MalformedJwtException | SignatureException e) { throw new InvalidJwtSignatureException(e); // 유효하지 않는 JWT 서명 입니다. } catch (ExpiredJwtException e) { // 만료된 JWT token 입니다. throw new ExpiredJwtTokenException(e); } catch (UnsupportedJwtException e) { // 지원되지 않는 JWT 토큰 입니다. throw new UnsupportedJwtTokenException(e); } catch (IllegalArgumentException e) { // 잘못된 JWT 토큰 입니다. throw new InvalidJwtTokenException(e); } }
3) 토큰에서 유저정보를 가져오기
- JwtUtil에 있는 getUserInfoFromToken 메서드를 이용해 토큰에 있는 유저정보(username)를 가져온다.
- getUserDetails 메소드를 이용해서 UserRepository에서 username을 조회해서 가져온다.
- 조회된 사용자 정보를 UsernamePasswordAuthenticationToken에 넣어 인증 객체(authentication)를 생성한다.
- 인증 객체를 빈 SecurityContext에 넣는다.
- SecurityContext를 SecurityContextHolder에 저장하면, Spring Security에서 현재 사용자에 대한 인증 정보에 접근할 수 있게 된다.(@AuthenticationPrincipal)
Claims info = jwtUtil.getUserInfoFromToken(token); // 토큰에서 user 정보를 가져옴 // 인증 정보에 유저정보(username) 넣기 // username -> user 조회 String username = info.getSubject(); SecurityContext context = SecurityContextHolder.createEmptyContext(); // userDetails에 저장 UserDetails userDetails = userDetailsService.getUserDetails(username); // authentication의 principal에 저장 Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null); // 저장한 내용을 securityContent에 저장 context.setAuthentication(authentication); // securityContext를 SecurityContextHolder에 저장 SecurityContextHolder.setContext(context); // 위 작업으로 인해 @AuthenticationPrincipal로 조회 가능 } else { // 인증 토큰이 유효하지 않을 떄 CommonResponseDto commonResponseDto = new CommonResponseDto("토큰이 유효하지 않습니다.", HttpStatus.BAD_REQUEST.value()); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.setContentType("application/json; charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(commonResponseDto)); } } filterChain.doFilter(request, response); }
public Claims getUserInfoFromToken(String token) { return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); }
public UserDetails getUserDetails(String username) { User user = userRepository.findByUsername(username) .orElseThrow(UserNotFoundException::new); return new UserDetailsImpl(user); }
3. 로그인 API
- 로그인 할 때, 헤더에 토큰을 넣어 응답할 때 보낸다.
- JwtUtil의 createToken 메서드를 사용해서 응답할 때 보낼 토큰을 생성한다.
// 로그인 @PostMapping("/login") public ResponseEntity<?> login( @RequestBody UserLoginRequestDto requestDto, HttpServletResponse response) { try { userService.login(requestDto); } catch (BusinessException be) { return ResponseEntity.status(be.getStatus()) .body(new CommonResponseDto(be.getMessage(), be.getStatus())); } // 로그인 시 헤더에 JWT 토큰이 보임 response.setHeader(JwtUtil.AUTHORIZATION_HEADER, jwtUtil.createToken(requestDto.getUsername())); return ResponseEntity.ok() .body(new CommonResponseDto("로그인 성공", HttpStatus.OK.value())); }
- matches 메서드를 이용해서 입력된 값 password와 DB에 저장된 password를 비교해서 다르면 예외처리 한다.
public void login(UserLoginRequestDto requestDto) { String username = requestDto.getUsername(); String password = requestDto.getPassword(); // 저장된 회원이 없는 경우 User user = userRepository.findByUsername(username) .orElseThrow(UserNotFoundException::new); if (!passwordEncoder.matches(password, user.getPassword())) { throw new UserNotFoundException(); } }
public String createToken(String username) { Date date = new Date(); // 토큰 만료시간 60분 long TOKEN_TIME = 60 * 60 * 1000; return BEARER_PREFIX + Jwts.builder() .setSubject(username) .setExpiration(new Date(date.getTime() + TOKEN_TIME)) .setIssuedAt(date) .signWith(key, signatureAlgorithm) .compact(); }
4. 순환참조
UserDetailsService에 있는 getUser 메서드를 원래 UserService에 만들었는데, 이렇게 하면 WebSecurityConfig에서는 UserService를 주입받고, UserSerivce에서는 PasswordEncoder를 주입받아서 서로가 서로를 의존하는 상태가 된다.
그래서 UserDetailsService를 만들어 getUser 메서드를 이 클래스로 옮기고, WebSecurityConfig에서는 UserDetailsService를 주입받게 해준다.
5. 포스트맨 확인
1) 로그인 성공
2) 로그인 실패 (username이 없거나 password가 일치하지 않는 경우)
'TIL' 카테고리의 다른 글
23.12.27) TIL (0) 2023.12.27 23.12.26) TIL (0) 2023.12.26 23.12.12) TIL (0) 2023.12.12 23.12.08) TIL (0) 2023.12.08 23.12.07) TIL (0) 2023.12.07 댓글