目前以帳號密碼登入的功能都做得差不多了,所以很自然的想來試試看登出功能~~
不過因為我們選用 JWT ,特性就是伺服器並不會記憶該用戶 Session,也沒有把 Token 存入資料庫中,讓用戶即時有感的登出可能就要留給屆時前端實作。
不過,在現行架構下我們還有一件事情可以做,就是在用戶登出時資料庫中刪除 RefreshToken!
如此一來,即便 Token 不幸外洩,只要原本的 Access Token 過期,就必須強制重新登入才能取得權限了。
定義登出時需傳入的參數,目前僅有 refreshToken(承襲前幾天的做法,從簡的將 token 都先放在 body 中進行傳送):
public record LogoutRequest (
String refreshToken
){
}
新增一個端點在AuthController,端點為 /users/logout
。因為目前我們簡單的將 Refresh Token ,並且 lougout 未來或許會包含更多動作,因此比起 Delete ,這邊我會先選用 Post 的 Http method:
@PostMapping("/logout")
public ResponseEntity<?> logoutUser(@Valid @RequestBody LogoutRequest logoutRequest,@AuthenticationPrincipal UserEntity currentUser) {
refreshTokenService.deleteByToken(logoutRequest.refreshToken(), currentUser);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
為了在後續檢察使用者身分是否可執行此操作,調用方法中傳入當前驗證完成的使用者資訊。
加入 deleteByToken 的方法。對於 logout 這件事,目前只有刪除 RefreshToken,未來或許有其他行為不屬於這個服務,以 deleteByToken 命名最為合適:
@Override
@Transactional
public void deleteByToken(String refreshToken, UserEntity currentUser) {
RefreshTokenEntity refreshTokenObj = refreshTokenRepository.findByToken(refreshToken)
.orElseThrow(() -> new TokenRefreshException(ErrorCode.REFRESH_TOKEN_NOT_FOUND, "Refresh token not found"));
boolean isAdmin = currentUser.getAuthorities().stream()
.anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_ADMIN"));
Long tokenOwnerId = refreshTokenObj.getUser().getId();
Long currentUserId = currentUser.getId();
if (!tokenOwnerId.equals(currentUserId) && !isAdmin){
logger.warn(" User '{}' attempting to log out session of another user '{}'",
currentUserId,tokenOwnerId) ;
throw new AccessDeniedException("You do not have permission to perform this action");
}
// 如為本人可刪除
refreshTokenRepository.delete(refreshTokenObj);
}
除了檢查 Token存在與否,這邊也檢查使用者身分是否可執行本次操作,預計可執行登出的應該只有本人與管理員。
原本在建立登入與註冊請求將 /users
下的端點都設定為permitAll
,既然現在有新增的功能,就將權限控管更清楚地寫下來:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.exceptionHandling(exception -> exception.authenticationEntryPoint(authEntryPoint))
.authorizeHttpRequests(auth -> auth
// 登入跟註冊還沒拿到Token,開放所有人打
.requestMatchers("/users/login", "/users/register").permitAll()
// 其他的請求要經過驗證
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
因為前面幾篇已經做過很多次,這邊就簡單列出步驟跟測試結果~
Bear ${剛剛取得的 Access Token}
目前沒有需要回傳的內容,成功回傳 204 ,資料庫也正確刪除這筆資源。
今天,完成了身份驗證流程的登出功能。透過在後端撤銷 Refresh Token,算是實作了一個兼顧無狀態架構與安全性的登出機制(?)
因為跟原本預期的進度小有落差,打算花點時間調整未來九天的內容,或許這三十天就專注在學習驗證與授權服務~也很想試著實作看看第三方登入的功能,因此今天內容相對簡略,敬請見諒:)!