
스프링 시큐리티에서는 기본적으로 세션 기반 인증을 제공한다.
토큰 기반 인증을 토큰을 사용하는 방법으로, 서버에서 클라이언트를 구분하기 위한 유일한 값을 토큰이라고 한다.
서버가 토큰을 생성하여 클라이언트에 제공하면 클라이언트는 여러 요청을 이 토큰과 함께 신청한다.
JSON포맷을 이용한 claim 기반 웹 토큰이다.
일반적으로 쿠키 저장소에 jwt를 저장한다.
장점
로그인 정보를 서버에 저장하지 않고, 클라이언트에 암호화하여 저장 > 서버 부담 ↓
HTTP 헤더의 Authorization 키값에 "Bearer {토큰}" 형태로 담아서 보냄
구조
accessToken : 사용자 인증
refreshToken : accessToken 갱신을 위한 인증 정보
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
◾ jwt 생성시 사용할 secret key를 application.properties에 작성후 불러오기 위한 클래스 만들기
@Setter
@Getter
@Component
@ConfigurationProperties("jwt")
public class JwtProperties {
private String secretKey;
}
// application.properties 작성
//jwt.secret.key = ...
◾ 토큰 제공 클래스 작성
@Slf4j(topic = "TokenProvider")
@RequiredArgsConstructor
@Service
public class TokenProvider {
private final JwtProperties jwtProperties;
public String generateToken(User user, Duration expiredAt) {
Date now = new Date();
return makeToken(new Date(now.getTime() + expiredAt.toMillis()), user);
}
private String makeToken(Date expiry, User user) {
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuedAt(now)
.setExpiration(expiry)
.setSubject(user.getUsername())
.claim("id", user.getId())
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}
public boolean validToken(String token) {
try {
Jwts.parser().setSigningKey(jwtProperties.getSecretKey()).parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | SignatureException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
// 토큰기반으로 인증 정보 가져오기
public Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
// User는 내가 만든 entity가 아니라 시큐리티가 제공하는 User!!!!!
return new UsernamePasswordAuthenticationToken(new org.springframework.security.core.userdetails.User(claims.getSubject
(), "", authorities), token, authorities);
}
//토큰 기반으로 유저 ID 가져오기
public Long getUserId(String token) {
Claims claims = getClaims(token);
return claims.get("id", Long.class);
}
private Claims getClaims(String token) {
return Jwts.parser() //클레임 조회
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody();
}
}
refresh 토큰은 서버에 저장되어 유효하지 않은 access 토큰으로 요청이 왔을 때 새로운 access 토큰을 발급을 위해 사용한다.
◾ refresh token entity
@Entity
@Getter
@NoArgsConstructor
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "user_id", nullable = false, unique = true)
private Long userId;
@Column(name = "refresh_token", nullable = false)
private String refreshToken;
public RefreshToken(Long userId, String refreshToken) {
this.userId = userId;
this.refreshToken = refreshToken;
}
public RefreshToken update(String newRefreshToken) {
this.refreshToken = newRefreshToken;
return this;
}
}
◾ 토큰 필터
- 요청이 들어오면 토큰 필터로 유효한 토큰인지 확인
- 유효하다면 security context holder에 인증 정보 저장
- 서비스 로직 실행
- security context
: 인증 객체가 저장되는 보관소
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;
private final static String HEADER_AUTHORIZATION = "Authorization";
private final static String TOKEN_PREFIX = "Bearer ";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
String token = getAccessToken(authorizationHeader);
if(tokenProvider.validToken(token)){
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getAccessToken(String authorizationHeader){
if(authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)){
return authorizationHeader.substring(TOKEN_PREFIX.length());
}
return null;
}
}
◾ 리프레시 토큰 서비스
리프레시 토큰 불러오기
@Service
@RequiredArgsConstructor
public class RefreshTokenService {
private final RefreshTokenRepository refreshTokenRepository;
public RefreshToken findByRefreshToken(String refreshToken){
return refreshTokenRepository.findByRefreshToken(refreshToken)
.orElseThrow(()-> new IllegalArgumentException("Unexpected token"));
}
}
◾ 토큰 서비스
리프레시 토큰 유효성 검사 후 새로운 액세스 토큰 발급
@Service
@RequiredArgsConstructor
public class TokenService {
private final TokenProvider tokenProvider;
private final RefreshTokenService refreshTokenService;
private final UserService userService;
public String createNewAccessToken(String refreshToken){
if(!tokenProvider.validToken(refreshToken)){
throw new IllegalArgumentException("Unexpected token");
}
Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId();
User user = userService.findById(userId);
return tokenProvider.generateToken(user, Duration.ofHours(2));
}
}
◾ 토큰 request, response dto 작성 후 컨트롤러 작성
requestDto에는 refreshToken이 들어가고, responseDto에는 accessToken이 들어간다고 생각하면 된다.
위에서 만든 서비스 로직을 이용하여 컨트롤러를 작성하면 끝