spring security 설정 (2) - jwt

진병욱·2023년 11월 7일
post-thumbnail

jwt 사용을 위한 만료 시간 설정

ExpireTime.java

public class ExpireTime {

    public static final long ACCESS_TOKEN_EXPIRE_TIME = 6 * 60 * 60 * 1000L;               // 액세스 토큰 6시간
    public static final long REFRESH_TOKEN_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L;     // 리프레시 토큰 7일
}

jwt 사용을 위한 secretkey 지정

application-jwt.yml

jwt:
  secret: ${JWT_SECRET_KEY}    # JWT를 Encoding / Decoding하기 위한 Private Key

시크릿 키를 지정하기 위해선 특정 글자수를 넘겨야 한다. 너무 짧으면 안 된다.

jwt 관련 처리 클래스

JwtTokenProvider.java

@Slf4j
@Component
public class JwtTokenProvider {

    private final MemberRepository memberRepository;
    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String AUTHORITIES_KEY = "auth";
    private static final String BEARER_TYPE = "Bearer";
    private static final String TYPE_ACCESS = "access";
    private static final String TYPE_REFRESH = "refresh";

    private final Key key;

    public JwtTokenProvider(@Value("${JWT_SECRET_KEY}") String secretKey, @Autowired MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

    //Authentication 을 가지고 AccessToken, RefreshToken 을 생성하는 메서드
    public UserResponseDto.TokenInfo generateToken(Authentication authentication) {

        return generateToken(authentication.getName(), authentication.getAuthorities());


    }

    //name, authorities 를 가지고 AccessToken, RefreshToken 을 생성하는 메서드
    public UserResponseDto.TokenInfo generateToken(String name, Collection<? extends GrantedAuthority> inputAuthorities) {

        //권한 가져오기
        String authorities = inputAuthorities.stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        Date now = new Date();

        //Generate AccessToken
        String accessToken = Jwts.builder()
                .setSubject(name)
                .claim(AUTHORITIES_KEY, authorities)
                .claim("type", TYPE_ACCESS)
                .setIssuedAt(now)   //토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + ExpireTime.ACCESS_TOKEN_EXPIRE_TIME))  //토큰 만료 시간 설정
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        //Generate RefreshToken
        String refreshToken = Jwts.builder()
                .claim("type", TYPE_REFRESH)
                .setIssuedAt(now)   //토큰 발행 시간 정보
                .setExpiration(new Date(now.getTime() + ExpireTime.REFRESH_TOKEN_EXPIRE_TIME)) //토큰 만료 시간 설정
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
                
		// 회원 정보를 찾아서 리프레시 토큰을 저장 -> 이 부분은 추후에 redis를 사용하여, refreshtoken을 저장하여 관리 할 예정
        Member member = memberRepository.findById(name).orElse(null);

        member.updateRefreshToken(refreshToken);

        memberRepository.save(member);

        return UserResponseDto.TokenInfo.builder()
                .grantType(BEARER_TYPE)
                .accessToken(accessToken)
                .accessTokenExpirationTime(ExpireTime.ACCESS_TOKEN_EXPIRE_TIME)
                .refreshToken(refreshToken)
                .refreshTokenExpirationTime(ExpireTime.REFRESH_TOKEN_EXPIRE_TIME)
                .build();
    }

    //JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 메서드
    public Authentication getAuthentication(String accessToken) {
        //토큰 복호화
        Claims claims = parseClaims(accessToken);

        if (claims.get(AUTHORITIES_KEY) == null) {
            //TODO:: Change Custom Exception
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");
        }

        //클레임에서 권한 정보 가져오기
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        //UserDetails 객체를 만들어서 Authentication 리턴
        UserDetails principal = new User(claims.getSubject(), "", authorities);
        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }

    //토큰 정보를 검증하는 메서드
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT Token", e);
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT Token", e);
            throw new JwtTokenException("JWT 토큰 만료");
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT Token", e);
        } catch (IllegalArgumentException e) {
            log.info("JWT claims string is empty.", e);
        }
        return false;
    }

	// 액세스 토큰으로부터 정보 추출
    private Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
        } catch (ExpiredJwtException e) {
            // ???
            return e.getClaims();
        }
    }
    
	// 헤더에서 액세스 토큰 추출
    public String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_TYPE)) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

jwt 인증 정보 처리 필터

2024.03.29 변경 사항
기존 GenericFilterBean > 변경 OncePerRequestFilter
변경 이유 : GenericFilterBean의 경우 최초 1회 인증이 완료된 요청이더라도, 해당 요청으로 여러 요청이 일어날 경우 그 요청에 대해 모두 인증 처리가 들어감.
OncePerRequestFilter의 경우 최초 1회만 인증. 그 이후는 미인증
(한 번의 요청에 대해서는 한 번의 인증만 이루어진다는 말)

JwtAuthenticationFilter.java

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;
    private final CustomUserDetailsService customUserDetailsService;

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {

        //1. Request Header 에서 JWT Token 추출
        String token = jwtTokenProvider.resolveToken(request);

        //2. validateToken 메서드로 토큰 유효성 검사
        // 유효한 경우에
        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication jwtAuthentication = jwtTokenProvider.getAuthentication(token);
            UserDetails userDetails = customUserDetailsService.loadUserByUsername(jwtAuthentication.getName());
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
}

현재는 AccessToken만을 사용. RefreshToken을 활용하지 않음
사용을 원하는 경우 Redis와 쿠키를 활용한 JWT 관리 시리즈를 참고 바람

profile
새로운 기술을 접하는 것에 망설임이 없고, 부족한 것이 있다면 항상 배우고자 하는 열정을 가지고 있습니다!

0개의 댓글