基于SpringBoot實(shí)現(xiàn)七天免登錄的完整流程
引言
作為一名Java后端高級(jí)開發(fā),我敢說“七天免登錄”是業(yè)務(wù)系統(tǒng)里最常見的需求之一——用戶登錄一次后,一周內(nèi)再次訪問系統(tǒng)無需重復(fù)輸入賬號(hào)密碼,直接就能進(jìn)入主頁。這個(gè)需求看似簡(jiǎn)單,但實(shí)現(xiàn)不好很容易踩坑:要么免登錄失效影響用戶體驗(yàn),要么出現(xiàn)安全漏洞導(dǎo)致賬號(hào)被盜。
很多初級(jí)開發(fā)會(huì)直接把用戶信息存Cookie,或者簡(jiǎn)單用Session過期時(shí)間控制,這些做法要么不安全,要么在分布式環(huán)境下失效。今天這篇文章,我就結(jié)合實(shí)際工作經(jīng)驗(yàn),講透“七天免登錄”的標(biāo)準(zhǔn)實(shí)現(xiàn)方案,從原理到代碼全拆解,看完就能直接落地。
一、先搞懂:七天免登錄的核心原理
免登錄的本質(zhì)很簡(jiǎn)單:用戶首次登錄成功后,服務(wù)器生成一個(gè)“身份憑證”返回給客戶端,客戶端持久化存儲(chǔ);后續(xù)用戶訪問時(shí),自動(dòng)攜帶這個(gè)憑證,服務(wù)器驗(yàn)證通過后就直接放行。
這里的關(guān)鍵是解決三個(gè)問題:
- 憑證怎么生成?要唯一、不可偽造、帶過期時(shí)間;
- 憑證存在哪?客戶端存儲(chǔ)方案要兼顧安全和可用性;
- 怎么驗(yàn)證?服務(wù)器要快速校驗(yàn)憑證的合法性,還要支持分布式部署。
工作中最成熟的方案是:Cookie + JWT Token + Redis黑名單。為什么選這個(gè)組合?
核心優(yōu)勢(shì):JWT自帶過期時(shí)間和簽名機(jī)制,能避免偽造;Cookie自動(dòng)攜帶憑證,無需前端額外處理;Redis存儲(chǔ)黑名單,解決JWT無法主動(dòng)失效的問題,還能支撐分布式系統(tǒng)。
二、分步實(shí)現(xiàn):七天免登錄完整流程(附實(shí)戰(zhàn)代碼)
我們基于Spring Boot框架實(shí)現(xiàn),整體流程分為5步:用戶登錄生成憑證→客戶端存儲(chǔ)憑證→攔截器校驗(yàn)憑證→活躍續(xù)期→退出登錄失效。下面逐一拆解,代碼可直接復(fù)用。
1. 第一步:準(zhǔn)備依賴和核心配置
首先引入JWT和Redis依賴(如果是單體應(yīng)用,Redis可選,但分布式必須要):
<!-- JWT依賴 -->
<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>
<!-- Redis依賴(分布式必選) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
然后在application.yml配置JWT密鑰、過期時(shí)間、Cookie參數(shù):
# JWT配置 jwt: secret: your-secret-key-32bytes-long-12345678 # 密鑰必須足夠長(建議32位),放配置中心,不要硬編碼 expire: 604800000 # 7天過期(單位:毫秒) refresh-expire: 86400000 # 1天內(nèi)活躍自動(dòng)續(xù)期(單位:毫秒) # Cookie配置 cookie: name: auto_login_token # Cookie名稱 domain: localhost # 域名(生產(chǎn)環(huán)境填實(shí)際域名,如xxx.com) path: / # 作用路徑 max-age: 604800 # 7天(單位:秒) http-only: true # 僅HTTP訪問,禁止JS操作(防XSS) secure: false # 生產(chǎn)環(huán)境開啟HTTPS后設(shè)為true(僅HTTPS傳輸) same-site: Lax # 防CSRF攻擊
2. 第二步:封裝JWT工具類(核心)
JWT負(fù)責(zé)生成和解析身份憑證,核心是“簽名防偽造”和“自帶過期時(shí)間”。工具類包含3個(gè)核心方法:生成Token、解析Token、驗(yàn)證Token合法性。
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.Map;
@Component
public class JwtUtil {
// 注入JWT密鑰和過期時(shí)間
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expire}")
private long expire;
// 生成JWT Token(傳入用戶信息,如userId、username)
public String generateToken(Map<String, Object> claims) {
// 密鑰編碼(必須和配置的密鑰長度匹配)
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
return Jwts.builder()
.setClaims(claims) // 自定義載荷(存放用戶信息)
.setIssuedAt(new Date()) // 簽發(fā)時(shí)間
.setExpiration(new Date(System.currentTimeMillis() + expire)) // 過期時(shí)間
.signWith(key) // 簽名
.compact();
}
// 解析Token,獲取載荷信息
public Claims parseToken(String token) {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
// 驗(yàn)證Token是否合法(未過期+簽名正確)
public boolean validateToken(String token) {
try {
Claims claims = parseToken(token);
// 檢查是否過期
return !claims.getExpiration().before(new Date());
} catch (Exception e) {
// 解析失?。ê灻e(cuò)誤、過期、格式錯(cuò)誤)都返回false
return false;
}
}
}
3. 第三步:登錄接口生成憑證(核心流程)
用戶首次登錄成功后,生成JWT Token,然后通過Cookie返回給客戶端存儲(chǔ)。這里要注意:敏感信息(如密碼)不能放進(jìn)JWT載荷,只放非敏感的用戶標(biāo)識(shí)(如userId、username)。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@RestController
public class LoginController {
@Autowired
private UserService userService; // 自定義用戶服務(wù)(校驗(yàn)賬號(hào)密碼)
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RedisTemplate<String, Object> redisTemplate; // Redis模板(分布式用)
// 注入Cookie配置
@Value("${cookie.name}")
private String cookieName;
@Value("${cookie.domain}")
private String cookieDomain;
@Value("${cookie.path}")
private String cookiePath;
@Value("${cookie.max-age}")
private int cookieMaxAge;
@Value("${cookie.http-only}")
private boolean cookieHttpOnly;
@Value("${cookie.secure}")
private boolean cookieSecure;
@Value("${cookie.same-site}")
private String cookieSameSite;
@PostMapping("/login")
public Result login(@RequestBody LoginDTO loginDTO, HttpServletResponse response) {
// 1. 校驗(yàn)賬號(hào)密碼(實(shí)際業(yè)務(wù)中要加密校驗(yàn),如BCrypt)
User user = userService.verifyUser(loginDTO.getUsername(), loginDTO.getPassword());
if (user == null) {
return Result.fail("賬號(hào)或密碼錯(cuò)誤");
}
// 2. 生成JWT Token(載荷放userId和username,非敏感信息)
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
String token = jwtUtil.generateToken(claims);
// 3. (分布式必做)將Token存入Redis(可選,用于黑名單校驗(yàn))
// redisTemplate.opsForValue().set("auto_login:blacklist:" + token, user.getId(), jwtUtil.getExpire(), TimeUnit.MILLISECONDS);
// 4. 生成Cookie,返回給客戶端
Cookie cookie = new Cookie(cookieName, token);
cookie.setDomain(cookieDomain);
cookie.setPath(cookiePath);
cookie.setMaxAge(cookieMaxAge); // 7天過期
cookie.setHttpOnly(cookieHttpOnly); // 防XSS
cookie.setSecure(cookieSecure); // 生產(chǎn)環(huán)境HTTPS開啟
cookie.setAttribute("SameSite", cookieSameSite); // 防CSRF
response.addCookie(cookie);
return Result.success("登錄成功");
}
}
4. 第四步:攔截器校驗(yàn)憑證(自動(dòng)登錄核心)
用戶后續(xù)訪問系統(tǒng)時(shí),瀏覽器會(huì)自動(dòng)攜帶Cookie中的Token。我們用Spring攔截器攔截所有請(qǐng)求,校驗(yàn)Token合法性——合法則放行,不合法則跳轉(zhuǎn)到登錄頁。
4.1 自定義攔截器
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class AutoLoginInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Value("${cookie.name}")
private String cookieName;
@Value("${jwt.refresh-expire}")
private long refreshExpire; // 1天內(nèi)活躍自動(dòng)續(xù)期
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 跳過登錄接口(避免攔截登錄請(qǐng)求)
if (request.getRequestURI().contains("/login")) {
return true;
}
// 2. 從Cookie中獲取Token
String token = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookieName.equals(cookie.getName())) {
token = cookie.getValue();
break;
}
}
}
// 3. Token不存在,跳轉(zhuǎn)到登錄頁
if (token == null) {
response.sendRedirect("/login.html");
return false;
}
// 4. 校驗(yàn)Token合法性(未過期+簽名正確)
if (!jwtUtil.validateToken(token)) {
response.sendRedirect("/login.html");
return false;
}
// 5. (分布式必做)校驗(yàn)Token是否在黑名單(用戶退出登錄后失效)
Boolean isBlack = redisTemplate.hasKey("auto_login:blacklist:" + token);
if (Boolean.TRUE.equals(isBlack)) {
response.sendRedirect("/login.html");
return false;
}
// 6. 解析Token,獲取用戶信息,存入Request(后續(xù)業(yè)務(wù)可用)
Claims claims = jwtUtil.parseToken(token);
request.setAttribute("userId", claims.get("userId"));
request.setAttribute("username", claims.get("username"));
// 7. 活躍續(xù)期:如果Token剩余有效期小于1天,自動(dòng)刷新Token(提升用戶體驗(yàn))
long remainTime = claims.getExpiration().getTime() - System.currentTimeMillis();
if (remainTime < refreshExpire) {
Map<String, Object> newClaims = new HashMap<>();
newClaims.put("userId", claims.get("userId"));
newClaims.put("username", claims.get("username"));
String newToken = jwtUtil.generateToken(newClaims);
// 更新Cookie中的Token
Cookie newCookie = new Cookie(cookieName, newToken);
newCookie.setDomain(request.getServerName());
newCookie.setPath("/");
newCookie.setMaxAge(cookieMaxAge);
newCookie.setHttpOnly(true);
newCookie.setSecure(false);
newCookie.setAttribute("SameSite", "Lax");
response.addCookie(newCookie);
// 更新Redis中的Token(分布式必做)
// redisTemplate.delete("auto_login:blacklist:" + token);
// redisTemplate.opsForValue().set("auto_login:blacklist:" + newToken, claims.get("userId"), jwtUtil.getExpire(), TimeUnit.MILLISECONDS);
}
// 8. 校驗(yàn)通過,放行
return true;
}
}
4.2 注冊(cè)攔截器
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private AutoLoginInterceptor autoLoginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoLoginInterceptor)
.addPathPatterns("/**") // 攔截所有請(qǐng)求
.excludePathPatterns("/login", "/login.html", "/static/**"); // 排除登錄頁和靜態(tài)資源
}
}
5. 第五步:退出登錄(憑證失效)
用戶主動(dòng)退出登錄時(shí),需要清除客戶端的Cookie,同時(shí)將Token加入Redis黑名單(避免被盜用)。
@PostMapping("/logout")
public Result logout(HttpServletRequest request, HttpServletResponse response) {
// 1. 從Cookie中獲取Token
String token = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookieName.equals(cookie.getName())) {
token = cookie.getValue();
break;
}
}
}
// 2. 將Token加入Redis黑名單(分布式必做)
if (token != null) {
// 黑名單有效期和Token一致
redisTemplate.opsForValue().set("auto_login:blacklist:" + token,
request.getAttribute("userId"),
jwtUtil.getExpire(),
TimeUnit.MILLISECONDS);
}
// 3. 清除Cookie(設(shè)置maxAge=0)
Cookie cookie = new Cookie(cookieName, null);
cookie.setDomain(cookieDomain);
cookie.setPath(cookiePath);
cookie.setMaxAge(0); // 立即過期
cookie.setHttpOnly(true);
cookie.setSecure(false);
cookie.setAttribute("SameSite", "Lax");
response.addCookie(cookie);
return Result.success("退出成功");
}
三、高級(jí)開發(fā)必關(guān)注:安全防護(hù)細(xì)節(jié)
七天免登錄的核心風(fēng)險(xiǎn)是“憑證被盜用”,一旦Token被別人獲取,就能直接登錄用戶賬號(hào)。作為高級(jí)開發(fā),必須做好以下5點(diǎn)防護(hù):
1. Cookie安全屬性必須設(shè)對(duì)
- HttpOnly=true:禁止JavaScript操作Cookie,防止XSS攻擊竊取Token;
- Secure=true:僅在HTTPS協(xié)議下傳輸Cookie,避免HTTP協(xié)議被抓包竊??;
- SameSite=Lax:限制Cookie僅在同站點(diǎn)請(qǐng)求中攜帶,防止CSRF攻擊;
- Domain和Path精準(zhǔn)配置:不要設(shè)為頂級(jí)域名(如.com),避免Cookie被同域名下的其他應(yīng)用獲取。
2. JWT密鑰不能硬編碼
JWT的安全性依賴于密鑰,必須將密鑰放在配置中心(如Nacos、Apollo),禁止硬編碼在代碼里。密鑰長度至少32位,建議用隨機(jī)字符串生成(如UUID)。
3. 分布式環(huán)境必須用Redis黑名單
JWT本身是無狀態(tài)的,一旦生成無法主動(dòng)失效。用戶退出登錄后,必須將Token加入Redis黑名單,攔截器校驗(yàn)時(shí)先查黑名單,避免Token被復(fù)用。
4. 載荷不存敏感信息
JWT的載荷是Base64編碼的,不是加密的,任何人都能解碼查看。因此不能存放密碼、手機(jī)號(hào)、身份證等敏感信息,只放userId、username等非敏感標(biāo)識(shí)。
5. 可選:結(jié)合設(shè)備/IP驗(yàn)證
如果業(yè)務(wù)安全性要求高,可以在生成Token時(shí),將用戶的設(shè)備信息(如瀏覽器版本、系統(tǒng)版本)、IP地址存入載荷。校驗(yàn)時(shí)對(duì)比當(dāng)前請(qǐng)求的設(shè)備/IP,不一致則拒絕登錄(注意:IP可能動(dòng)態(tài)變化,需平衡安全性和用戶體驗(yàn))。
四、避坑指南:工作中常見問題解決
結(jié)合實(shí)際開發(fā)經(jīng)驗(yàn),我總結(jié)了3個(gè)常見坑,幫你快速避坑:
坑1:免登錄在分布式環(huán)境下失效
原因:不同服務(wù)節(jié)點(diǎn)生成的Token不同,或者Cookie沒有共享。
解決方案:
- 所有服務(wù)使用相同的JWT密鑰(配置中心統(tǒng)一配置);
- Cookie的Domain設(shè)為服務(wù)的統(tǒng)一域名(如api.xxx.com);
- 用Redis統(tǒng)一存儲(chǔ)Token黑名單,所有服務(wù)共享黑名單。
坑2:Token過期前用戶活躍,卻被要求重新登錄
原因:沒有做活躍續(xù)期,Token到期后直接失效。
解決方案:在攔截器中判斷Token剩余有效期,小于1天(或其他閾值)時(shí),自動(dòng)生成新Token并更新Cookie,實(shí)現(xiàn)“無縫續(xù)期”。
坑3:Cookie跨域無法攜帶
原因:前后端分離項(xiàng)目中,前端和后端域名不同,Cookie跨域不攜帶。
解決方案:
- 后端配置CORS,允許前端域名的跨域請(qǐng)求,同時(shí)設(shè)置allowCredentials=true;
- 前端請(qǐng)求時(shí)設(shè)置withCredentials=true(如Axios、Fetch);
- Cookie的Domain設(shè)為后端域名,確??缬蛘?qǐng)求時(shí)能攜帶。
五、總結(jié)
七天免登錄的核心實(shí)現(xiàn)邏輯很簡(jiǎn)單:登錄生成JWT Token→Cookie存儲(chǔ)→攔截器校驗(yàn)→活躍續(xù)期→退出加入黑名單。但關(guān)鍵在于“安全”和“兼容性”——既要防止Token被盜用,又要保證分布式環(huán)境下正常工作,還要兼顧用戶體驗(yàn)。
本文給出的方案是工作中的標(biāo)準(zhǔn)實(shí)現(xiàn),代碼可直接落地。核心要點(diǎn)總結(jié):
- 用JWT生成帶簽名和過期時(shí)間的憑證,避免偽造;
- 用Cookie存儲(chǔ)憑證,開啟HttpOnly、Secure等安全屬性;
- 用攔截器統(tǒng)一校驗(yàn)憑證,實(shí)現(xiàn)自動(dòng)登錄;
- 分布式環(huán)境必須用Redis維護(hù)黑名單,解決JWT無法主動(dòng)失效的問題;
- 做好活躍續(xù)期,提升用戶體驗(yàn)。
以上就是基于SpringBoot實(shí)現(xiàn)七天免登錄的完整流程的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot七天免登錄的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java?file類中renameTo方法操作實(shí)例
renameTo()方法是File類的一部分,renameTo()函數(shù)用于將文件的抽象路徑名重命名為給定的路徑名??,下面這篇文章主要給大家介紹了關(guān)于Java?file類中renameTo方法操作的相關(guān)資料,需要的朋友可以參考下2022-11-11
Spring實(shí)戰(zhàn)之ResourceLoaderAware加載資源用法示例
這篇文章主要介紹了Spring實(shí)戰(zhàn)之ResourceLoaderAware加載資源用法,結(jié)合實(shí)例形式分析了spring使用ResourceLoaderAware加載資源相關(guān)配置與操作技巧,需要的朋友可以參考下2020-01-01
Java 實(shí)戰(zhàn)項(xiàng)目之精美物流管理系統(tǒng)的實(shí)現(xiàn)流程
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SpringBoot+Vue+maven+Mysql實(shí)現(xiàn)一個(gè)精美的物流管理系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平2021-11-11
如何取消IDEA中的項(xiàng)目和Git倉庫的關(guān)聯(lián)
這篇文章介紹了一種取消已經(jīng)開啟代碼控制的項(xiàng)目與Git倉庫關(guān)聯(lián)的簡(jiǎn)單方法,首先,在IntelliJ IDEA中打開項(xiàng)目,刪除.idea目錄下的vcs.xml文件,這樣IDE界面上的Git圖標(biāo)就會(huì)消失,接下來,打開項(xiàng)目所在位置,通過文件管理器啟用“顯示隱藏的項(xiàng)目”選項(xiàng)2024-10-10
Struts2學(xué)習(xí)教程之Action類如何訪問WEB資源
這篇文章主要給大家介紹了關(guān)于Struts2學(xué)習(xí)教程之Action類如何訪問WEB資源的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-04-04
springboot+maven快速構(gòu)建項(xiàng)目的示例代碼
本篇文章主要介紹了springboot+maven快速構(gòu)建項(xiàng)目的示例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
java實(shí)現(xiàn)簡(jiǎn)單音樂播放器
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)簡(jiǎn)單音樂播放器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-06-06
SpringBoot啟動(dòng)yaml報(bào)錯(cuò)的解決
這篇文章主要介紹了SpringBoot啟動(dòng)yaml報(bào)錯(cuò)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08

