工作中七天免登录如何实现
作为一名Java后端高级开发,我敢说"七天免登录"是业务系统里最常见的需求之一------用户登录一次后,一周内再次访问系统无需重复输入账号密码,直接就能进入主页。这个需求看似简单,但实现不好很容易踩坑:要么免登录失效影响用户体验,要么出现安全漏洞导致账号被盗。
很多初级开发会直接把用户信息存Cookie,或者简单用Session过期时间控制,这些做法要么不安全,要么在分布式环境下失效。今天这篇文章,我就结合实际工作经验,讲透"七天免登录"的标准实现方案,从原理到代码全拆解,看完就能直接落地。
一、先搞懂:七天免登录的核心原理
免登录的本质很简单:用户首次登录成功后,服务器生成一个"身份凭证"返回给客户端,客户端持久化存储;后续用户访问时,自动携带这个凭证,服务器验证通过后就直接放行。
这里的关键是解决三个问题:
- 凭证怎么生成?要唯一、不可伪造、带过期时间;
- 凭证存在哪?客户端存储方案要兼顾安全和可用性;
- 怎么验证?服务器要快速校验凭证的合法性,还要支持分布式部署。
工作中最成熟的方案是:Cookie + JWT Token + Redis黑名单。为什么选这个组合?
核心优势:JWT自带过期时间和签名机制,能避免伪造;Cookie自动携带凭证,无需前端额外处理;Redis存储黑名单,解决JWT无法主动失效的问题,还能支撑分布式系统。
二、分步实现:七天免登录完整流程(附实战代码)
我们基于Spring Boot框架实现,整体流程分为5步:用户登录生成凭证→客户端存储凭证→拦截器校验凭证→活跃续期→退出登录失效。下面逐一拆解,代码可直接复用。
1. 第一步:准备依赖和核心配置
首先引入JWT和Redis依赖(如果是单体应用,Redis可选,但分布式必须要):
xml
<!-- 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参数:
yaml
# 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合法性。
typescript
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)。
kotlin
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 自定义拦截器
kotlin
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 注册拦截器
kotlin
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黑名单(避免被盗用)。
scss
@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无法主动失效的问题;
- 做好活跃续期,提升用户体验。