Spring security/02.JWT활용

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까지 만료된 경우는 다시 로그인을 요청하고 있다.

 

Contents

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

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