jwt
JSON 객체를 사용해서 토큰 자체에 정보를 저장하는 Web Token으로 클라이언트에 저장된다. (탈취 가능성 있음)
JwtFilter
이 필터는 SecurityConfig의 필터체인에서 UsernamePasswordAuthenticationFilter 전에 등록된다.
@Component
@RequiredArgsConstructor
public class JwtFilter extends GenericFilterBean {
private final JwtProvider tokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//header 에서 token 추출
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String accessToken = tokenProvider.resolveToken(httpServletRequest);
if (accessToken != null) {
Authentication authentication = tokenProvider.getAuthentication(accessToken);//customAuthentication 반환
SecurityContextHolder.getContext().setAuthentication(authentication); //securityContext 에 authentication 객체 저장
}
chain.doFilter(request, response);
}
}
JwtProvider
토큰 발급, 재발급, access Token 생성, refresh Token 생성, 토큰에서 인증 정보 조회, 유효성, 만료일자 확인 등의 메소드를 구현했다.
@Component
@RequiredArgsConstructor
public class JwtProvider {
public static final String AUTHORIZATION = "Authorization";
@Value("${jwt.secret-key}")
private final String secretKey;
@Value("${jwt.access-expiration}")
private final Duration accessExpiration;
@Value("${jwt.refresh-expiration}")
private final Duration refreshExpiration;
public AuthenticationToken issue(Student student){
return AuthenticationToken.builder()
.accessToken(createAccessToken(student.getId()))
.refreshToken(createRefreshToken()).build();
}
public AuthenticationToken reissue(String accessToken, String refreshToken) {
String validateRefreshToken = validateRefreshToken(refreshToken);
String refreshAccessToken = refreshAccessToken(accessToken);
return AuthenticationToken.builder()
.accessToken(refreshAccessToken)
.refreshToken(validateRefreshToken)
.build();
}
//JWT 토큰 생성
private String createAccessToken(Long studentId) {
Claims claims = Jwts.claims().setSubject(String.valueOf(studentId)); //JWT payload에 저장되는 정보 단위, user 식별값 넣음 (id)
LocalDateTime now = LocalDateTime.now();
LocalDateTime validity = now.plus(accessExpiration);
return Jwts.builder()
.setClaims(claims) //정보 저장
.setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant())) //토큰 발생 시간
.setExpiration(Date.from(validity.atZone(ZoneId.systemDefault()).toInstant())) //만료 기간
.signWith(SignatureAlgorithm.HS256, secretKey.getBytes()) //암호화 알고리즘, secret 값
.compact();
}
private String createRefreshToken() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime validity = now.plus(refreshExpiration);
return Jwts.builder()
.setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant())) //토큰 발생 시간
.setExpiration(Date.from(validity.atZone(ZoneId.systemDefault()).toInstant())) //만료 기간
.signWith(SignatureAlgorithm.HS256, secretKey.getBytes()) //암호화 알고리즘, secret 값
.compact();
}
//JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String accessToken) {
Jws<Claims> claimsJws = validateAccessToken(accessToken);
Long studentId = Long.parseLong(claimsJws.getBody().getSubject());
//발급할 때는 인증이 필요없지만 검증 시에는 user 데이터에 token의 학번과 일치하는 사용자가 있는지 확인 필요함
return new CustomAuthentication(studentId);
}
//JWT 토큰에서 회원 정보(학번) 추출 -> token을 발급받을 때는 무조건 인증된 사용자만 발급받아서 회원 정보 검증할 필요가 없음
//request header에서 token 값 가져옴
public String resolveToken(HttpServletRequest request) {
String header = request.getHeader(AUTHORIZATION);
if(!StringUtils.hasText(header)) return null;
return request.getHeader(AUTHORIZATION);
}
//token 유효성, 만료일자 확인
private String validateRefreshToken(String refreshToken) {
try {
Jwts.parser().
setSigningKey(secretKey.getBytes()).
parseClaimsJws(refreshToken);
return refreshToken;
} catch (ExpiredJwtException e) {
throw new CustomException(EXPIRED_TOKEN);
} catch (JwtException e){
throw new CustomException(INVALID_TOKEN);
}
}
}