✏️ JWT
- JWT 토큰을 발급받기 위해 사용했던 추상 객체로,
Spring Security 라이브러리를 의존하면 사용할 수 있었다.
- 2022 년 2월 21일에 업데이트된 Spring Security 5.7.0-M2 버전 이후부터 서비스가 종료되었다.
- 즉, 이 객체를 사용하지 않고 최신 방법으로 JWT 토큰을 발급받을 예정이다.
- 만약
W**ebSecurityConfigureAdapter
객체를 사용하고 싶다면 버전을 낮춰야한다.**
- java 11
- spring boot 2.7.3
✏️ 환경설정
📍 Dependency
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
📍 Application yml
- JWT 의 암호화 복호화를 위한 Secrit key 를 추가한다.
- HS256 알고리즘을 사용해야 하기 때문에 256 bit 보다 커야한다.
- 한단어 당 8bit 이므로 32 글자 이상이 필요하다.
jwt:
secret: VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa
✏️ TokenInfo
- 클라이언트에 토큰을 보내기 위한 DTO
grantType
- HTTP header 에 prefix 로 붙여주는 타입으로
Bearer
를 사용한다.
package com.baeker.baeker.base.security.jwt;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
@AllArgsConstructor
public class JwtTokenInfo {
private String grantType;
private String accessToken;
private String refreshToken;
}
✏️ JwtTokenProbider
- Access Token 과 Refresh Token 을 생성하는 Class
86480000
- Date 생성자에 입력하는 수치로 토큰의 유효기간을 뜻한다.
- 86480000 는 24시간을 뜻한다.
- 보통 토큰은 30분 정도로 생성하는데 원활한 test 를 위해 24시간으로 세팅했다.
package com.baeker.baeker.base.security.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;
@Slf4j
@Component
public class JwtTokenProvider {
private final Key key;
public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
byte[] keyByes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyByes);
}
public JwtTokenInfo generateToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
long now = (new Date()).getTime();
Date accessTokenExpiresIn = new Date(now + 86400000);
String accessToken = Jwts.builder()
.setSubject(authentication.getName())
.claim("auth", authorities)
.setExpiration(accessTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
String refreshToken = Jwts.builder()
.setExpiration(new Date(now + 86400000))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
return JwtTokenInfo.builder()
.grantType("Bearer")
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
public Authentication getAuthentication(String accessToken) {
Claims claims = parseClaims(accessToken);
if (claims.get("auth") == null)
throw new RuntimeException("권한 정보가 없는 토큰입니다.");
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get("auth").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
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 (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("Invalid JWT Token", e);
} catch (ExpiredJwtException e) {
log.info("Expired JWT Token", e);
} 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();
}
}
}
✏️ JwtAuthenticationFilter
- 클라이언트 요청시 JWT 를 인증하는 커스텀 필터
UsernamePasswordAuthenticationFilter
이전에 실행된다.
JwtAuthenticationFilter
를 통과 하면 UsernamePasswordAuthenticationFilter
이후 필터는 통과한것으로 본다는 의미이다.
- 즉, Username + PW 를 통과한 인증을 JWT 를 통해 수행한다는 의미
package com.baeker.baeker.base.security.jwt;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;
import java.io.IOException;
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = resolveToken((HttpServletRequest) request);
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer"))
return bearerToken.substring(7);
return null;
}
}