
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:
secret: ${JWT_SECRET_KEY} # JWT를 Encoding / Decoding하기 위한 Private Key
시크릿 키를 지정하기 위해선 특정 글자수를 넘겨야 한다. 너무 짧으면 안 된다.
@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;
}
}
2024.03.29 변경 사항
기존 GenericFilterBean > 변경 OncePerRequestFilter
변경 이유 : GenericFilterBean의 경우 최초 1회 인증이 완료된 요청이더라도, 해당 요청으로 여러 요청이 일어날 경우 그 요청에 대해 모두 인증 처리가 들어감.
OncePerRequestFilter의 경우 최초 1회만 인증. 그 이후는 미인증
(한 번의 요청에 대해서는 한 번의 인증만 이루어진다는 말)
@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 관리 시리즈를 참고 바람