Spring Boot 登錄實現:JWT 與 Session 全面對比與實戰講解
2025.5.21-23:11今天在學習黑馬點評時突然發現用的是與蒼穹外賣jwt不一樣的登錄方式-Session,于是就想記錄一下這兩種方式有什么不同
在實際開發中,登錄認證是后端最基礎也是最重要的模塊之一。常見的實現方式主要有兩種:Session 登錄 和 JWT(JSON Web Token)登錄。
下面將從原理、使用場景、優缺點、代碼結構等角度,來對比這兩種方案,然后給出 Spring Boot 中的實際應用建議,幫助在實際項目中做出合理選擇。
一、登錄認證基本流程
無論是 JWT 還是 Session,登錄的本質流程都是:
- 用戶發送用戶名和密碼;
- 后端驗證成功后,生成認證信息;
- 后端將認證信息返回給客戶端;
- 客戶端攜帶認證信息訪問受保護接口;
- 后端驗證該認證信息是否合法。
二、Session 登錄機制
1. 原理說明
- 用戶第一次登錄成功后,服務器創建一個
Session
,并在服務器內存或 Redis 中保存用戶信息; - 同時將 Session ID 寫入到瀏覽器的 Cookie 中;
- 用戶每次請求都會自動攜帶該 Cookie,服務器根據 Session ID 獲取用戶信息。
2. 代碼實現示例
登錄接口:
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO, HttpSession session) {User user = userService.login(loginDTO);session.setAttribute("user", user);return Result.success();
}
攔截器判斷是否登錄:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {Object user = request.getSession().getAttribute("user");if (user == null) {response.setStatus(401);return false;}return true;
}
三、JWT 登錄機制
1. 原理說明
- 用戶登錄成功后,服務器簽發一個加密的 JWT Token;
- 客戶端將 Token 保存在 LocalStorage 或 Cookie;
- 每次請求都將 Token 放在 Authorization 請求頭中;
- 服務器解析并驗證 Token,從中讀取用戶信息。
2. JWT Token 的組成
JWT 一般由三部分組成:
Header.Payload.Signature
例如:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEyMywidXNlcm5hbWUiOiJsaTIzIn0.sD_Kpdi2M...
3. JWT 登錄代碼示例
登錄生成 Token:
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {User user = userService.login(loginDTO);String token = JwtUtil.generateToken(user); // 自定義工具類生成 tokenreturn Result.success(token);
}
前端請求攜帶 Token:
GET /user/info HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
攔截器驗證 Token:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String token = request.getHeader("Authorization");Claims claims = JwtUtil.parseToken(token);if (claims == null) {response.setStatus(401);return false;}return true;
}
四、Session vs JWT 對比總結
特性 | Session | JWT |
---|---|---|
存儲位置 | 服務端(內存/Redis) | 客戶端(LocalStorage/Cookie) |
狀態性 | 有狀態 | 無狀態 |
跨域支持 | 不友好(需處理 Cookie) | 友好 |
性能 | 頻繁讀寫服務端存儲 | 只需簽名驗證 |
安全性 | 較高(信息在服務端) | 中等(需防止 Token 泄露) |
適用場景 | 傳統 Web 應用 | 前后端分離、微服務、移動端 |
五、實戰項目推薦
在實際項目中,建議根據以下場景選擇:
- 中小型 Web 應用:推薦使用 Session,實現簡單,安全性高;
- 前后端分離項目:推薦使用 JWT,無狀態,跨域友好;
- 大型系統:考慮 JWT + Session 混合使用,結合兩者優勢。
六、JWT 常見安全建議
- 設置過期時間(exp),避免 token 永久有效;
- 使用 HTTPS,防止 Token 被中間人截獲;
- 配合 Refresh Token 實現續簽;
- 用戶登出時將 Token 加入 Redis 黑名單;
- Token 不應包含敏感信息(如密碼、身份證號等)。
七、總結
Session 和 JWT 各有優缺點,選擇時需根據項目實際情況權衡:
- Session 適合傳統 Web 應用,安全性高,實現簡單;
- JWT 適合前后端分離、微服務架構,無狀態,擴展性強。
在實際開發中,還可以結合兩者的優勢,實現更靈活、安全的認證方案。
八、登錄超時控制
1. Session 登錄的過期控制
# application.yml 中配置 Session 失效時間(單位分鐘)
server:servlet:session:timeout: 30
Session 登錄通常依賴瀏覽器 Cookie,每次請求自動續期,適合長期在線場景。
2. JWT 的過期控制
JWT 自帶 exp 字段,在生成 Token 時設置過期時間:
// JwtUtil 生成 Token 示例
public static String generateToken(User user) {Date now = new Date();Date expireDate = new Date(now.getTime() + EXPIRE_TIME); // 設置過期時間return Jwts.builder().setHeaderParam("typ", "JWT").setSubject(user.getId().toString()).setIssuedAt(now).setExpiration(expireDate).claim("username", user.getUsername()).signWith(SignatureAlgorithm.HS512, SECRET) // 簽名加密.compact();
}
九、JWT 的刷新機制(Refresh Token)
為了避免用戶頻繁登錄,可以實現 Refresh Token 機制:
- 登錄時生成兩個 Token:AccessToken(短過期)和 RefreshToken(長過期);
- AccessToken 過期后,使用 RefreshToken 重新獲取 AccessToken;
- RefreshToken 也過期時,才需要用戶重新登錄。
// 登錄接口返回兩個 Token
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO) {User user = userService.login(loginDTO);String accessToken = JwtUtil.generateAccessToken(user);String refreshToken = JwtUtil.generateRefreshToken(user);Map<String, String> tokens = new HashMap<>();tokens.put("accessToken", accessToken);tokens.put("refreshToken", refreshToken);return Result.success(tokens);
}// 刷新 Token 接口
@PostMapping("/refreshToken")
public Result refreshToken(@RequestBody RefreshTokenDTO dto) {// 驗證 RefreshToken 有效性Claims claims = JwtUtil.parseRefreshToken(dto.getRefreshToken());if (claims == null) {return Result.error("登錄已過期,請重新登錄");}// 重新生成 AccessTokenUser user = userService.getById(Long.valueOf(claims.getSubject()));String accessToken = JwtUtil.generateAccessToken(user);return Result.success(accessToken);
}
十、登出與 Redis 黑名單機制
JWT 本身無法主動失效,但可以通過 Redis 黑名單實現:
// 登出接口
@PostMapping("/logout")
public Result logout(HttpServletRequest request) {String token = request.getHeader("Authorization");if (token != null && token.startsWith("Bearer ")) {token = token.substring(7);// 解析 Token 獲取過期時間Claims claims = JwtUtil.parseToken(token);if (claims != null) {Date expireDate = claims.getExpiration();long expireSeconds = (expireDate.getTime() - System.currentTimeMillis()) / 1000;// 將 Token 加入 Redis 黑名單,直到過期redisTemplate.opsForValue().set("jwt:blacklist:" + token, "invalid", expireSeconds, TimeUnit.SECONDS);}}return Result.success();
}// 攔截器中檢查黑名單
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String token = request.getHeader("Authorization");if (token != null && token.startsWith("Bearer ")) {token = token.substring(7);// 檢查黑名單if (redisTemplate.hasKey("jwt:blacklist:" + token)) {response.setStatus(401);return false;}// 驗證 TokenClaims claims = JwtUtil.parseToken(token);if (claims == null) {response.setStatus(401);return false;}// 將用戶信息存入請求request.setAttribute("userId", claims.getSubject());}return true;
}
十一、實際開發中推薦的 JWT 完整實現流程圖
用戶登錄請求
│
▼
后端驗證用戶名密碼
│
▼
驗證成功
│
├─┬─ 生成 AccessToken(含用戶 ID、角色等)
│ │
│ ├─ 生成 RefreshToken(與用戶 ID 綁定)
│ │
│ └─ 將 RefreshToken 存入 Redis(設置過期時間)
│
▼
返回 AccessToken 和 RefreshToken 給前端
│
▼
前端保存 Token(如 LocalStorage)
│
▼
前端每次請求攜帶 AccessToken(放在 Header)
│
▼
后端攔截器驗證 AccessToken
│
├─ 驗證失敗 ──┐
│ │
│ ▼
│ 返回 401 未授權
│ │
│ ▼
│ 前端使用 RefreshToken 請求新 AccessToken
│ │
│ ▼
│ 后端驗證 RefreshToken
│ │
│ ┌───────┴───────┐
│ │ │
│ 有效 無效
│ │ │
│ ▼ ▼
│ 生成新 Token 用戶重新登錄
│ │
│ ▼
返回新 AccessToken
十二、附錄:JWT 工具類參考實現
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import java.util.Date;
import java.util.HashMap;
import java.util.Map;@Component
public class JwtUtil {@Value("${jwt.secret}")private String secret;@Value("${jwt.accessTokenExpireTime}")private long accessTokenExpireTime; // 分鐘@Value("${jwt.refreshTokenExpireTime}")private long refreshTokenExpireTime; // 天/*** 生成 Access Token*/public String generateAccessToken(User user) {Map<String, Object> claims = new HashMap<>();claims.put("username", user.getUsername());claims.put("roles", user.getRoles());return generateToken(user.getId().toString(), claims, accessTokenExpireTime * 60 * 1000);}/*** 生成 Refresh Token*/public String generateRefreshToken(User user) {return generateToken(user.getId().toString(), null, refreshTokenExpireTime * 24 * 60 * 60 * 1000);}/*** 生成 Token*/private String generateToken(String subject, Map<String, Object> claims, long expireTime) {Date now = new Date();Date expireDate = new Date(now.getTime() + expireTime);return Jwts.builder().setHeaderParam("typ", "JWT").setClaims(claims).setSubject(subject).setIssuedAt(now).setExpiration(expireDate).signWith(SignatureAlgorithm.HS512, secret).compact();}/*** 解析 Token*/public Claims parseToken(String token) {try {return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();} catch (Exception e) {return null;}}/*** 驗證 Token 是否過期*/public boolean isTokenExpired(String token) {Claims claims = parseToken(token);if (claims == null) {return true;}Date expiration = claims.getExpiration();return expiration.before(new Date());}
}
十三、JWT 與 Session 混合使用的實踐方案
在一些大型系統中,可能存在不同端口或子系統使用不同的認證方式(如后臺管理系統用 Session,移動端用 JWT),這時可以采用 混合認證策略:
1. 使用建議
模塊 | 建議認證方式 |
---|---|
管理后臺(單體、Spring MVC) | 使用 Session(Cookie 自動管理,方便權限控制) |
移動端 / 小程序 / H5 | 使用 JWT(無狀態認證,跨域安全,適合 API) |
多端統一用戶體系 | JWT + Session 混合,并通過 Redis 存儲登錄狀態 |
2. 核心實現思路
- 登錄成功時,服務端生成 JWT,同時創建一個短 Session,用于后臺系統交互;
- Redis 中統一存儲用戶 Token 狀態,便于管理和失效控制;
- 攔截器判斷請求來源(如是否含
Authorization
),自動適配認證方式。
十四、常見問題 FAQ
Q1:JWT 一旦泄露是否會被無限使用?
是的,如果沒有設置過期時間或失效機制(如黑名單),Token 會一直有效。所以必須:
- 設置過期時間;
- 使用 HTTPS;
- 實現登出邏輯(Redis 黑名單);
Q2:JWT 是不是比 Session 更安全?
不完全正確。JWT 只是在無狀態架構下更合適,但它暴露信息在客戶端,若加密不嚴密,反而更容易被篡改或偽造。而 Session 只存在服務端,反而更隱蔽和安全。
Q3:JWT 必須存放在 LocalStorage 嗎?
不一定。也可以存放在:
LocalStorage
(常見方式,刷新頁面不丟失);SessionStorage
(更安全,但刷新頁面會清空);HttpOnly Cookie
(防 XSS,但不支持 JS 訪問,需配合后端設置跨域 Cookie);
十六、推薦學習與實踐路徑
1. 學會使用 Spring MVC + Session 登錄機制
在 Spring MVC 項目中,Session 登錄是一種傳統且有效的認證方式。
登錄接口實現:
通過 HttpSession
對象將用戶信息存入 Session:
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password, HttpSession session) {User user = userService.login(username, password);if (user != null) {session.setAttribute("user", user); // 存入 Sessionreturn "redirect:/home"; // 登錄成功跳轉}return "login"; // 登錄失敗返回登錄頁
}
攔截器驗證登錄狀態:
編寫攔截器檢查 Session 中是否存在用戶信息:
@Component
public class LoginInterceptor extends HandlerInterceptorAdapter {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession();User user = (User) session.getAttribute("user");if (user == null) { // 未登錄則重定向到登錄頁response.sendRedirect("/login");return false;}return true; // 已登錄,允許訪問}
}
注冊攔截器:
在 Spring MVC 配置類中指定攔截路徑(如 /api/**
)和排除路徑(如 /login
):
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/api/**") // 攔截所有 API 接口.excludePathPatterns("/login", "/static/**"); // 排除登錄頁和靜態資源}
}
2. 掌握前后端分離項目中 JWT 的基本使用方式
JWT 工具類實現:
生成和解析 Token,包含過期時間和簽名加密:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;public class JwtUtil {private static final String SECRET = "your_secret_key_keep_it_secure"; // 密鑰(需保密)private static final long EXPIRATION_TIME = 3600000; // 1小時(毫秒)// 生成 Tokenpublic static String generateToken(String username) {Claims claims = Jwts.claims().setSubject(username); // 載荷存儲用戶標識Date now = new Date();Date expiration = new Date(now.getTime() + EXPIRATION_TIME); // 設置過期時間return Jwts.builder().setClaims(claims).setIssuedAt(now).setExpiration(expiration).signWith(SignatureAlgorithm.HS256, SECRET) // HS256 簽名算法.compact();}// 解析 Tokenpublic static Claims parseToken(String token) {try {return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); // 成功解析返回載荷} catch (Exception e) {return null; // 解析失敗返回 null}}
}
登錄接口返回 Token:
驗證用戶信息后生成 Token 并返回給前端:
@PostMapping("/login")
public ResponseEntity<Map<String, String>> login(@RequestBody UserLoginRequest request) {User user = userService.authenticate(request.getUsername(), request.getPassword());if (user != null) {String token = JwtUtil.generateToken(user.getUsername());Map<String, String> result = new HashMap<>();result.put("token", token);return ResponseEntity.ok(result); // 返回 Token}return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); // 認證失敗
}
后端驗證 Token:
通過攔截器從請求頭中獲取 Token 并解析驗證:
public class JwtInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String token = request.getHeader("Authorization");if (token != null && token.startsWith("Bearer ")) {token = token.substring(7); // 去除 "Bearer " 前綴Claims claims = JwtUtil.parseToken(token);if (claims != null) {// 解析成功,將用戶信息存入請求(如用戶 ID、角色)request.setAttribute("userId", claims.getSubject());return true;}}response.setStatus(HttpStatus.UNAUTHORIZED); // 未認證或 Token 無效return false;}
}
3. 實現 Token 過期 + 刷新機制
雙 Token 設計:
- AccessToken:短有效期(如 1 小時),用于接口認證;
- RefreshToken:長有效期(如 7 天),用于刷新 AccessToken。
登錄時返回雙 Token:
@PostMapping("/login")
public ResponseEntity<Map<String, String>> login(@RequestBody UserLoginRequest request) {User user = userService.authenticate(request.getUsername(), request.getPassword());if (user != null) {String accessToken = JwtUtil.generateToken(user.getUsername(), "access"); // 短過期String refreshToken = JwtUtil.generateToken(user.getUsername(), "refresh"); // 長過期Map<String, String> tokens = new HashMap<>();tokens.put("accessToken", accessToken);tokens.put("refreshToken", refreshToken);return ResponseEntity.ok(tokens);}return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null);
}
刷新 Token 接口:
使用 RefreshToken 生成新的 AccessToken:
@PostMapping("/refresh-token")
public ResponseEntity<Map<String, String>> refreshToken(@RequestHeader("Refresh-Token") String refreshToken) {Claims claims = JwtUtil.parseToken(refreshToken);if (claims != null && "refresh".equals(claims.get("type"))) { // 驗證 Token 類型String username = claims.getSubject();String newAccessToken = JwtUtil.generateToken(username, "access"); // 生成新 AccessTokenMap<String, String> result = new HashMap<>();result.put("accessToken", newAccessToken);return ResponseEntity.ok(result);}return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null);
}
4. 實現 Redis 黑名單、登錄限制、踢出機制
Redis 黑名單:
用戶登出時將 Token 加入 Redis 黑名單,設置與 Token 剩余有效期一致的過期時間:
@Autowired
private RedisTemplate<String, Object> redisTemplate;// 登出邏輯
public void logout(String token) {Claims claims = JwtUtil.parseToken(token);if (claims != null) {long expireTime = claims.getExpiration().getTime() - System.currentTimeMillis();if (expireTime > 0) {redisTemplate.opsForValue().set("blacklist:" + token, "invalid", expireTime, TimeUnit.MILLISECONDS);}}
}// 攔截器中檢查黑名單
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String token = request.getHeader("Authorization");if (redisTemplate.hasKey("blacklist:" + token)) { // 存在于黑名單則拒絕訪問response.setStatus(HttpStatus.UNAUTHORIZED);return false;}// 其他驗證邏輯...return true;
}
登錄頻率限制:
使用 Redis 記錄用戶登錄次數,限制每分鐘最多 5 次嘗試:
public boolean checkLoginFrequency(String username) {String key = "login:attempts:" + username;Integer count = (Integer) redisTemplate.opsForValue().get(key);if (count != null && count >= 5) { // 超過限制return false; // 禁止登錄}// 次數加 1,設置過期時間 1 分鐘redisTemplate.opsForValue().increment(key, 1);redisTemplate.expire(key, 1, TimeUnit.MINUTES);return true;
}
管理員踢出用戶:
將用戶所有有效 Token 加入黑名單(需結合用戶 ID 批量操作):
public void kickUser(String userId) {// 查詢用戶所有有效 Token(需業務系統存儲 Token 與用戶的映射)List<String> tokens = tokenRepository.findByUserId(userId);tokens.forEach(token -> logout(token)); // 批量加入黑名單
}
5. 掌握 Spring Security 對 JWT 的整合
自定義 JWT 認證過濾器:
繼承 OncePerRequestFilter
,解析 Token 并設置安全上下文:
public class JwtAuthenticationFilter extends OncePerRequestFilter {private final JwtUtil jwtUtil;public JwtAuthenticationFilter(JwtUtil jwtUtil) {this.jwtUtil = jwtUtil;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = request.getHeader("Authorization");if (token != null && token.startsWith("Bearer ")) {token = token.substring(7);Claims claims = jwtUtil.parseToken(token);if (claims != null) {String username = claims.getSubject();// 構建認證對象(可添加角色權限)Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));SecurityContextHolder.getContext().setAuthentication(authentication); // 設置安全上下文}}filterChain.doFilter(request, response); // 繼續執行后續過濾器}
}
Spring Security 配置:
禁用 CSRF,添加 JWT 過濾器并配置權限規則:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {private final JwtUtil jwtUtil;public SecurityConfig(JwtUtil jwtUtil) {this.jwtUtil = jwtUtil;}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable() // 前后端分離場景禁用 CSRF.authorizeRequests().antMatchers(HttpMethod.POST, "/login").permitAll() // 允許登錄接口.antMatchers("/admin/**").hasRole("ADMIN") // 管理員接口需 ADMIN 角色.anyRequest().authenticated() // 其他接口需認證.and().addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class); // 添加 JWT 過濾器}
}
通過 Spring Security 的整合,可更便捷地實現細粒度權限控制(如 @PreAuthorize
注解)和安全策略管理。