工作中七天免登录如何实现

工作中七天免登录如何实现

作为一名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&gt;

<!-- 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被盗用,又要保证分布式环境下正常工作,还要兼顾用户体验。

本文给出的方案是工作中的标准实现,代码可直接落地。核心要点总结:

  1. 用JWT生成带签名和过期时间的凭证,避免伪造;
  2. 用Cookie存储凭证,开启HttpOnly、Secure等安全属性;
  3. 用拦截器统一校验凭证,实现自动登录;
  4. 分布式环境必须用Redis维护黑名单,解决JWT无法主动失效的问题;
  5. 做好活跃续期,提升用户体验。
相关推荐
小杨同学492 小时前
C 语言实战:水果总价计算程序(结构体应用 + 细节优化)
后端·算法·程序员
q***44152 小时前
Java性能优化实战技术文章大纲Java性能优化的核心目标与原则
java·开发语言·性能优化
用户948357016512 小时前
《自动化埋点:利用 AOP 统一记录接口入参、出参及执行耗时》
后端
undsky2 小时前
【RuoYi-SpringBoot3-Pro】:多租户功能上手指南
spring boot·后端·mybatis
明天有专业课2 小时前
松耦合的设计模式-观察者
后端
毕设源码-郭学长2 小时前
【开题答辩全过程】以 基于springbootvue图书馆选座系统设计与实现为例,包含答辩的问题和答案
java
鱼跃鹰飞2 小时前
怎么排查线上CPU100%的问题
java·jvm·后端
Seven972 小时前
剑指offer-62、⼆叉搜索树的第k个结点
java
哈库纳2 小时前
dbVisitor 的双层适配架构
后端