
스프링 클라우드로 프로젝트 실습 중이고, 오늘은 JWT 적용에 대해 자세히 배웠다. JWT 부분 개념이 많이 부족한 지 어떻게 알고 ^^!
사용자가 누구인지 확인하는 단계.
ex. 로그인
인증을 통해 검증된 사용자가 리소스에 접근할 때, 리소스에 접슨할 권리가 있는지 확인하는 과정.
접근 주체.
토큰은 Access Token, Refresh Token이 있다. 리프레시 토큰은 주로 엑세스 토큰이 만료되었을 때 새로운 엑세스 토큰을 발급하기 위해 사용된다. db에 저장되는 토큰이다.
MSA 에서 JWT 적용할 때 달라진 점은 토큰 발생은 USER 서비스가 하고 인증은 게이트웨이에서 한다는 점이다. 일단 적용해보자.
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-gson:0.12.5'
jwt:
expires-in: 86400
mobile-expires-in: 31536000
table-expires-in: 31536000
secret-key: AADfaskllew32dsfasdTG764aaaaaaGdslkj298GsWg86G
expires-in: 86400 웹은 유효기간을 하루로 설정.
시크릿 키는 20자 이상이어야 한다.
해당 값들을 변수로 접근하는데 사용할 클래스이다.
@Component
@ConfigurationProperties(value = "jwt", ignoreInvalidFields = true)
@Getter
@Setter
public class JwtConfigProperties {
private Integer expiresIn;
private Integer mobileExpiresIn;
private Integer tabletExpiresIn;
private String secretKey;
}
-> yml에서 설정한 값이 매핑된다.
토큰 정보를 담을 dto를 설정했다.
cf. 코드 중, @NoArgsConstructor(access = AccessLevel.PRIVATE) 이 부분은 외부에서 이 클래스(TokenDto)를 직접 new 하지 못하도록 막는 코드이다.
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TokenDto {
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class JwtToken {
private String token;
private Integer expiresIn;
}
@Getter
@RequiredArgsConstructor
public static class AccessToken {
private final JwtToken access;
}
@Getter
@Setter
@RequiredArgsConstructor
public static class AccessRefreshToken {
private final JwtToken access;
private final JwtToken refresh;
}
}
토큰 생성 클래스.
아래 클래스 안에 정의된 메서드는 다음과 같다.
@Component
@RequiredArgsConstructor
public class TokenGenerator {
private final JwtConfigProperties configProperties;
private volatile SecretKey secretKey;
private SecretKey getSecretKey() {
if (secretKey == null) {
synchronized (this) {
if (secretKey == null) {
secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(configProperties.getSecretKey()));
}
}
}
return secretKey;
}
public TokenDto.AccessToken generateAccessToken(String userId, String deviceType) {
TokenDto.JwtToken jwtToken = this.generateToken(userId, deviceType, false);
return new TokenDto.AccessToken(jwtToken);
}
public TokenDto.AccessRefreshToken generateAccessRefreshToken(String userId, String deviceType) {
TokenDto.JwtToken accessJwtToken = this.generateToken(userId, deviceType, false);
TokenDto.JwtToken refreshJwtToken = this.generateToken(userId, deviceType, true);
return new TokenDto.AccessRefreshToken(accessJwtToken, refreshJwtToken);
}
public TokenDto.JwtToken generateToken(String userId, String deviceType, boolean refreshToken) {
int tokenExpiresIn = tokenExpiresIn(refreshToken, deviceType);
String tokenType = refreshToken ? "refresh" : "access";
String token = Jwts.builder()
.issuer("welab")
.subject(userId)
.claim("userId", userId)
.claim("deviceType", deviceType)
.claim("tokenType", tokenType)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + tokenExpiresIn * 1000L))
.signWith(getSecretKey())
.header().add("typ", "JWT")
.and()
.compact();
return new TokenDto.JwtToken(token, tokenExpiresIn);
}
이 밖에도 검증하는 메서드가 있는데 그 코드는 생략했다.
| 항목 | Access Token | Refresh Token |
|---|---|---|
| 용도 | API 접근 인증 | 새로운 Access Token 발급 |
| 저장 위치 | 주로 클라이언트 메모리 | HttpOnly 쿠키 / DB |
| 유효기간 | 짧음 (예: 1일) | 김 (예: 1년) |
| 보관 방식 | 클라이언트 측 | 서버/DB 저장 권장 |
| 보안 위험 | 탈취 시 곧바로 API 접근 가능 | 탈취 시 새 토큰 발급 위험 |
현재 코드는 게이트웨이 jwt적용 전이다. 후에 게이트웨이를 적용하고 나면, 게이트웨이에서 JWT 유효성 검증, 서명 확인, 토큰 만료 확인, 사용자 정보 추출 등을 담당하게 된다.