코딩과 결혼합니다

231025 - [Game_Crew]리팩토링 : Spring Security JWT 로그인 본문

코딩과 매일매일♥/Game_Crew

231025 - [Game_Crew]리팩토링 : Spring Security JWT 로그인

코딩러버 2023. 10. 25. 17:54
728x90
기존에는 로그인 기능을 어플리케이션 도메인에 구현하였는데, 이번에 Spring Security JWT 로그인으로 Filter에서 로그인을 처리하도록 리팩토링 해보았다. 이것에는 몇 가지 이점이 있다.
1. Spring Security는 보안 관련 다양한 기능을 제공하므로 보안 구현 부담을 줄일 수 있다.
2. Spring Security의 다양한 확장 기능을 활용하여 인증 방식이나 사용자 관리 기능을 유연하게 변경하거나 확장할 수 있다. 

📌JWT  검증 및 인가

Slf4j(topic = "JWT 검증 및 인가")
@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {

        String tokenValue = jwtUtil.getTokenFromRequest(req);

        if (StringUtils.hasText(tokenValue)) {
            // JWT 토큰 substring
            tokenValue = jwtUtil.substringToken(tokenValue);
            log.info(tokenValue);

            if (!jwtUtil.validateToken(tokenValue)) {
                log.error("Token Error");
                return;
            }

            Claims info = jwtUtil.getUserInfoFromToken(tokenValue);

            try {
                setAuthentication(info.getSubject());
            } catch (Exception e) {
                log.error(e.getMessage());
                return;
            }
        }

        filterChain.doFilter(req, res);
    }
  • doFilterInternal 메소드 : HTTP 요청이 들어올 때마다 실행되는 메소드이다. 여기서 JWT 토큰을 추출하고, 토큰의 유효성을 검증하며, 토큰에서 사용자 정보를 추출하여 인증 처리를 진행한다.
  • 토큰 검증이 실패하거나 예외가 발생하면 로그를 출력하고 다음 필터로 넘어가지 않는다.
    // 인증 처리
    public void setAuthentication(String nickname) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = createAuthentication(nickname);
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }
  • SecurityContext context = SecurityContextHolder.createEmptyContext(); : 빈 SecurityContext 객체를 생성한다. SecurityContext는 보안 관련 정보를 담고 있는 객체로, 인증 정보를 포함한다.
  • createAuthentication 메소드를 호출하여 주어진 닉네임을 기반으로 인증 객체(Authentication)를 생성한다.
  • createAuthentication 메소드는 사용자의 닉네임을 받아서 UserDetails 객체를 로드하고, 이를 이용하여 UsernamePassWordAuthenticationToken 객체를 생성한다.
  • context.setAuthentication(authentication); : 생성된 인증 객체를 SecurityContext에 설정한다. 이를 통해 현재 사용자의 인증 정보가 SecurityContext에 저장된다.
  • SecurityContext 객체를 SecurityContextHolder에 설정 한다.
  • SecurityContextHolder는 SecurityContext 를 전역적으로 접근할 수 있는 홀더 객체로 이를 통해 다른 곳에서도 해당 인증 정보를 참조할 수 있다.

📌로그인 및 JWT 생성

@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
        setFilterProcessesUrl("/auth/login");
    }
  • UsernamePasswordAuthenticationFilter : 폼 로그인 방식의 인증을 처리하는
  • JwtUtil 인스턴스를 주입받아 이를 통해 JWT 관련 작업을 수행
  • setFilterProcessesUrl("/auth/login"); : 이 필터가 처리할 요청 URL을 설정
  @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        log.info("로그인 시도");
        try {
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);

            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getEmail(),
                            requestDto.getPassword(),
                            null
                    )
            );
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }
  • 클라이언트로부터 받은 요청을 LoginRequestDto로 변환한다. ObjectMapper는 Jackson 라이브러리에서 제공하는 클래스로, JSON 데이터를 자바 객체로 변환하는 역직렬화를 수행한다.
  • 변환된 LoginRequestDto에 담긴 이메일과 비밀번호를 바탕으로 UsernamePasswordAuthenticationToken인스턴스를 생성하고, 이를 authenticate 메소드에 전달하여 인증을 시도하고, 성공하면 인증 정보를 담은 객체를 반환한다.
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        log.info("로그인 성공 및 JWT 생성");
        String email = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();

        String token = jwtUtil.createToken(email);
        jwtUtil.addJwtToCookie(token, response);

        // 클라이언트로 토큰을 응답으로 보내주기
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        // 토큰을 JSON 형식으로 응답 바디에 담기
        String jsonResponse = "{\"token\":\"" + token + "\"}";
        response.getWriter().write(jsonResponse);
        response.getWriter().flush();
    }
  • 인증 성공 결과로 받은 Authenticaion 객체에서 UserDetailsImpl 객체를 가져와 이메일을 추출한다.
  • 추출한 이메일을 이용하여 JWT 토큰을 생성한다.
  • 생성된 JWT 토큰을 쿠키에 추가한다.
  • 응답의 컨텐트 타입을 JSON으로 설정하고, 문자 인코딩을 UTF-8로 설정한다.
  • 생성된 JWT 토큰을 JSON 형식으로 변환하여 이 문자열을 응답 바디에 쓰고, 출력 스트림을 플러시하여 클라이언트에 전송한다.
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        log.info("로그인 실패");

        // 클라이언트로 실패 메시지를 응답으로 보내기
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // HTTP 상태 코드 401 - Unauthorized
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        // 실패 메시지를 JSON 형식으로 응답 바디에 담기
        String jsonResponse = "{\"message\":\"로그인에 실패하였습니다.\"}";
        response.getWriter().write(jsonResponse);
        response.getWriter().flush();
    }
}
  • 사용자의 인증이 실패했을 때 실행되는 코드로 상태코드 401과 실패 메세지를 클라이언트에 전송한다.