들어가기 전에
REST API를 개발하는 입장에서 JWT는 한번쯤은 들어봤거나 JWT를 사용해서 인증을 하는 작업을 해보셨을 거라 생각합니다. 이번 글에서는 JWT에 대한 개념과 JWT를 어떻게 사용하면 좋을지에 대해서 다뤄보도록 하겠습니다.
01. JWT
1.1 개념
클라이언트와 서버 사이에서 통신할 때 권한을 위해 사용하는 토큰입니다. Self-contained 방식으로 JWT안에 인증에 필요한 모든 정보를 자체적으로 지니고 있는 특징이 있습니다.
JWT를 사용하는 부분에 대해서도 문제점이 있긴 하지만 Cookie또는 Session 기반 인증에서 문제점이 드러났기 때문에 JWT를 많이 사용합니다. (그렇다고 해서 JWT기반 인증 시에 Cookie를 사용하지 않는 것은 아닙니다!)
Cookie의 경우에는 말그래도 Cookie에 실제 인증에 필요한 값을 저장해서 사용하게 되는데 이는 보안에 취약하다는 단점이 있습니다. 쿠키 값이 조작되거나 유출될 가능성이 있고 용량제한이 있어 많은 정보를 담을 수 없다는 것이 단점입니다. 물론 조작면에서 HTTP ONLY 값을 설정하면 방지할 수 있지만 이는 client가 server에 요청할 시에 header안에 값을 넣어서 보내줄 수 없습니다.
Session 기반 인증은 클라이언트 식별자인 JSESSIONID를 쿠키에 담아서 요청을 보내는 방식인데 매 요청마다 서버는 세션 저장소를 뒤져 인증을 하기 때문에 요청이 많을 시에 서버에 부하가 심하다는 단점이 있습니다.
1.2 JWT 구조
JWT 구조는 위와 같습니다. 크게 Header, Payload, Signature 3개의 파트로 나누어집니다.
Header
일반적으로 Token 유형과 서명 알고리즘으로 구성됩니다.
{
"alg": "HS256",
"typ": "JWT"
}
Payload
Payload는 상태값 또는 추가적인 정보가 담겨져 있는 파트입니다. 아래의 key값들에 대해서 value값을 지정하거나 개발자가 임의의 값을 담아 payload에 담을 수 있습니다. 여기서 추가적인 정보라는 것은 token을 생성할 시에 유저에 대한 데이터가 필요할 경우 그 값에 대해 key-value로 담을 수 있다는 것입니다.
- sub: 인증 주체(subject)
- iss: 토큰 발급처
- typ: 토큰의 유형(type)
- alg: 서명 알고리즘(algorithm)
- iat: 발급 시각(issued at)
- exp: 말료 시작(expiration time)
- aud: 클라이언트(audience)
Signature
Header에 명시된 서명 알고리즘을 통해 생성된 signature가 담기는 파트입니다. signature는 base64UrlEncode를 통해 나온 header와 payload, secretKey(개발자가 생성)를 기반으로 생성됩니다. 이 signature를 통해서 변조된 토큰인지 확인할 수 있습니다.
HMACSHA256(
base64UrlEncode(header) +"."+ base64UrlEncode(payload),
"s6v8y/B?E(H+MbQeThWmZq4t7w!z$C&F" // secretKey
)
실제로 서버에서 JWT에 대한 유효성 검사를 할 경우 아래의 signature에 대한 부분도 확인을 해야합니다.
client에서 server로부터 Signature에 대한 에러를 반환받을 경우 client에서는 JWT에서 마지막 파트에 해당하는 signature 부분이 이상하거나 잘못된 값을 담아 요청했다는 것을 알 수 있습니다.
public JwtValidationType validateToken(String token) {
try {
Jwts.parser().setSigningKey(getJwtSecretKey()).parseClaimsJws(token);
return JwtValidationType.VALID_JWT;
} catch (SignatureException ex) {
return JwtValidationType.INVALID_JWT_SIGNATURE;
} catch (MalformedJwtException ex) {
return JwtValidationType.INVALID_JWT_TOKEN;
} catch (ExpiredJwtException ex) {
return JwtValidationType.EXPIRED_JWT_TOKEN;
} catch (UnsupportedJwtException ex) {
return JwtValidationType.UNSUPPORTED_JWT_TOKEN;
} catch (IllegalArgumentException ex) {
return JwtValidationType.EMPTY_JWT;
}
}
1.3 AccessToken과 RefreshToken
AccessToken은 말그대로 서버에 요청을 보낼 때 인증 역할을 하는 token입니다. 허나 이 보안적인 부분에서 AccessToken 만료기간은 짧아야 하는데 그 경우에 유저들은 잦은 로그인을 해야하는 불편함을 겪어야 합니다. 그래서 나온 것이 RefreshToken입니다. RefreshToken을 통해서 AccessToken이 만료될 경우 재발급을 할 수 있도록 합니다. 즉 한동안 로그인을 하지 않았을 때 재로그인을 해야하는 기간은 RefreshToken의 유효기간 설정에 따라 결정됩니다.
02. JWT 기반 인증 방식
2.1 기본 동작 방식
아래의 그림처럼 기본적인 동작 방식을 따르는데 그 안에 세부적인 값들을 설정을 해줘야 합니다. accessToken과 refreshToken에 대해서 만료기간을 어떻게 설정할 것인지 응답을 줄 때에는 어떻게 할 것인지에 대한 부분이 필요합니다.
2.2 JWT 기반 인증 과정 설정
Token Expiration 설정
저는 보통 AccessToken의 ExpirationTime을 2시간, RefreshToken의 ExpirationTime을 2주로 설정합니다. 이는 개발자마다 차이가 있을 거라 생각하지만 공통된 점은 AccessToken의 만료시간을 짧게 설정한다는 것입니다.
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
private static final int ACCESS_TOKEN_EXPIRATION_TIME = 7200000; // 2 hour
private static final int REFRESH_TOKEN_EXPIRATION_TIME = 1209600000; // 14 * 24 * 60 * 60 * 1000 = 2 weeks
private final Environment env;
public TokenDto generateAccessToken(Authentication authentication) {
Date expiredTime = getExpirationDate(ACCESS_TOKEN_EXPIRATION_TIME);
String token = Jwts.builder()
.setSubject(String.valueOf(authentication.getPrincipal()))
.setIssuedAt(new Date())
.setExpiration(expiredTime)
.signWith(getJwtSecretKey(), SignatureAlgorithm.HS512)
.compact();
return TokenDto.builder()
.value(token)
.expiredTime(expiredTime)
.build();
}
public TokenDto generateRefreshToken(Authentication authentication) {
Date expiredTime = getExpirationDate(REFRESH_TOKEN_EXPIRATION_TIME);
String token = Jwts.builder()
.setSubject(String.valueOf(authentication.getPrincipal()))
.setIssuedAt(new Date())
.setExpiration(getExpirationDate(REFRESH_TOKEN_EXPIRATION_TIME))
.signWith(getJwtSecretKey(), SignatureAlgorithm.HS512)
.compact();
return TokenDto.builder()
.value(token)
.expiredTime(expiredTime)
.build();
}
AccessToken과 RefreshToken 전달방식
처음에 소셜로그인을 하게 되면 응답으로 AccessToken을 준다. RefreshToken도 같이 응답에 넣어서 보내줘도 되는데 저는 보통 RefreshToken을 쿠키에 저장해서 사용합니다.
아래는 처음 Login을 했을 때 기준입니다. response에 AccessToken을 담아서 보내주지만 RefreshToken은 HTTP_ONLY라는 String 값을 담아 보내줍니다.
@Transactional
public LoginResponseDto createLoginMember(Member member, HttpServletResponse response) {
Authentication authentication = new UserAuthentication(member.getId(), null, null);
TokenDto accessToken = jwtTokenProvider.generateAccessToken(authentication);
TokenDto refreshToken = jwtTokenProvider.generateRefreshToken(authentication);
member.updateRefreshToken(refreshToken);
memberRepository.save(member);
setCookieWithRefreshToken(response, refreshToken);
JwtTokenDto jwtTokenDto = JwtTokenDto.builder()
.accessToken(accessToken.getValue())
.refreshToken(REFRESH_TOKEN_SECURE_MESSAGE)
.build();
MemberResponseDto memberResponseDto = MemberResponseDto.builder()
.member(member)
.build();
List<TapeResponseDto> tapes = member.getTapes().stream().map(TapeResponseDto::new).toList();
return LoginResponseDto.builder()
.jwtInformation(jwtTokenDto)
.memberInformation(memberResponseDto)
.tapes(tapes)
.build();
}
RefreshToken의 경우에는 Cookie에 담습니다.
private void setCookieWithRefreshToken(HttpServletResponse response, TokenDto refreshToken) {
ResponseCookie cookie = ResponseCookie.from("refreshToken", refreshToken.getValue())
.path("/")
.secure(true)
.httpOnly(true)
.domain("12playlist.com")
.maxAge(14*24*60*60)
.build();
response.setHeader("Set-Cookie", cookie.toString());
}
딱 하나 조심해야 될 점은 RefreshToken이 만료될 경우 재발급을 해줘야 하는 경우입니다. Cookie는 같은 key값에 대한 value값을 새로운 값으로 업데이트를 할 수 없기 때문에 삭제를 하고 다시 저장해야합니다.
private void removeRefreshTokenInCookie(HttpServletResponse response, Cookie refreshToken) {
refreshToken.setMaxAge(0);
refreshToken.setDomain("12playlist.com");
refreshToken.setPath("/");
response.addCookie(refreshToken);
}
03. 중요한 포인트
1. JWT 구조에 대해서 설명해주세요.
-> JWT는 Header, Payload, Signature 3개의 파트로 이루어 집니다.
2. JWT를 사용하는 이유에 대해서 cookie와 session 기반 인증방식과 같이 설명해주세요.
-> Cookie의 경우에는 말그래도 Cookie에 실제 인증에 필요한 값을 저장해서 사용하게 되는데 이는 보안에 취약하다는 단점이 있습니다. 쿠키 값이 조작되거나 유출될 가능성이 있고 용량제한이 있어 많은 정보를 담을 수 없다는 것이 단점입니다. 물론 조작면에서 HTTP ONLY 값을 설정하면 방지할 수 있지만 이는 client가 server에 요청할 시에 header안에 값을 넣어서 보내줄 수 없습니다.
Session 기반 인증은 클라이언트 식별자인 JSESSIONID를 쿠키에 담아서 요청을 보내는 방식인데 매 요청마다 서버는 세션 저장소를 뒤져 인증을 하기 때문에 요청이 많을 시에 서버에 부하가 심하다는 단점이 있습니다.
JWT는 token의 유효기간을 짧게 설정해서 보안을 높이고, cookie에 token을 저장하고 요청을 할 때 사용하기 때문에 서버의 부담을 감소화할 수 있습니다.
3. AccessToken과 RefreshToken의 역할에 대해서 설명해주세요.
-> AccessToken은 서버에 요청을 보낼 때 인증 역할을 하는 token입니다. 허나 이 보안적인 부분에서 AccessToken 만료기간은 짧아야 하는데 그 경우에 유저들은 잦은 로그인을 해야하는 불편함을 겪어야 합니다. 그래서 나온 것이 RefreshToken입니다.
RefreshToken을 통해서 AccessToken이 만료될 경우 재발급을 할 수 있도록 합니다.
4. Token기반 인증방식과정에 대해서 설명해주세요.
02. JWT 기반 인증 방식 파트의 이미지를 참고해주세요.
04. 레퍼런스
https://velog.io/@hahan/JWT%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80
JWT란 무엇인가?
JWT(Json Web Token) > 정보를 비밀리에 전달하거나 인증할 때 주로 사용하는 토큰으로, Json객체를 이용함 JWT는 Json Web Token의 약자로 일반적으로 클라이언트와 서버 사이에서 통신할 때 권한을 위해
velog.io
https://velog.io/@jkijki12/Jwt-Refresh-Token-%EC%A0%81%EC%9A%A9%EA%B8%B0
Jwt Refresh Token 적용기
오늘은 이전에 포스팅한 jwt 적용기의 2편이다.문제인식해결방법구현Access Token을 적용하고 아주 큰? 문제를 발견했다.보안 상으로 Access Token은 매우 짧은 만료기간을 가지고 있다. 그래서 사용자
velog.io
https://tecoble.techcourse.co.kr/post/2021-05-22-cookie-session-jwt/
인증 방식 : Cookie & Session vs JWT
1. HTTP 특성 HTTP는 인터넷 상에서 데이터를 주고 받기 위한 서버/클라이언트 모델을 따르는 프로토콜입니다. 클라이언트가 서버에게 요청을 보내면 서버는 응답을 보냄으로써, 데이터를 교환합
tecoble.techcourse.co.kr
https://targetcoders.com/jwt-%EA%B5%AC%EC%A1%B0/
JWT 구조 이해하기 - 타깃코더스
목차JWT란?JWT 구조헤더 (Header)페이로드 (Payload)시그니처 (Signature)참고 자료 JWT는 토큰 기반 인증 방식에서 주로 사용되는 토큰입니다. 주로 로그인 된 특정 사용자를 인증하기 […]
targetcoders.com
GitHub - twentiethcenturygangsta/cassette-server
Contribute to twentiethcenturygangsta/cassette-server development by creating an account on GitHub.
github.com