💡 JWT란
두 당사자 간의 클레임을 안전하게 표현하기 위한 개방형 업계 표준 RFC 7519 방법
why JWT?
Client에서stateless
환경을 유지하는 방법을 고려하던 중 쉽게인증/인가
가 가능하며 정보 교환을 할 수 있는 JWT가 확산되었다. 뿐만아니라 다양한 플랫폼과 기술에서 사용할 수 있고, JSON 형식이므로 대부분의 개발자들이 익숙하고 다루기 쉽다. 이 같은 이유로 나또한 JWT를 프로젝트에 적용했다.
+ JWT 토큰은 클라이언트에 저장되기 때문에 서버 확장에 용이하다.
Header: 사용된 알고리즘과 token type이 저장되어 있음
PAYLOAD: 저장하고 싶은 Data들
VERIFY SIGNATURE: 올바른 Token인지 인증
나는 Service Directory에 Token Service를 추가하여 아래의 메소드들을 수정해서 구현했다.
크게 어려운 내용은 없으니 천천히 읽어보면 충분히 적용할 수 있을 것이다.
📂 디렉터리 구조
├─main
├─java
│ └─com
│ └─Project
│ │ Project.java
│ │
│ ├─domain
│ │ └─user
│ │ ├─controller
│ │ │ MemberController.java
│ │ │ TokenController.java
│ │ │
│ │ ├─dto
│ │ │ MemberAccessDto.java
│ │ │
│ │ ├─entity
│ │ │ Member.java
│ │ │
│ │ ├─ouath
│ │ │ OAuth2SuccessHandler.java
│ │ │ Token.java
│ │ │
│ │ ├─repository
│ │ │
│ │ └─service
│ │ CustomOAuth2UserService.java
│ │ MemberService.java
│ │ OAuth2Attribute.java
│ │ TokenService.java
│ │
│ └─global
│ ├─config
│ │ RedisConfig.java
│ │ SecurityConfig.java
│ │
│ ├─error
│ │ │ ErrorCode.java
│ │ │ ErrorResponse.java
│ │ │ GlobalExceptionHandler.java
│ │ │
│ │ └─exception
│ │ AccessDeniedException.java
│ │ NotFoundException.java
│ │
│ ├─filter
│ │ JwtAuthFilter.java
│ │
│ ├─response
│ BaseResponse.java
│
│
│
│
└─resources
application-env.yml
application.yml
generateToken - 새로운 토큰을 생성한다.
public Token generateToken(Long uid, String role) {
long tokenPeriod = Long.parseLong(
env.getProperty("jwt.access-token.expire-length")); // 30 min
long refreshPeriod = Long.parseLong(
env.getProperty("jwt.refresh-token.expire-length")); // 2 week
Claims claims = Jwts.claims().setSubject(uid.toString());
claims.put("role", role);
Date now = new Date();
//uid 정보가 포함된 token 발행
return new Token(
Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + tokenPeriod))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact(),
Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + refreshPeriod))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact());
}
verifyToken - 토큰의 유효성을 체크한다.
public boolean verifyToken(String token, HttpServletResponse response)
throws Exception {
try {
Jws<Claims> claims = Jwts.parser()
.setSigningKey(secretKey) //개발자가 추가한 secretKey를 통해 token 정보 확인
.parseClaimsJws(token);
return claims.getBody()
.getExpiration() //저장된 유효기간 확인
.after(new Date());
} catch (ExpiredJwtException e) {
((HttpServletResponse) response).sendError(ErrorCode.EXPIRED_ACCESSTOKEN.getCode(), ErrorCode.NOT_AUTHENTICATION.getMessage());
throw new AccessDeniedException(ErrorCode.EXPIRED_ACCESSTOKEN.getMessage());
}
...
}
getUid, getRole - 토큰에 저장된 정보를 가져온다.
public Long getUid(String token) {
return Long.valueOf(
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject());
}
public String getRole(String token) {
return (String) Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody()
.get("role");
}
SSAFY 2학기 공통 프로젝트. 백엔드 구현에서 회원관련 파트를 담당하게 되었다. 로그인을 진행한 후 사용자 정보를 가져올 때 로컬스토리지에 사용자의 모든 정보를 담는 경우는 없을 것이다.
그래서 Server와 Client에서 토큰을 주고받는 방식을 생각해봤다.
많은 사이트들을 찾아보면서 느낀것은 토큰 저장은 정말 사람마다 다르다는 것.
보통 AT(Access Token)를 로컬스토리지 혹은 쿠키에 저장하고 RT(Refresh Token)를 쿠키(httponly)에 저장한다.
내가 처음 구현한 방식은 store에 AT를 저장하는 방식이었다. store에 저장할 때 가장 큰 문제점은 새로고침하는 경우 store가 초기화 된다는 것이다. 이를 해결하기 위해 vuex-persistedstate 등과 같이 로컬스토리지에 저장한 후 가져오는 방법을 생각해봤지만 이러면 결국 store에 저장하는 의미가 사라지게 된다. 결국 새로고침할 때의 문제점을 개선하기 위해 페이지 변경이 있을 때마다 AT를 확인 및 재발급하였다.
AT의 단점인 짧은 인증 주기를 보완하기 위해 Refresh Token을 사용하게 되었다.
RT은 AT의 재발급을 위해 사용되기 때문에 AT와 마찬가지로 보안이 매우 중요하다. RT를 탈취당하게 되면 AT를 사용하는 이유가 사라지기 때문이다.
Refresh Token를 활용하여 만료기간이 짧은 Access token을 재발급 해주어 정상 응답을 받을 수 있게 해준다.
프로젝트를 진행하면서 AT는 Store에 변수로 저장하였고, RT는 cookie에 저장하였다.
응답이 있을 때마다 AT를 확인한 후 만료가 된 AT면 cookie에 있는 RT를 통해 AT를 재발급 해주었다.
이 방법을 통해 로그인 유지를 구현할 수 있게 되었고, 사용자 정보 또한 숨겨 보안을 강화 하였다.
두 번째 프로젝트를 하면서 소셜 로그인을 다시한번 담당하게 되었다. 첫 프로젝트 때 부족했다고 생각한 부분들이 많아서 보완하는 것을 목표로 하였다.
- /api/token/refresh를 활용하여 access token 재발급
- cookie의 path를 재발급 경로만으로 설정하여 cookie를 숨김.
- RT를 DB, Redis에 저장하여 올바른 사용자인지 확인.
- 토큰으로 구분 역할
위와 같은 방법으로 access token을 재 발급 하였다.
cookie.setPath("/refresh")를 사용하여 access token을 재발급해야할 상황이 아닌경우 cookie를 숨김.
DB와 Redis를 모두 고민하였다. 하지만 DB를 사용하게 되면 refresh가 되는 모든 상황마다 DB에 접근해야 한다. 이는 효율적이지 못한 방법이라고 생각 되었고, inMemory Cache인 Redis에 Refresh Token을 저장하도록 구현하였다. + 로그아웃을 할 때는 Redis에서 삭제하여 cookie와 redis 에서 모두 없는 경우로 만들어 주었다.
회원관리부분 뿐만아니라 다른 api기능 및 Front 부분을 담당하게 되었기에 완료하지는 못했다.
그래도 많이 보고 배우면서 Develop할 수 있어서 매우 좋은 경험을 했다고 생각한다.
프론트에서 아래와 같이 header에 토큰정보를 보내고 만료 후 refresh token으로 재발급 받는 상황에서 cookie를 받지 못하는 상황이 발생함
instance.interceptors.request.use(
function (config) {
// Do something before request is sent
const Authorization = localStorage.getItem("Authorization");
if (Authorization) {
config.headers.Authorization = Authorization;
}
return config;
},
function (error) {
// Do something with request error
return Promise.reject(error);
}
);
서버에서 rquest.getCookies를 하면 항상 null 값이 들어옴
chatGPT나 검색을 해봐도 찾는데 오래걸림.. 같은 도메인에, port만 다르고 url도 다 맞춰줬는데 못찾아서 한참 헤맸다..
문제는 port만 달라도 cross-origin이라 클라이언트와 서버 모두 설정해줘야 한다는 것!
=> Cookie가 null인 경우 해결한 방법