Spring Security 공부기록 - (3) JWT 방식 적용하기

j00r6·2024년 1월 16일
0

Spring Security

목록 보기
3/3

JWT 적용하기

JWT를 적용하기 위해서는 다양한 방법이 존재하지만
제가 사용한 방식의 구현 단계는 아래와 같습니다.

  1. SecurityConfiguration 에서의 사전설정
  2. 사용자의 엔티티 정보를 전달 받을 UserDetails, UserDetailsService 구현
  3. Token 생성기 구현
  4. 생성된 토큰을 dofilter 를 활용하여 SecurityContextHolder 에 추가하기
  5. Token관련 Controller, Service, DTO 구현

SecurityConfiguration

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

@Bean
    public SecurityFilterChain basicConfig (HttpSecurity http) throws Exception {
        http 
                .formLogin(AbstractHttpConfigurer::disable)
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                ...
                ...

기존에 학습 시에도 JWT를 적용했었다.
일전에 살펴보았던 SecurityFilterchain 작성법에서

위 3가지 코드를 사용하면 토큰 방식을 활용하는데 필요한 기초세팅이 끝난다.

formLogin(AbstractHttpConfigurer::disable) 은 말그대로 폼로그인 방식을 활용할 경우 활성화하는 설정이며

sessionManagement 설정은 session 방식을 활용해서 로그인을 구현할때 사용된다.
위와같이 STATELESS 로 적용할 경우 session 방식을 사용하지 않는다는 의미로 적용된다.

추가로 formLogin 일때 사용되는 필터인

TokenProvider

JWT를 생성하는 토큰 생성기 입니다.

	@Getter
    @Value("${jwt.key}")
    private String secretKey;

    @Getter
    @Value("${jwt.access-token-expiration-minutes}")
    private int accessTokenTime;

    @Getter
    @Value("${jwt.refresh-token-expiration-minutes}")
    private int refreshTokenTime;

기본적으로 JWT 설정관련된 파일은 위와같이 환경변수 처리하여
키값의 유출로부터 보호합니다.

기존코드


public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

 @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException {
        토큰 인증성공후 로직 작성
        ...
        ...
        ...
        }
        
        
        
@Override
    protected void unsuccessfulAuthentication(HttpServletRequest request,
                                              HttpServletResponse response,
                                              AuthenticationException failed) throws IOException {
        토큰 인증실패후 로직 작성
        ...
        ...
        ...
}

변경된 코드

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // 유효한 자격증명을 제공하지 않고 접근하려 할때 401
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException {
        //필요한 권한이 없이 접근하려 할때 403
        response.sendError(HttpServletResponse.SC_FORBIDDEN);
    }
}

기존에 활용했던 방식은 UsernamePasswordAuthenticationFilter 를 상속받아 구현체인 JwtAuthenticationFilter 를 구현하여 인증성공과 실패시의 로직을 작성했지만

변경된 코드에서는 AccessDeniedHandlerAuthenticationEntryPoint 를 활용해서 Security 인증절차가 진행되는 과정에서 예외처리를 진행하였습니다.

더 자세한 사항은 공식문서를 참조해주시기 바랍니다.

참조 - Security 인증방식 (공식문서)

기존코드

@Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException {
        Member member = (Member) authResult.getPrincipal();
        String accessToken = delegateAccessToken(member);

        Long memberId = member.getMemberId();

        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenTime());
        String TokenExpirationDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(expiration);
        response.setHeader("TokenExpiration", TokenExpirationDate);

        String refreshToken = delegateRefreshToken(member);
        String nickName = member.getNickName();
        String profileImage = member.getProfileImage();
        response.setHeader("Authorization", "Bearer " + accessToken);
        response.setHeader("Refresh", refreshToken);
        response.setHeader("memberId", String.valueOf(memberId));
        Map<String, Object> responseMessage = new HashMap<>();
        responseMessage.put("nickName", nickName);
        responseMessage.put("profileImage", profileImage);
        String responseBody = new ObjectMapper().writeValueAsString(responseMessage);
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(responseBody);
    }

또한 UsernamePasswordAuthenticationFilter 를 구현하여 필요한 유저 정보를 하나하나 전달하다보니 코드 내용이 길어지고 가독성이 떨어졌습니다.

변경된 코드

        // 토큰에 유저정보 담기
        CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
        Long memberId = userDetails.getMemberId();
        String email = userDetails.getUsername();

        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .claim(AUTHORITIES_KEY, authorities)
                // 필요한 정보만 추가
                .claim("memberId", memberId)
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(validity)
                .compact();

UsernamePasswordAuthenticationToken을 활용하여 UserDetails UserDetailsService 로 부터 보다 간편하게 회원정보를 넘겨받아 토큰에 추가하도록 하여, 토큰에 담긴 정보들을 활용했습니다.

Spring Security 공식문서에서는 JWT 관련 세팅 정보를 제공하지 않아서 그밖의 정보들은 블로그를 참조하여 구현하였습니다.

0개의 댓글