Giken Dev
article thumbnail
반응형

들어가며

Spring 프로젝트에서 로그인(인증)을 JWT 로 구현하는 것을 성공했다.

나는 데이터베이스를 전혀 사용하지 않는 Spring Security 스프링 기본 JWT 방식이 아닌,  DB에 인증 테이블을 만들고 User의 Id 와 Refresh Token을 같이 저장했는데 그 이유는 아래에서 얘기해보자.

 

이제 로그아웃을 만들어보려는데 고민이 됐다.

(처음엔 별 생각 없이 Refresh Token 인증 테이블에서 삭제하는 삽질을..)

 

 

 

기본 JWT 방식으로는 안전한 로그아웃을 구현이 어려울걸?

기본 JWT 방식에서는 서버가 토큰 상태를 제어할 수 없어 안전한 로그아웃 구현이 어렵다.

토큰 발급 후 서버는 토큰 관리 권한을 잃어버리는데,

이는 서버 어디에도 사용자 정보만료 시간이 포함된 JWT 가 저장되지 않았기 때문이다.

기본 JWT 방식에서 로그아웃은 클라이언트 쪽에서 JWT를 삭제하는 방식으로 이뤄지지만, 이 방법은 토큰 탈취 시 서버가 토큰을 무효화할 수 없는 단점이 있다. 특히 Refresh Token까지 탈취되면 해커는 토큰 만료 시간을 계속 갱신할 수 있다.

따라서, 데이터베이스를 전혀 사용하지 않는 기본 JWT 방식으로는 안전한 로그아웃을 구현할 수 없다.

 

 

 

기본 JWT 방식으로는 Refresh Token을 통한 Access Token 재발급을 구현할 수 없다.

Refresh Token을 사용한 Access Token 재발급도 기본 JWT 방식에서는 구현하기 힘들다.

기본 JWT 방식에서는 서버가 Refresh Token을 저장하지 않는다. Access Token 이 만료되면 사용자는 로그아웃 상태가 되고, 다시 로그인을 해야만 하기 때문에 Access Token의 만료기간이 짧은 경우에는 안 좋은 사용자 경험을 줄 수 있다. 반대로 길게 하면 보안이 약해져 해커에게 머리를 세게 맞을 수 있다.

 

결국, 기본 JWT 방식은 서버 쪽 토큰 관리가 제한적이라 안전한 사용자 인증 및 세션 관리를 위해 다른 방법을 고려하는 게 좋다.

 

 

 

로그아웃을 어떻게 구현할건데?

서치하면서 2가지 방법을 알아냈다. (내가 찾지 못한 방법이 있을 있음)

 

 

1. 클라이언트에서 저장중인 JWT를 삭제하기

JWT 는 클라이언트의 storage에 저장되므로, 프론트엔드에서 이 토큰을 지우면 로그아웃 처리가 가능하다.

그러나, 유저가 토큰을 미리 복사해두었다면 서버에 계속 요청을 보낼 수 있다는 문제가 있다.

 

2. 블랙리스트로 인증 관리

로그아웃 할 때 Refresh Token을 Api 요청에 담아서 블랙리스트 테이블에 저장하도록 구현한다.

JWT 방식에 데이터베이스를 조금 섞어 사용한다. 이는 보안사용자 경험을 동시에 향상시키는 방법이다. 

로그아웃 하면 프론트엔드 코드에서 직접 Access Token을 삭제해주면 된다. 만약 Access Token이 탈취되었더라도 30분 정도로 만료 기간을 짧게 설정해두면 비교적 안전하다.

Access Token 만료되어 Refresh Token으로 새로운 Access Token 요청하는 경우에도, 서버는 블랙리스트에 해당 Refresh Token 저장되어 있는 것을 확인하고, 해당 Refresh Token 로그아웃된 유저라는 것을 있기 때문에 Access Token 재발급을 거절할 있다.

토큰 재발급에 성공하면 자주 로그인이 풀리는 문제가 해결된다. 사용자 경험을 끌어올리는 것이다. 

 

2번 방법은 Access Token이 탈취당한 경우, 만료될 때까지 기다려야 한다는 점은 1번과 동일하지만

Refresh Token을 통한 Access Token 의 재발급이 가능하기에 Access Token의 만료 기간을 짧게 설정할 수 있어 보안 측면에서 확실히 개선된다.

 

 

 

 

 

AuthService.java

 

로그인을 하면, UserId 를 SecretKey 를 넣은 알고리즘을 통해서 Refresh Token 과 Access Token이 발급된다.

@Transactional
	public AuthResponse login(AuthRequest authRequest) {
		Long userId = userService.getUserByEmail(authRequest.email()).getId();
		AuthResponse authResponse = saveAuth(userId);
		String plainPassword = authRequest.password();
		String hashedPassword = userService.getUserById(userId).getPassword();

		if (encryptHelper.isMatch(plainPassword, hashedPassword)) {
			return authResponse;
		}
		throw new NotFoundException(FAILED_LOGIN_BY_ANYTHING);
	}

 

 

 

 

사용자가 로그아웃을 하면

User 의 RefreshToken 을 블랙리스트에 추가해준다.

	@Transactional
	public void logout(Long userId) {
		authRepository.findByUserId(userId).ifPresentOrElse(
			auth ->
				blacklistRepository.save(BlacklistToken.of(auth.getRefreshToken())),
			() -> {
				throw new NotFoundException(NOT_FOUND_USER_ID);
			}
		);
	}

 

 

 

사용자의 AccessToken 이 만료됐을 때는

재로그인 할 필요 없이 토큰을 재발급해주는 부분이다.

토큰 재발급 Api 요청에 포함된 RefreshToken 이 블랙리스트에 존재하면 재발급을 거부하고 예외를 던진다.

public String createAccessTokenByRefreshToken(String refreshToken) {
		boolean isBlacklisted = blacklistRepository.existsByRefreshToken(refreshToken);
		if (isBlacklisted) {
			throw new AuthException(BLACKLISTED_TOKEN);
		}

		Auth auth = getAuthByRefreshToken(refreshToken);
		Long userId = userService.getUserById(auth.getUserId()).getId();
		return jwtProvider.createAccessToken(userId);
	}

 

 

 

마치며

프로젝트 당시에 팀원의 코드리뷰를 통해 고민하게 된 부분이 있다.

블랙리스트의 토큰들에 대한 삭제를 고려해주지 않았다.

(아마 스케줄러로 특정 주기로 한 번씩 지워주면 되겠지?)

 

 

 

 

참고한 글

https://engineerinsight.tistory.com/232

https://velog.io/@joonghyun/SpringBoot-Jwt%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83

https://hudi.blog/session-based-auth-vs-token-based-auth/ 

https://hudi.blog/refresh-token/

반응형
profile

Giken Dev

@기켄

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!