JWT + 攔截器實現(xiàn)無狀態(tài)登錄系統(tǒng)
? 引言
在現(xiàn)代 Web 開發(fā)中,傳統(tǒng)的 Session 認證方式在分布式、微服務(wù)架構(gòu)下面臨挑戰(zhàn):
- Session 存儲依賴服務(wù)器內(nèi)存或 Redis
- 跨域、跨服務(wù)共享 Session 復(fù)雜
- 增加服務(wù)器負擔
JWT(JSON Web Token) 提供了一種無狀態(tài)的解決方案:用戶登錄后,服務(wù)器返回一個 Token,后續(xù)請求攜帶該 Token 即可完成身份驗證,無需服務(wù)器存儲會話信息。
本文將結(jié)合 Spring Boot 攔截器,手把手實現(xiàn)一個完整的 JWT 無狀態(tài)登錄系統(tǒng)。
?? 一、JWT 是什么?
JWT 是一個開放標準(RFC 7519),用于在各方之間安全地傳輸信息。
一個 JWT 通常由三部分組成,用 . 分隔:
xxxxx.yyyyy.zzzzz
- Header:令牌類型和簽名算法
- Payload:存放用戶信息(如用戶 ID、角色、過期時間等)
- Signature:簽名,用于驗證 Token 是否被篡改
? 優(yōu)點:自包含、可擴展、跨語言、無狀態(tài)。
?? 二、技術(shù)選型
- Spring Boot 2.7+
- JWT 庫:
io.jsonwebtoken:jjwt - 加密算法:HMAC SHA256
- 攔截器:
HandlerInterceptor
?? 三、項目結(jié)構(gòu)
src/ ├── main/ │ ├── java/ │ │ └── com/example/jwtlogin/ │ │ ├── config/ → 配置類 │ │ ├── interceptor/ → 攔截器 │ │ ├── util/ → 工具類 │ │ ├── controller/ → 控制器 │ │ └── entity/ → 實體類
?? 四、核心代碼實現(xiàn)
4.1 添加依賴(pom.xml)
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>?? 注意:JJWT 0.11+ 版本拆分了模塊,需引入三個依賴。
4.2 JWT 工具類
@Component
public class JwtUtil {
// 密鑰(應(yīng)放在配置文件中)
private static final String SECRET = "your-256-bit-secret-your-256-bit-secret";
// 過期時間:24小時
private static final long EXPIRATION = 1000 * 60 * 60 * 24;
/**
* 生成 Token
*/
public String generateToken(String username, Long userId, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("role", role);
return Jwts.builder()
.setSubject(username)
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
}
/**
* 解析 Token
*/
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
throw new RuntimeException("Token 已過期");
} catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
throw new RuntimeException("Token 無效");
}
}
/**
* 驗證 Token 是否有效
*/
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}4.3 登錄控制器
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
/**
* 用戶登錄
*/
@PostMapping("/login")
public ResponseEntity<Map<String, Object>> login(@RequestBody LoginRequest request) {
// 這里應(yīng)調(diào)用 UserService 驗證用戶名密碼
// 為簡化,假設(shè)用戶名密碼正確
if ("admin".equals(request.getUsername()) && "123456".equals(request.getPassword())) {
String token = jwtUtil.generateToken(request.getUsername(), 1L, "ADMIN");
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("username", request.getUsername());
result.put("role", "ADMIN");
return ResponseEntity.ok(result);
} else {
return ResponseEntity.status(401).body(Map.of("msg", "用戶名或密碼錯誤"));
}
}
/**
* 用戶登出(前端清空 Token 即可)
*/
@PostMapping("/logout")
public ResponseEntity<String> logout() {
return ResponseEntity.ok("登出成功");
}
}
// 登錄請求 DTO
class LoginRequest {
private String username;
private String password;
// getter & setter
}4.4 JWT 攔截器
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
private static final String AUTH_HEADER = "Authorization";
private static final String TOKEN_PREFIX = "Bearer ";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 1. 放行 OPTIONS 請求(預(yù)檢請求)
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
// 2. 放行登錄接口
String requestURI = request.getRequestURI();
if ("/auth/login".equals(requestURI) || "/auth/logout".equals(requestURI)) {
return true;
}
// 3. 獲取并驗證 Token
String token = request.getHeader(AUTH_HEADER);
if (token == null || !token.startsWith(TOKEN_PREFIX)) {
response.setStatus(401);
response.getWriter().write("{\"code\":401,\"msg\":\"缺少 Token\"}");
return false;
}
token = token.substring(TOKEN_PREFIX.length());
try {
Claims claims = jwtUtil.parseToken(token);
// 將用戶信息存入 request,供后續(xù) Controller 使用
request.setAttribute("currentUser", claims.getSubject());
request.setAttribute("userId", claims.get("userId"));
request.setAttribute("role", claims.get("role"));
return true;
} catch (RuntimeException e) {
response.setStatus(401);
response.getWriter().write("{\"code\":401,\"msg\":\"" + e.getMessage() + "\"}");
return false;
}
}
}4.5 注冊攔截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/**") // 保護所有 API 接口
.excludePathPatterns("/auth/**", "/public/**"); // 放行認證和公共接口
}
}4.6 測試受保護的接口
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/profile")
public Map<String, Object> getProfile(HttpServletRequest request) {
Map<String, Object> profile = new HashMap<>();
profile.put("username", request.getAttribute("currentUser"));
profile.put("userId", request.getAttribute("userId"));
profile.put("role", request.getAttribute("role"));
profile.put("email", "admin@example.com");
return profile;
}
@GetMapping("/admin/data")
public String adminData(HttpServletRequest request) {
String role = (String) request.getAttribute("role");
if ("ADMIN".equals(role)) {
return "敏感數(shù)據(jù):只有管理員可見";
} else {
return "權(quán)限不足";
}
}
}?? 五、測試流程
1. 登錄獲取 Token
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'響應(yīng):
{
"token": "eyJhbGciOiJIUzI1NiJ9.xxxxx.yyyyy",
"username": "admin",
"role": "ADMIN"
}2. 攜帶 Token 訪問受保護接口
curl http://localhost:8080/api/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxx.yyyyy"響應(yīng):
{
"username": "admin",
"userId": 1,
"role": "ADMIN",
"email": "admin@example.com"
}3. 不帶 Token 訪問 → 401 未授權(quán)
?? 六、生產(chǎn)環(huán)境優(yōu)化建議
| 優(yōu)化點 | 說明 |
|---|---|
| ?? 密鑰安全 | 將 SECRET 放在 application.yml 或環(huán)境變量中,不要硬編碼 |
| ?? Token 刷新 | 實現(xiàn) Refresh Token 機制,避免頻繁登錄 |
| ?? Token 黑名單 | 使用 Redis 記錄已注銷的 Token,防止被盜用 |
| ?? 自定義注解 | 使用 @RequireAuth(role="ADMIN") 簡化權(quán)限控制 |
| ?? 日志監(jiān)控 | 記錄 Token 解析失敗日志,便于排查問題 |
? 總結(jié)
| 步驟 | 說明 |
|---|---|
| 1. 用戶登錄 | 驗證賬號密碼,生成 JWT |
| 2. 客戶端存儲 | 將 Token 存入 localStorage 或 Cookie |
| 3. 攜帶請求 | 每次請求在 Authorization 頭中攜帶 Bearer Token |
| 4. 攔截器驗證 | 解析 Token,校驗簽名和過期時間 |
| 5. 放行或拒絕 | 驗證通過則放行,否則返回 401 |
?? 無狀態(tài)登錄的核心:服務(wù)器不保存會話狀態(tài),所有信息都封裝在 Token 中,由客戶端負責(zé)攜帶和管理。
?? 推薦
到此這篇關(guān)于JWT + 攔截器實現(xiàn)無狀態(tài)登錄系統(tǒng)的文章就介紹到這了,更多相關(guān)JWT 攔截器無狀態(tài)登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
IntelliJ IDEA 詳細圖解最常用的配置(適合剛剛用的新人)
這篇文章主要介紹了IntelliJ IDEA 詳細圖解最常用的配置,本篇教程非常適合剛剛用的新人,本文圖文并茂給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08
深度解析Java中volatile的內(nèi)存語義實現(xiàn)以及運用場景
這篇文章主要介紹了Java中volatile的內(nèi)存語義實現(xiàn)以及運用場景,通過JVM的機制來分析volatile關(guān)鍵字在線程編程中的作用,需要的朋友可以參考下2015-12-12
java對象轉(zhuǎn)化成String類型的四種方法小結(jié)
在java項目的實際開發(fā)和應(yīng)用中,常常需要用到將對象轉(zhuǎn)為String這一基本功能。本文就詳細的介紹幾種方法,感興趣的可以了解一下2021-08-08
SpringBoot結(jié)合Redis實現(xiàn)緩存管理功能
本篇文章主要介紹spring boot緩存管理機制及相關(guān)概念,以及如何結(jié)合Redis實現(xiàn)緩存管理,文中通過代碼示例給大家介紹的非常詳細,具有一定的參考價值,需要的朋友可以參考下2024-01-01
IDEA下lombok安裝及找不到get,set的問題的解決方法
這篇文章主要介紹了IDEA下lombok安裝及找不到get,set的問題的解決方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04

