引言
作为一名java后端高级开发,我敢说“七天免登录”是业务系统里最常见的需求之一——用户登录一次后,一周内再次访问系统无需重复输入账号密码,直接就能进入主页。这个需求看似简单,但实现不好很容易踩坑:要么免登录失效影响用户体验,要么出现安全漏洞导致账号被盗。
很多初级开发会直接把用户信息存cookie,或者简单用session过期时间控制,这些做法要么不安全,要么在分布式环境下失效。今天这篇文章,我就结合实际工作经验,讲透“七天免登录”的标准实现方案,从原理到代码全拆解,看完就能直接落地。
一、先搞懂:七天免登录的核心原理
免登录的本质很简单:用户首次登录成功后,服务器生成一个“身份凭证”返回给客户端,客户端持久化存储;后续用户访问时,自动携带这个凭证,服务器验证通过后就直接放行。
这里的关键是解决三个问题:
- 凭证怎么生成?要唯一、不可伪造、带过期时间;
- 凭证存在哪?客户端存储方案要兼顾安全和可用性;
- 怎么验证?服务器要快速校验凭证的合法性,还要支持分布式部署。
工作中最成熟的方案是:cookie + jwt token + redis黑名单。为什么选这个组合?
核心优势:jwt自带过期时间和签名机制,能避免伪造;cookie自动携带凭证,无需前端额外处理;redis存储黑名单,解决jwt无法主动失效的问题,还能支撑分布式系统。
二、分步实现:七天免登录完整流程(附实战代码)
我们基于spring boot框架实现,整体流程分为5步:用户登录生成凭证→客户端存储凭证→拦截器校验凭证→活跃续期→退出登录失效。下面逐一拆解,代码可直接复用。
1. 第一步:准备依赖和核心配置
首先引入jwt和redis依赖(如果是单体应用,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密钥、过期时间、cookie参数:
# jwt配置 jwt: secret: your-secret-key-32bytes-long-12345678 # 密钥必须足够长(建议32位),放配置中心,不要硬编码 expire: 604800000 # 7天过期(单位:毫秒) refresh-expire: 86400000 # 1天内活跃自动续期(单位:毫秒) # cookie配置 cookie: name: auto_login_token # cookie名称 domain: localhost # 域名(生产环境填实际域名,如xxx.com) path: / # 作用路径 max-age: 604800 # 7天(单位:秒) http-only: true # 仅http访问,禁止js操作(防xss) secure: false # 生产环境开启https后设为true(仅https传输) same-site: lax # 防csrf攻击
2. 第二步:封装jwt工具类(核心)
jwt负责生成和解析身份凭证,核心是“签名防伪造”和“自带过期时间”。工具类包含3个核心方法:生成token、解析token、验证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密钥和过期时间
@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()) // 签发时间
.setexpiration(new date(system.currenttimemillis() + expire)) // 过期时间
.signwith(key) // 签名
.compact();
}
// 解析token,获取载荷信息
public claims parsetoken(string token) {
secretkey key = keys.hmacshakeyfor(secret.getbytes());
return jwts.parserbuilder()
.setsigningkey(key)
.build()
.parseclaimsjws(token)
.getbody();
}
// 验证token是否合法(未过期+签名正确)
public boolean validatetoken(string token) {
try {
claims claims = parsetoken(token);
// 检查是否过期
return !claims.getexpiration().before(new date());
} catch (exception e) {
// 解析失败(签名错误、过期、格式错误)都返回false
return false;
}
}
}
3. 第三步:登录接口生成凭证(核心流程)
用户首次登录成功后,生成jwt token,然后通过cookie返回给客户端存储。这里要注意:敏感信息(如密码)不能放进jwt载荷,只放非敏感的用户标识(如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; // 自定义用户服务(校验账号密码)
@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. 校验账号密码(实际业务中要加密校验,如bcrypt)
user user = userservice.verifyuser(logindto.getusername(), logindto.getpassword());
if (user == null) {
return result.fail("账号或密码错误");
}
// 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(可选,用于黑名单校验)
// 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); // 生产环境https开启
cookie.setattribute("samesite", cookiesamesite); // 防csrf
response.addcookie(cookie);
return result.success("登录成功");
}
}
4. 第四步:拦截器校验凭证(自动登录核心)
用户后续访问系统时,浏览器会自动携带cookie中的token。我们用spring拦截器拦截所有请求,校验token合法性——合法则放行,不合法则跳转到登录页。
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天内活跃自动续期
@override
public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) throws exception {
// 1. 跳过登录接口(避免拦截登录请求)
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不存在,跳转到登录页
if (token == null) {
response.sendredirect("/login.html");
return false;
}
// 4. 校验token合法性(未过期+签名正确)
if (!jwtutil.validatetoken(token)) {
response.sendredirect("/login.html");
return false;
}
// 5. (分布式必做)校验token是否在黑名单(用户退出登录后失效)
boolean isblack = redistemplate.haskey("auto_login:blacklist:" + token);
if (boolean.true.equals(isblack)) {
response.sendredirect("/login.html");
return false;
}
// 6. 解析token,获取用户信息,存入request(后续业务可用)
claims claims = jwtutil.parsetoken(token);
request.setattribute("userid", claims.get("userid"));
request.setattribute("username", claims.get("username"));
// 7. 活跃续期:如果token剩余有效期小于1天,自动刷新token(提升用户体验)
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. 校验通过,放行
return true;
}
}
4.2 注册拦截器
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("/**") // 拦截所有请求
.excludepathpatterns("/login", "/login.html", "/static/**"); // 排除登录页和静态资源
}
}
5. 第五步:退出登录(凭证失效)
用户主动退出登录时,需要清除客户端的cookie,同时将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(设置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("退出成功");
}
三、高级开发必关注:安全防护细节
七天免登录的核心风险是“凭证被盗用”,一旦token被别人获取,就能直接登录用户账号。作为高级开发,必须做好以下5点防护:
1. cookie安全属性必须设对
- httponly=true:禁止javascript操作cookie,防止xss攻击窃取token;
- secure=true:仅在https协议下传输cookie,避免http协议被抓包窃取;
- samesite=lax:限制cookie仅在同站点请求中携带,防止csrf攻击;
- domain和path精准配置:不要设为顶级域名(如.com),避免cookie被同域名下的其他应用获取。
2. jwt密钥不能硬编码
jwt的安全性依赖于密钥,必须将密钥放在配置中心(如nacos、apollo),禁止硬编码在代码里。密钥长度至少32位,建议用随机字符串生成(如uuid)。
3. 分布式环境必须用redis黑名单
jwt本身是无状态的,一旦生成无法主动失效。用户退出登录后,必须将token加入redis黑名单,拦截器校验时先查黑名单,避免token被复用。
4. 载荷不存敏感信息
jwt的载荷是base64编码的,不是加密的,任何人都能解码查看。因此不能存放密码、手机号、身份证等敏感信息,只放userid、username等非敏感标识。
5. 可选:结合设备/ip验证
如果业务安全性要求高,可以在生成token时,将用户的设备信息(如浏览器版本、系统版本)、ip地址存入载荷。校验时对比当前请求的设备/ip,不一致则拒绝登录(注意:ip可能动态变化,需平衡安全性和用户体验)。
四、避坑指南:工作中常见问题解决
结合实际开发经验,我总结了3个常见坑,帮你快速避坑:
坑1:免登录在分布式环境下失效
原因:不同服务节点生成的token不同,或者cookie没有共享。
解决方案:
- 所有服务使用相同的jwt密钥(配置中心统一配置);
- cookie的domain设为服务的统一域名(如api.xxx.com);
- 用redis统一存储token黑名单,所有服务共享黑名单。
坑2:token过期前用户活跃,却被要求重新登录
原因:没有做活跃续期,token到期后直接失效。
解决方案:在拦截器中判断token剩余有效期,小于1天(或其他阈值)时,自动生成新token并更新cookie,实现“无缝续期”。
坑3:cookie跨域无法携带
原因:前后端分离项目中,前端和后端域名不同,cookie跨域不携带。
解决方案:
- 后端配置cors,允许前端域名的跨域请求,同时设置allowcredentials=true;
- 前端请求时设置withcredentials=true(如axios、fetch);
- cookie的domain设为后端域名,确保跨域请求时能携带。
五、总结
七天免登录的核心实现逻辑很简单:登录生成jwt token→cookie存储→拦截器校验→活跃续期→退出加入黑名单。但关键在于“安全”和“兼容性”——既要防止token被盗用,又要保证分布式环境下正常工作,还要兼顾用户体验。
本文给出的方案是工作中的标准实现,代码可直接落地。核心要点总结:
- 用jwt生成带签名和过期时间的凭证,避免伪造;
- 用cookie存储凭证,开启httponly、secure等安全属性;
- 用拦截器统一校验凭证,实现自动登录;
- 分布式环境必须用redis维护黑名单,解决jwt无法主动失效的问题;
- 做好活跃续期,提升用户体验。
以上就是基于springboot实现七天免登录的完整流程的详细内容,更多关于springboot七天免登录的资料请关注代码网其它相关文章!
发表评论