Spring security/02.JWT활용

JWT를 이용한 인증 처리 6. Refresh Token 구현

  • -

이번 포스트에서는 기존의 예제에 refresh token 개념을 추가해보자.

 

먼저 Server Side에서 추가 또는 수정해야 할 내용들이다.

refresh token을 발행하기 위한 메서드를 추가한다.

/** * Refresh 토큰을 생성한다 * 이때는 인증을 위한 정보는 유지하지 않고 유효기간을 auth-token의 5배로 잡았다. * @return */ public String createRefreshToken() { return create(null, "refreshToken", expireMin * 5); }

 

로그인 시 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); }

 

컨트롤러에는 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에서 추가/수정해야 할 내용을 살펴보자.

일단 setStorage에는 refreshToken 정보를 업데이트 할 수 있는 부분이 추가 되었다.

let setStorage = function (authToken, email, refreshToken) { sessionStorage.setItem("jwt-auth-token", authToken); sessionStorage.setItem("email", email); sessionStorage.setItem("jwt-refresh-token", refreshToken); }

 

로그인 과정에서는 서버가 보내준 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); } })(); })

 

기존 버전과 달리 서버에 토큰의 정보가 저장되어있기 때문에 이 정보를 지우기 위해 서버와의 통신이 필요하다.

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); } })(); })

 

이제 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; } };

 

이제 마지막으로 정보를 요청하고 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까지 만료된 경우는 다시 로그인을 요청하고 있다.

 

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.