JWT를 이용한 인증 처리 6. Refresh Token 구현
- -
이번 포스트에서는 기존의 예제에 refresh token 개념을 추가해보자.
Server Side 추가/수정
먼저 Server Side에서 추가 또는 수정해야 할 내용들이다.
JwtUtil
refresh token을 발행하기 위한 메서드를 추가한다.
/**
* Refresh 토큰을 생성한다
* 이때는 인증을 위한 정보는 유지하지 않고 유효기간을 auth-token의 5배로 잡았다.
* @return
*/
public String createRefreshToken() {
return create(null, "refreshToken", expireMin * 5);
}
UserService
로그인 시 auth-token과 함께 refresh-token도 생성한다. 이 정보는 사용자 계정과 함께 DB에 저장되는게 맞지만 편의상 Map에서 관리한다.
public User signin(String email, String password) {
if (email.equals("member@quietjun.xyz") && password.equals("1234")) {
// 인증 성공 시 authToken 생성
String authToken = jwtUtil.createAuthToken(email);
// refresh token도 함께 생성한다.
String refreshToken = jwtUtil.createRefreshToken();
// 계정 정보와 함께 refresh token을 저장한다 - 인증 정보는 저장되지 않는다.
saveRefreshToken(email, refreshToken);
return User.builder().email(email).authToken(authToken).refreshToken(refreshToken).build();
} else {
throw new RuntimeException("그런 사람은 없어요~");
}
}
/**
* 사용자 인증 정보를 Map에 저장한다. 실제로는 DB에 저장할 것
*/
Map<String, String> refreshTokens = new HashMap<>();
/**
* 사용자의 토큰 정보 저장
*
* @param userId
* @param refreshToken
*/
public void saveRefreshToken(String email, String refreshToken) {
refreshTokens.put(email, refreshToken);
}
public String getRefreshToken(String email) {
return refreshTokens.get(email);
}
추가로 logout 시 로그인 과정에서 저장한 refresh token 정보를 지워줘야할 책임도 생겼다.
public void logout(String email) {
refreshTokens.remove(email);
}
UserRestController
컨트롤러에는 refresh 요청이 들어왔을 때 1차로 전달된 refresh 토큰이 valid 하다면 2차로 저장된 refresh 토큰과 비교해서 역시 동일하다면 최종적으로 새로운 auth-token을 발행해주면 된다.
@PostMapping("/user/refresh")
public ResponseEntity<Map<String, Object>> refreshToken(@RequestBody User user, HttpServletResponse res) {
Map<String, Object> resultMap = new HashMap<>();
// refresh token이 valid 한지 점검
jwtService.checkAndGetClaims(user.getRefreshToken());
// DB에 저장된 refresh 토큰의 정보가 전달된 토큰의 정보와 같은지 판단
if (user.getRefreshToken().equals(userService.getRefreshToken(user.getEmail()))) {
// 새로운 토큰의 발행 및 배포
String authToken = jwtService.createAuthToken(user.getEmail());
resultMap.put("jwt-auth-token",authToken);
Map<String, Object> info = jwtService.checkAndGetClaims(authToken);
resultMap.putAll(info);
}
return new ResponseEntity<Map<String, Object>>(resultMap, HttpStatus.ACCEPTED);
}
다음은 logout 요청시의 동작이다.
@GetMapping("/user/logout")
public ResponseEntity<Void> logout(@RequestParam String email) {
log.debug("logout: {}", email);
userService.logout(email);
return new ResponseEntity<>(HttpStatus.ACCEPTED);
}
Client Side 추가/수정
다음으로 client side에서 추가/수정해야 할 내용을 살펴보자.
setStorage
일단 setStorage에는 refreshToken 정보를 업데이트 할 수 있는 부분이 추가 되었다.
let setStorage = function (authToken, email, refreshToken) {
sessionStorage.setItem("jwt-auth-token", authToken);
sessionStorage.setItem("email", email);
sessionStorage.setItem("jwt-refresh-token", refreshToken);
}
login 처리
로그인 과정에서는 서버가 보내준 refresh token을 저장해주는 부분 즉 setStorage를 호출하는 부분이 변경되었다.
document.querySelector("#btn-login").addEventListener("click", function () {
setInfo("", "", "", "");
setStorage("", "", "");
(async () => {
let res = "";
try {
res = await axios.post("/api/user/login", {
"email": document.querySelector("#email").value,
"pass": document.querySelector("#pass").value
});
setInfo(res.data["jwt-auth-token"], res.data.exp, "로그인 성공", null);
// refresh token 저장!
setStorage(res.data["jwt-auth-token"], res.data.user, res.data["jwt-refresh-token"]);
} catch (error) {
setInfo("", "", "로그인 실패", error.response.data.message);
}
})();
})
logout 처리
기존 버전과 달리 서버에 토큰의 정보가 저장되어있기 때문에 이 정보를 지우기 위해 서버와의 통신이 필요하다.
document.querySelector("#btn-logout").addEventListener("click", function () {
setInfo(null, null, "", "");
(async () => {
try {
await axios.get("/api/user/logout", {
params: {
email: sessionStorage.getItem("email")
}
})
setStorage("", "", "");
setInfo("", "", "로그아웃 성공", "")
} catch (error) {
setInfo("", "", "로그아웃 실패", error.response.data);
}
})();
})
token refresh 추가
이제 refresh token을 요청하는 부분을 살펴보자.
async function refresh() {
try {
// 사용자를 확인하기 위한 email 정보와 refresh token을 각각 데이터와 header를 통해서 전송한다.
let res = await axios.post("/api/user/refresh", {
email: sessionStorage.getItem("email"),
refreshToken: sessionStorage.getItem("jwt-refresh-token")
});
console.log("조회 결과: ", res);
alert("토큰이 갱신되었습니다.");
setInfo(res.data["jwt-auth-token"], res.data.exp,
document.querySelector("#status").value += ">토큰 갱신");
sessionStorage.setItem("jwt-auth-token", res.data["jwt-auth-token"])
return true;
} catch (error) {
console.log(error)
if (error.response.status === 401) {
alert("refresh token까지 만료 되었습니다. 다시 로그인 해주세요.")
}
return false;
}
};
info 수정
이제 마지막으로 정보를 요청하고 auth token 만료 시 refresh token을 요청하고 다시 정보를 가져오는 부분이다.
document.querySelector("#btn-info").addEventListener("click", function () {
setInfo(null, null, "", "");
(async () => {
let res;
try {
res = await axios.get("/api/info", {
headers: {
"jwt-auth-token": sessionStorage.getItem("jwt-auth-token")
}
});
console.log("1차", res)
}
catch (error) {
document.querySelector("#status").value += ">정보 조회 실패"
if (error.response.status == 401 && await refresh()) {
try {
res = await axios.get("/api/info", {
headers: {
"jwt-auth-token": sessionStorage.getItem("jwt-auth-token")
}
});
console.log("2차", res)
} catch (error) {
console.log(error)
}
}
}
if (res) {
setInfo(null, null, document.querySelector("#status").value += ">정보 조회 성공",
res.data.info)
}
})();
});
동작 확인
이제 auth token의 refresh가 필요한 경우 refresh token을 요청해서 다시 요청하는 것을 확인할 수 있다.
추가로 refresh token까지 만료된 경우는 다시 로그인을 요청하고 있다.
'Spring security > 02.JWT활용' 카테고리의 다른 글
[JWT]JWT를 위한 Spring Security 설정 (0) | 2024.12.02 |
---|---|
[JWT]프로젝트 구성 및 토큰 확인 (0) | 2024.11.26 |
[JWT]JWT 개요 (0) | 2024.11.25 |
JWT를 이용한 인증 처리 4 (0) | 2022.04.28 |
JWT를 이용한 인증 처리 3 (0) | 2022.04.28 |
소중한 공감 감사합니다