개요
일단 JWT에 대한 개념부터 알고가자!
JWT는 JSON Web Token의 약자로, 로그인 인증 기능에서 사용하는 토큰을 만들 때 사용하는 기술이다.
JWT를 구현하는 방법은 두 가지이다.
1. jwt-java 라이브러리를 사용하여 JWT를 생성하고 파싱하는 방법
2. spring security 라이브러리 이용하기
이번에 로그인 로직을 짤 때 일반 로그인은 1번으로, 소셜 로그인은 2번 로직으로 짜게 되어 JWT와 많이 친해질 수 있었다.
이번 글은 1번 방법에 대한 설명이다. 2번 방법(JWT+소셜로그인) 에 대한 설명은 이쪽에서 확인 가능하다.
UMC 동아리에서 제공해준 템플릿 중 로그인 구현 방법 코드를 인용하였다. 아래 코드로 자세히 알아보자.
service/JwtService.java
@Service
public class JwtService {
private Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
/*
JWT 생성
@param userIdx
@return String
*/
public String createJwt(Long userIdx){
Date now = new Date();
return Jwts.builder()
.setHeaderParam("type","jwt")
.claim("userIdx",userIdx)
.setIssuedAt(now)
.setExpiration(new Date(System.currentTimeMillis()+1*(1000*60*60*24*365)))
.signWith(key)
.compact();
}
public String createRepJwt(int repInx){
Date now = new Date();
return Jwts.builder()
.setHeaderParam("type","jwt")
.claim("repInx",repInx)
.setIssuedAt(now)
.setExpiration(new Date(System.currentTimeMillis()+1*(1000*60*60*24*365)))
.signWith(key)
.compact();
}
/*
Header에서 X-ACCESS-TOKEN 으로 JWT 추출
@return String
*/
public String getJwt(){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
return request.getHeader("Authorization");
}
/*
JWT에서 userIdx 추출
@return int
@throws BaseException
*/
public Long getmemberId() throws ResponseException {
//1. JWT 추출
String accessToken = getJwt();
if(accessToken == null || accessToken.length() == 0){
throw new ResponseException(EMPTY_JWT);
}
// 2. JWT parsing
Jws<Claims> claims;
try{
claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(accessToken);
} catch (Exception ignored) {
throw new ResponseException(INVALID_JWT);
}
// 3. userIdx 추출
return claims.getBody().get("userIdx",Long.class); // jwt 에서 userIdx를 추출합니다.
}
}
JWT의 가장 큰 편리한 특징은 토큰으로 이루어진다는 것이다. 아래는 Token으로 회원을 인증하는 방식이다.
하지만 가장 취약한 점도 토큰으로 이루어진다는 점인데 토큰은 세션과 달리 클라이언트에 정보를 가지고 있기 때문이다. 이 점은 어쩔 수 없고, 사용 기간 제한을 설정하는 식으로 극복한다.
Access token은 탈취될 가능성이 높으므로 보통 30분~2시간 으로 사용 기간을 제한한다. 반면에 refresh token은 아예 유효기간을 두지 않는 경우도 있고 한 달~3개월로 두는 곳도 있다고 한다.
* 참고) 위에 코드에서는 refresh token은 따로 구현하지 않았습니다 !!
유효기간
위 코드의 이 쪽 부분에 Date를 보면 1년을 기준으로 accessToken에 대한 유효기간을 설정한 것을 볼 수 있다. claims의 내용으로 userIdx 를 저장할 수 있으며, Key 객체를 이용하여 JWT를 암호화한다.
이 때 Key는 Spring 낮은 버전에서는 상수로 할당 가능하였으나, 이제는 deprecated되었으므로 Key 라이브러리를 이용해 할당해주어야 한다.!
private Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
그리고 JWT는 userIdx를 기반으로 만들었다. 위에서 JWT는 취약하다고 했다.! 물론 암호화가 되어있지만, payload를 복호화한다면 정보가 탈취될 가능성도 있기에 memberId와 같은 민감하지 않은 정보를 JWT에 넣어두면 좋다.
return Jwts.builder()
.setHeaderParam("type","jwt")
.claim("userIdx",userIdx)
.setIssuedAt(now)
.setExpiration(new Date(System.currentTimeMillis()+1*(1000*60*60*24*365)))
.signWith(key)
.compact();
JWT 추출 메소드
Header에 Authorization 부분에 AccesssToken을 넣으면 이 JWT을 추출할 수 있다.
/*
Header에서 X-ACCESS-TOKEN 으로 JWT 추출
@return String
*/
public String getJwt(){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
return request.getHeader("Authorization");
}
서버 개발자라면, JWT를 Postman으로 확인하기 위해 다음과 같이 실습해 볼 수 있다.
JWT 파싱
getmemberId()에서는 JWT를 파싱하여 userIdx 값을 추출한다. 위에서 우리가 userIdx를 기반으로 JWT를 만들었으니 당연히 파싱하면 userIdx가 나올 거다.
// 2. JWT parsing
Jws<Claims> claims;
try{
claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(accessToken);
} catch (Exception ignored) {
throw new ResponseException(INVALID_JWT);
}
사용 예시
controller/memberController.java
요즘에는 거의 JWT를 활용하여 현재 앱/웹을 사용하는 사용자의 id를 따로 받지 않고 서버를 구현하는 경우가 많다. 아래 처럼 사용자의 이름을 변경하고자할 때 jwtService 코드를 활용하여 사용할 수 있다.
아래 api는 위 Postman에서 활용한 url, method와 동일하다.
/**
* 유저정보변경 API
* [PATCH] /members/:memberId
*/
@ResponseBody
@PatchMapping("/{memberId}")
public ResponseTemplate<String> modifyUserName(@PathVariable("memberId") Long memberId, @RequestBody UpdateNickNameReq updateNickNameReq) {
try {
//jwt에서 idx 추출.
Long userIdxByJwt = jwtService.getmemberId();
//userIdx와 접근한 유저가 같은지 확인
if(memberId != userIdxByJwt){
return new ResponseTemplate<>(NO_JWT);
}
memberService.modifyNickName(updateNickNameReq);
String result = "회원정보가 수정되었습니다.";
return new ResponseTemplate<>(result);
} catch (ResponseException exception) {
return new ResponseTemplate<>((exception.getStatus()));
}
}
결론
JWT를 구현하고 사용해보니, 이 토큰이 REST API 통신의 무상태성(Stateless)를 극복할 수 있는 강력한 도구로써 빛을 발하는 것을 느낄 수 있었다.
HTTP는 기본적으로 Statless하다. 즉, 서버는 단순히 요청이 오면 응답을 보내는 역할만을 수행하면 되고 상태관리는 전적으로 클라이언트가 맡게 된다. 따라서 서버 확장에 용이하나 Stateless는 매번 요청할 때마다 자신의 부가정보를 줘야하므로 더 많은 데이터가 소모된다는 특징이 있다.
그래서 stateless 특징을 유지하면서도 로그인 상태 유지를 가능하게 하기 위해 JWT 토큰을 사용하는 등의 방법으로 REST API 의 Stateless 한계를 극복할 수 있게 되는 것이다 ! 👍👍
다음에는 Spring Security와 JWT를 함께 활용하여 인증 기능을 처리할 수 있는 과정에 대해 알아보자 !
'BackEnd > JAVA\SPRING' 카테고리의 다른 글
[Spring] Spring으로 JWT 구현하기2 - Spring Security 라이브러리 (0) | 2023.02.11 |
---|---|
[SPRING] Swagger, 프론트와의 소통을 편하게 하는 자동 툴 (2) | 2023.02.11 |
[SPRING] SPRING 기초, SPRINGBOOT 프로젝트 폴더 구조 (0) | 2023.01.15 |
[JAVA] 우테코 2주차 과제 - 숫자 야구 게임 (0) | 2022.11.08 |
[JAVA] 우아한테크코스 5기 지원 및 1주차 과제 (1) | 2022.10.30 |