지난번엔 곽두철 프로젝트에 카카오 로그인을 도입했었다.
이번엔 JWT를 적용해 보겠다.
서버가 클라이언트 인증을 확인하는 방식은 크게 3가지가 있다.
- 쿠키
- 세션
- 토큰
이 중에서 토큰 방식을 사용해 볼 건데, 장단점이 무엇이 있는지 알아보겠다.
장점
토큰 인증 방식은 클라이언트가 서버에 접속하면 서버에서 인증을 위한 토큰을 부여하는 방식이다.
이 토큰은 유일하며, 클라이언트는 요청시 헤더나 쿠키에 심어서 서버에 보낸다.
서버는 받은 토큰을 유효한지 확인한다.
이 방법은 토큰 자체에 데이터를 저장하고 있어 DB를 확인하지 않아도 된다.
단점
- 쿠키 / 세션과 다르게 토큰 자체의 데이터 길이가 길어서, 인증 요청이 많아질수록 네트워크 부하가 심해질 수 있다.
- Payload 자체는 조회가 가능하기 때문에 유저의 중요한 정보를 담을 수 없다.
- 토큰은 발급하면 만료될 때까지 계속 사용이 가능하기 때문에 토큰이 탈취당하면 대처하기가 어렵다.
그래서 JWT는 뭐야?
JWT는 크게 3가지로 이루어져있다.
Header, Payload, Signature가. 을 기준으로 나뉜다.
Header
{
"alg": "HS256",
"typ": "JWT"
}
헤더에서는 해시 알고리즘과 토큰의 타입을 정의할 수 있다.
위 예시는 알고리즘은 HS256을 사용하고 있고, 타입은 JWT라는 뜻이다.
Payload
{
"sub": "12343214",
"name": "Seonjun",
"exp": 1516239022
}
Payload 부분에는 토큰에 담을 정보가 들어있다.
여기에 담는 정보의 한 ‘조각’을 클레임(Claim)이라고 부르고, 이는 Json(Key/Value) 형태의 한 쌍으로 이뤄져 있습니다.
토큰에는 여러 개의 클레임들을 넣을 수 있습니다.
Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Signature은 토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드이다.
Signature은 위에서 만든 헤더(Header)와 페이로드(Payload)의 값을 각각 BASE64로 인코딩하고, 인코딩한 값을 비밀 키를 이용해 헤더(Header)에서 정의한 알고리즘으로 해싱을 하고, 이 값을 다시 BASE64로 인코딩하여 생성한다.
토큰 인증 방식 흐름
- 클라이언트는 로그인을 한다.
- 서버는 토큰을 발급한다.
- 클라이언트는 서비스 요청을 할 때 발급받은 토큰을 쿠키나 헤더에 담아 요청한다.
- 서버는 토큰이 유효한지 만료되지 않았는지 체크한 후, 만약 유효하지 않은 토큰이면 예외를 터뜨린다.
- 클라이언트에게 만료되었다는 응답을 보낸다.
- 클라이언트는 다시 토큰을 요청한다.
이제 곽두철 프로젝트에선 어떻게 사용되었는지 보자!
의존성 추가
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
jwt 의존성을 추가해 준다.
AuthService
@Service
@RequiredArgsConstructor
@Slf4j
public class AuthService {
private final KakaoLoginTokenClient kakaoLoginTokenClient;
private final KakaoLoginUserClient kakaoLoginUserClient;
private final JwtProvider jwtProvider;
private final UserRepository userRepository;
public String login(final LoginRequest request) {
final KakaoTokenResponse kakaoTokenResponse = kakaoLoginTokenClient.getTokenInfo(request.authorizationCode());
final KakaoUserInfoResponse userInfo = kakaoLoginUserClient.getUserInfo(kakaoTokenResponse.access_token());
final User user = userRepository.findBySocialIdAndSocialType(userInfo.id(), request.socialType())
.orElseGet(() -> initUser(userInfo, userInfo.id()));
final String jwtToken = jwtProvider.createToken(user.getId());
log.info("User 로그인 성공: {} ", user);
return jwtToken;
}
private User initUser(final KakaoUserInfoResponse userInfo, final Long socialId) {
final User user = User.of(socialId.toString(), userInfo.getName());
return userRepository.save(user);
}
}
저번 글에서 본 AuthService이다.
카카오 로그인으로 유저 정보를 가져온 후, DB에서 확인한다.
만약 유저가 존재한다면 바로 JWT를 발급하고, 존재하지 않는다면 DB에 저장한 후, JWT를 발급한다.
JWTProvider
@RequiredArgsConstructor
@Component
public class JwtProvider {
private final SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
public String createToken(final Long id) {
final Date now = new Date();
final Claims claims = Jwts.claims().setSubject(String.valueOf(id));
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + (30 * 60 * 1000L)))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public Long getPayload(final String token) {
String sub = getClaims(token)
.getBody()
.get("sub", String.class);
return Long.parseLong(sub);
}
public Jws<Claims> getClaims(final String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token);
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
public boolean isValidToken(final String jwtToken) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwtToken)
.getBody();
return !claims.getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
}
isValidToken 메서드가 토큰의 유효성을 검사하는 로직이다.
AuthController
@GetMapping("/oauth/kakao")
@ResponseBody
public ResponseEntity<String> login(final LoginRequest loginRequest, HttpServletResponse response) {
final String jwtToken = authService.login(loginRequest);
Cookie cookie = new Cookie("Authorization", jwtToken);
cookie.setHttpOnly(true);
cookie.setMaxAge(24 * 60 * 60);
response.addCookie(cookie);
return ResponseEntity.status(HttpStatus.OK)
.body("JWT 토큰이 생성되었습니다.");
}
JWT는 쿠키 또는 헤더에 담을 수 있는데, 나는 쿠키를 선택했다.
'WEB' 카테고리의 다른 글
@NoArgsConstructor 액세스 레벨을 PROTECTED로 하는 이유 (1) | 2024.06.09 |
---|---|
Redis 내부동작 파헤치기 (0) | 2024.05.05 |
Redis Lock 동시성 해결하기 (0) | 2024.05.01 |
인터셉터와 리졸버 (0) | 2024.04.12 |
Oauth를 사용해 카카오 로그인 구현 (0) | 2024.04.12 |
리사이징 적용기 with Marvin (0) | 2024.03.14 |
Spring Cloud Config 도입기 (0) | 2024.03.08 |
OAuth 2.0 동작 방식 (0) | 2023.06.05 |