이전 글에서 정리한 JWT 토큰을 보관하는 방법 중 accessToken을 로컬 변수에 저장하고, refreshToken만 쿠키에 저장하는 방법으로 직접 구현해보려고 한다.
간단하게 구현방법을 요약해보면 다음과 같다.
secure
httpOnly
플래그 설정이 된 쿠키에 저장한다secure
httpOnly
플래그 설정이 된 쿠키에 저장되어 있기 때문에 javascript(XSS)로 데이터를 탈취할 수 없음✅ 로그인
secure
httpOnly
쿠키에 담고, accessToken을 response body에 담아 반환한다✅ accessToken이 사라졌을 때 & accessToken이 만료되었을 때
secure
httpOnly
가 설정된 cookie에 refresh token을 담고, response body에 accessToken을 담아 반환/refresh
과 같은 엔드포인트를 만듦 - 여기서는 사용자 요청 쿠키에서 refresh token을 꺼내 검증 후 새로운 refresh token을 쿠키에 설정하고 body로 새로운 accessToken을 반환/refresh
엔드포인트에 refreshToken과 accessToken 재발급 요청@PostMapping("/login")
public ResponseEntity<AuthTokenResponse> login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) {
User user = userService.login(loginRequest);
//user 테이블에 저장된 refreshToken 업데이트
String refreshToken = JwtUtil.createJwt(secretKey, expiredRefreshMs, user.getEmail());
user.updateRefreshToken(refreshToken);
userService.updateRefreshToken(user);
//`secure` `httpOnly`가 설정된 cookie에 refresh token을 담음
Cookie cookie = new Cookie("refresh-token", refreshToken);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setSecure(true);
response.addCookie(cookie);
// response body에 accessToken을 담아 반환
String accessToken = JwtUtil.createJwt(secretKey, expiredJwtMs, user.getEmail());
return ResponseEntity.ok(new AuthTokenResponse(accessToken));
}
@GetMapping("/refresh")
public ResponseEntity<AccessTokenResponse> getAccessToken(HttpServletRequest request) {
//사용자 요청 쿠키에서 refresh token을 꺼내 검증 후 새로운 refresh token을 쿠키에 설정하고 body로 새로운 accessToken을 반환
String refreshToken = getRefreshToken(request);
String memberEmail = JwtUtil.getMemberEmail(refreshToken, secretKey);
String accessToken = JwtUtil.createJwt(secretKey, expiredJwtMs, memberEmail);
return ResponseEntity.ok(new AccessTokenResponse(accessToken));
}
@GetMapping("/test")
public ResponseEntity<String> getInfo(@RequestHeader HttpHeaders header) {
//클라이언트 API 요청이 오면 Authrization header에 담긴 accessToken의 유효성을 검사
validateAccessToken(header);
return ResponseEntity.ok("hi lime!");
}
private void validateAccessToken(HttpHeaders header) {
String accessToken = header.getFirst("Authorization");
//만약 유효하지 않은 토큰이 오면 401을 반환
if (accessToken == null || JwtUtil.isExpired(accessToken, secretKey))
throw new UnAuthorizationException("유효하지 않은 토큰");
}
async login() {
axios.defaults.withCredentials = true;
try {
const response = await axios.post('http://localhost:80/login', {
id: this.username,
password: this.password
});
if (response.status == 200) {
const { accessToken } = response.data;
//엑세스 토큰을 axios headers 기본값으로 설정(로컬 변수)
axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
this.$router.push('/');
} else {
this.errorMessage = 'Invalid username or password';
}
} catch (error) {
this.errorMessage = '에러발생!';
console.error(error);
}
}
async apiCall() {
axios.defaults.withCredentials = true;
//accessToken이 없다면 refreshToken으로 accessToken 갱신
if (!axios.defaults.headers.common['Authorization']) {
console.log("accessToken이 사라짐, refreshToken으로 재발급")
await this.refreshAccessToken();
}
await this.fetchData();
}
async refreshAccessToken() {
try {
const response = await axios.get('http://localhost:80/refresh');
if (response.status === 200) {
const { accessToken } = response.data;
axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
console.log("성공");
return true;
} else {
console.log('리프레쉬 토큰이 없거나 만료됨');
return false;
}
} catch (error) {
console.log('리프레쉬 토큰을 가져오는 중 오류 발생');
return false;
}
}
async fetchData() {
try {
console.log("/test요청")
const response = await axios.get('http://localhost:80/test');
this.servermsg = response.data;
} catch (error) {
//accessToken이 만료되었을 때 리프레쉬 토큰으로 accessToken 재발급
if (error.response && error.response.status === 401) {
console.log("접근 토큰이 만료됨, 리프레쉬 토큰 사용 시도");
if (await this.refreshAccessToken()) {
const response = await axios.get('http://localhost:80/test');
this.servermsg = response.data;
} else {
console.log('리프레쉬 토큰 만료, 로그인 필요');
this.$router.push('/about');
}
} else {
console.log('로그인 필요');
this.$router.push('/about');
}
}
}
프론트엔드 지식이 부족해서 로컬변수에 어떻게 깔끔하게 저장해야하는지 몰랐는데, 직접 구현해보니 어떻게 로직이 돌아가는지 확실히 이해할 수 있었다.
역시 직접 만들어보는게 도움이 많이 되는 것 같다.