JWT登出功能是一個關鍵的安全性考慮,因為JWT是無狀態的,一旦簽發,就無法撤銷或註銷。然而,有幾種方法可以實現JWT登出功能,每種方法都有其優點和限制。
工作原理:
工作原理:
如果你本地已搭建Redis服務可以跳過這一步驟
service:
redis:
container_name: 'myredis'
image: redis:7.0-alpine
restart: always
ports:
- "6380:6379" # 將容器內的6379映射到本機的6380埠
environment:
host: localhost #Redis伺服器位址
port: 6379 #Redis伺服器埠號
database: 0 #Redis資料庫索引
networks:
- backend
# 省略其他服務
networks:
backend:
首先,確保您的Spring Boot項目中包含了Redis的依賴項。可以在項目的pom.xml
文件中添加以下依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在application.yml
中配置Redis連接信息。這包括Redis伺服器的主機和和埠號:
application.yml
:application-dev.yml
,這樣的好處是當換成生產環境只要改成spring.profiles.active: prod
,並準備好application-prod.yml
生產環境配置即可,詳細教學可以參考此系列的第6天文章。spring:
profiles:
active: dev # 導入application-dev.yml開發環境配置
---
spring:
data:
redis:
host: ${conf.redis.host} #Redis伺服器位址 (預設 localhost)
port: ${conf.redis.port} #Redis伺服器埠號 (預設 6379)
database: ${conf.redis.database} #Redis資料庫索引 (預設 0)
timeout: 4000 # 讀取超時
application-dev.yml
:conf:
token:
secret: "your secret key"
expiration: 900000 #Token有效期限 (設定15分鐘過期=15*60*1000 單位:毫秒)
redis:
host: localhost #Redis伺服器位址
port: 6380 #Redis伺服器埠號
database: 0 #Redis資料庫索引
為了實現黑名單登出功能,我們需要在Redis中維護一個令牌黑名單。可以使用Redis的String數據結構來實現,並且該值可以根據本身的存活時間自動被Redis清除。創建一個名為JwtBlackListService
,用於處理JWT黑名單等相關業務處理服務。
@Service
@RequiredArgsConstructor
public class JwtBlackListService {
@Value("${conf.token.expiration}")
private long jwtExpiration;
private final RedisTemplate<String, String> redisTemplate;
private static final String BLACKLIST_PREFIX = "jwt:blacklist:"; // 黑名單前綴
/** 將jwt加入黑名單 */
public void addJwtToBlackList(String jwt) {
String key = BLACKLIST_PREFIX + jwt;
Duration expirationDuration = Duration.ofMillis(jwtExpiration); // 設置存活時間
// 將jwt加入黑名單並設置存活時間,當過了存活時間就會自動被Redis自動清除
redisTemplate.opsForValue().set(key, "true", expirationDuration);
}
/** 檢查jwt是否在黑名單中 */
public boolean isJwtInBlackList(String jwt) {
String key = BLACKLIST_PREFIX + jwt;
return redisTemplate.opsForValue().get(key) != null;
}
}
AuthenticationController.class
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService service;
//** 登出接口 */
@PostMapping("/logout")
public ResponseEntity<StatusResponse> logout(@RequestHeader("Authorization") String token) {
return ResponseEntity.ok(service.logout(token));
}
// 其他接口程式碼
}
AuthenticationService.class
@Service
@Slf4j
@AllArgsConstructor
public class AuthenticationService {
private final JwtService jwtService;
private final JwtBlackListService jwtBlackListService;
/** 使用者登出處理 */
public StatusResponse logout(String token) {
// 調用JWT黑名單服務將該token加入到黑名單中
jwtBlackListService.addJwtToBlackList(token.substring(7));
// 清除Spring Security上下文
SecurityContextHolder.clearContext();
return StatusResponse.SUCCESS();
}
// 其他程式碼
}
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
// 以下條件為沒有攜帶Token的請求
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
try {
jwt = authHeader.substring(7); //取"Bearer "後面的Token
userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
//檢查token是否有效
if (jwtService.isTokenValid(jwt, userDetails)) { // <---關注點
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
} else {
sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "Token失效,請重新申請");
return;
}}
} catch (Exception e) {
// 異常處理的程式碼
}
filterChain.doFilter(request, response);
}
}
@Service
@Slf4j
@RequiredArgsConstructor
public class JwtService {
// Token有效期限
@Value("${conf.token.expiration:900000}")
private Long EXPIRATION_TIME; //單位ms
@Value("${conf.token.secret}")
private String SECRET_KEY;
private final JwtBlackListService jwtBlackListService;
/**
* 驗證Token有效性,比對JWT和UserDetails的Username(Email)是否相同
* @return 有效為True,反之False
*/
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()))
&& !isTokenExpired(token)
&& !isTokenInBlackList(token); // 多了這個是否在黑名單的判斷
}
/**
* check if the JWT is in the blacklist
* @return in the blacklist return true, else return false
*/
private boolean isTokenInBlackList(String jwt) {
return jwtBlackListService.isJwtInBlackList(jwt);
}
// 其他程式碼...
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
/**
# 用戶認證配置 #
1. authorizeHttpRequests()方法:對所有訪問HTTP端點的HttpServletRequest進行限制
2. anyRequest().authenticated()語句指定了對於所有請求都需要執行認證,也就是說沒有通過認證的用戶就無法訪問任何端點。
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(
"/error/**",
"/api/smarthome/auth/register", //用戶註冊
"/api/smarthome/auth/login", //用戶登入
"/api/smarthome/auth/password/forgot", //忘記密碼
"/api/smarthome/auth/verification/check", //檢查驗證碼
"/v3/api-docs/**",
"/swagger-ui/**",
"/api/test/**"
)
.permitAll()
.anyRequest()
.authenticated()
)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
TTL "jwt:blacklist:{你要查詢的JWT}"