로그인 및 로그아웃은 웹 애플리케이션에서 핵심적인 기능으로, 사용자의 인증(Authentication)과 권한 관리를 담당한다.
이를 구현하기 위해서는 사용자의 신원을 확인하고 권한을 부여하는 인증(Authentication)과 사용자가 요청한 자원에 대한 접근 권한을 확인하는 인가(Authorization)이 필요하며, JWT(JSON Web Token)는 이러한 인증과 권한 관리를 효과적으로 처리하기 위한 방법 중 하나이다.
JWT는 세 부분으로 구성되어 있다
: Header, Payload, Signature.
Header에는 토큰의 유형과 사용된 알고리즘 등에 대한 메타데이터가 포함된다. 주로 이러한 정보는 JSON 형식으로 인코딩되어 있다.
Payload에는 토큰에 담길 정보와 토큰의 발행 및 만료 시간이 포함되며, 중요한 정보를 포함할 때에는 주의가 필요하며, 사용자를 식별할 수 있는 고유한 식별자를 사용하는 것이 좋다.
Signature는 토큰의 무결성을 확인하기 위해 사용한다. Header와 Payload를 인코딩하고, 서버에서 사용하는 비밀 키를 사용하여 서명한다.
확장성: JSON 형식을 사용하므로, 다양한 환경에서 쉽게 사용할 수 있다.
보안성: 서명된 토큰을 사용하여 인증 및 권한 부여를 수행하므로, 보안성이 높다.
상태 없음(Stateless): 서버는 클라이언트의 상태를 유지할 필요가 없어서 확장성이 용이하다.
Spring Security는 사용자의 인증(Authentication)과 권한 부여(Authorization)를 위한 프레임워크.
JWT와 조합하여 로그인을 구현하는 방법은 다음과 같다:
로그인 요청이 들어오면 사용자의 인증을 수행합니다. 이를 위해 Spring Security의 AuthenticationManager를 사용하여 사용자를 인증한다
인증에 성공한 경우, 사용자에게 JWT를 발급하여 클라이언트에게 전달한다. JWT는 사용자의 인증 정보를 포함하고, 서버에서 발행된 서명이 포함되어 있다.
각 요청에 포함된 JWT를 검증하여 사용자의 권한을 확인하고, 보안적인 접근을 보장할 수 있다
public TokenProvider(CustomUserDetailsService customUserDetailsService,
@Value("${jwt.secret}") String secret) {
this.customUserDetailsService = customUserDetailsService;
this.secret = secret;
this.accessExpirationTime = 3 * 60 * 60 * 1000L; // 3 hours
this.refreshExpirationTime = 15 * 24 * 60 * 60 * 1000L; // 15 days
}
@Override
public void afterPropertiesSet() throws Exception {
// jwt 원시 키를 디코딩하고 jwt 서명하는데 사용
byte[] keyBytes = Decoders.BASE64.decode(secret);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
// Access Token 생성 메서드
public String createAccessToken(String uid) {
Claims claims = Jwts.claims()
.setSubject(uid);
claims.put("token_type", "accessToken");
Date expirationTime = getExpirationTime(accessExpirationTime);
String accessToken = Jwts.builder()
.setClaims(claims)
.setExpiration(expirationTime)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
return accessToken;
}
// 토큰에서 정보 추출
public Authentication getAuthentication(String token) {
String uid = parseClaims(token).getSubject();
UserDetails userDetails = customUserDetailsService.loadUserByUsername(uid);
return new UsernamePasswordAuthenticationToken(userDetails, token);
}
// Access Token 검증
public boolean validateAccessToken(String accessToken) {
try {
Claims claims = parseClaims(accessToken);
// 토큰 타입 확인
String tokenType = (String) claims.get("token_type");
if (!"accessToken".equals(tokenType)) {
return false;
}
// 만료 날짜 확인
return !claims.getExpiration().before(new Date());
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.warn("잘못된 JWT 서명입니다.", e);
} catch (ExpiredJwtException e) {
log.warn("만료된 JWT입니다.", e);
} catch (UnsupportedJwtException e) {
log.warn("지원되지 않는 JWT입니다.", e);
} catch (IllegalArgumentException e) {
log.warn("잘못된 JWT입니다.", e);
}
return false;
}