Java中Token的详细讲解

注:部分转载 【杂谈】傻傻分不清之 Cookie、Session、Token、J WT_jsw和session的区别

一、Token是什么?

Token(令牌) 是一种用于身份认证和授权的凭证,用于替代传统的Session机制。

1.1 为什么需要Token?

// ❌ 传统Session方式的问题:
// 1. 服务器需要存储Session,占用内存
// 2. 分布式环境下Session共享困难
// 3. 跨域问题
// 4. 移动端支持不友好

// ✅ Token方式的优势:
// 1. 无状态,服务器不存储
// 2. 天然支持分布式
// 3. 跨域友好
// 4. 支持移动端、小程序等多端

1.2 Acesss Token和Refresh Token

转载 【杂谈】傻傻分不清之 Cookie、Session、Token、J WT_jsw和session的区别

Acesss Token

  • 访问资源接口(API)时所需要的资源凭证
  • 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)
  • 特点:
    服务端无状态化、可扩展性好
    支持移动端设备
    安全
    支持跨程序调用
  • token 的身份验证流程:
    在这里插入图片描述

客户端使用用户名跟密码请求登录
服务端收到请求,去验证用户名与密码
验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里
客户端每次向服务端请求资源的时候需要带着服务端签发的 token
服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据
每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里

  • 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
  • token 完全由应用管理,所以它可以避开同源策略

Refresh Token

  • 另外一种 token——refresh token
  • refresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。

在这里插入图片描述

  • Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token 就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。
  • Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token 时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。

作者:机器学习之心AI
链接:https://juejin.cn/post/7417843794328436775
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

二、Token的类型

2.1 JWT (JSON Web Token) - 最常用

// JWT结构:Header.Payload.Signature
// 示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOjEyMzQsInVzZXJuYW1lIjoiamhvbiIsImV4cCI6MTYzNzc2ODQwMH0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

/**
 * JWT Token 结构
 */
public class JwtStructure {
    
    // Part 1: Header(头部)- 描述Token类型和加密算法
    {
        "alg": "HS256",  // 加密算法
        "typ": "JWT"     // Token类型
    }
    
    // Part 2: Payload(载荷)- 存储实际数据
    {
        "userId": 1234,           // 用户ID
        "username": "john",       // 用户名
        "roles": ["admin"],       // 角色
        "exp": 1637768400,        // 过期时间
        "iat": 1637682000         // 签发时间
    }
    
    // Part 3: Signature(签名)- 防止数据被篡改
    // HMACSHA256(
    //   base64UrlEncode(header) + "." + base64UrlEncode(payload),
    //   secret
    // )
}

转载 【杂谈】傻傻分不清之 Cookie、Session、Token、J WT_jsw和session的区别

什么是JWT

  • JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。
  • 是一种认证授权机制。
  • JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
  • 可以使用 HMAC 算法或者是 RSA 的公 / 私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。
  • 阮一峰老师的 JSON Web Token 入门教程 讲的非常通俗易懂,这里就不再班门弄斧了
生成JWT

www.jwt.io/
www.jsonwebtoken.io/

JWT的原理

在这里插入图片描述

  • JWT 认证流程:
    • 用户输入用户名 / 密码登录,服务端认证成功后,会返回给客户端一个 JWT
    • 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)
    • 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用 Bearer 模式添加 JWT,其内容看起来是下面这样
Authorization: Bearer <token>
  • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为
  • 因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要
  • 因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)
  • 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制
JWT的使用方式
  • 客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
方式一
  • 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。
GET /calendar/v1/events
Host: api.example.com
Authorization: Bearer <token>


  • 用户的状态不会存储在服务端的内存中,这是一种 无状态的认证机制
  • 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为。
  • 由于 JWT 是自包含的,因此减少了需要查询数据库的需要
  • JWT 的这些特性使得我们可以完全依赖其无状态的特性提供数据 API 服务,甚至是创建一个下载流服务。
  • 因为 JWT 并不使用 Cookie ,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)
方式二
  • 跨域的时候,可以把 JWT 放在 POST 请求的数据体里。
方式三
  • 通过 URL 传输
http://www.example.com/user?token=xxx
项目中使用JWT
Token 和 JWT 的区别
  • Token 和 JWT 的区别
    • 相同:
      • 都是访问资源的令牌
      • 都可以记录用户的信息
      • 都是使服务端无状态化
      • 都是只有验证成功后,客户端才能访问服务端上受保护的资源
    • 区别:
      • Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
      • JWT:将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。

2.2 其他Token类型

/**
 * 1. UUID Token - 简单的随机字符串
 */
String uuidToken = UUID.randomUUID().toString();
// 优点:简单
// 缺点:需要服务器存储,本质是Session的变种

/**
 * 2. OAuth2 Token - 用于第三方授权
 */
// Access Token:访问令牌(短期有效)
// Refresh Token:刷新令牌(长期有效)

/**
 * 3. 自定义加密Token
 */
// 使用AES、RSA等加密算法自定义Token格式

三、Token的完整应用流程

3.1 登录生成Token

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * Token工具类
 */
public class TokenUtils {
    
    // 密钥(实际开发中应该配置在配置文件中)
    private static final String SECRET_KEY = "mySecretKeyForJWT1234567890123456"; // 至少32字节
    private static final Key KEY = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
    
    // Token过期时间(7天)
    private static final long EXPIRATION_TIME = 7 * 24 * 60 * 60 * 1000;
    
    /**
     * 生成Token
     */
    public static String generateToken(Long userId, String username) {
        // 当前时间
        Date now = new Date();
        // 过期时间
        Date expirationDate = new Date(now.getTime() + EXPIRATION_TIME);
        
        // 自定义Claims(载荷数据)
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);
        claims.put("username", username);
        claims.put("loginTime", now.getTime());
        
        // 生成JWT Token
        return Jwts.builder()
                .setClaims(claims)                          // 设置载荷数据
                .setSubject(username)                        // 设置主题(一般是用户标识)
                .setIssuedAt(now)                           // 签发时间
                .setExpiration(expirationDate)              // 过期时间
                .signWith(KEY, SignatureAlgorithm.HS256)   // 签名算法
                .compact();
    }
    
    /**
     * 解析Token
     */
    public static Map<String, Object> parseToken(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(KEY)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            throw new RuntimeException("Token解析失败", e);
        }
    }
    
    /**
     * 验证Token是否有效
     */
    public static boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(KEY)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    /**
     * 从Token中获取用户ID
     */
    public static Long getUserIdFromToken(String token) {
        Map<String, Object> claims = parseToken(token);
        return Long.valueOf(claims.get("userId").toString());
    }
}

3.2 登录Controller

import org.springframework.web.bind.annotation.*;

/**
 * 登录控制器
 */
@RestController
@RequestMapping("/auth")
public class AuthController {
    
    @Resource
    private UserService userService;
    
    /**
     * 用户登录
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginRequest request) {
        // 1. 验证用户名密码
        User user = userService.validateUser(request.getUsername(), request.getPassword());
        if (user == null) {
            return Result.error("用户名或密码错误");
        }
        
        // 2. 生成Token
        String token = TokenUtils.generateToken(user.getId(), user.getUsername());
        
        // 3. 返回Token给前端
        Map<String, Object> data = new HashMap<>();
        data.put("token", token);
        data.put("userInfo", user);
        
        return Result.success(data);
    }
    
    /**
     * 用户登出(可选)
     */
    @PostMapping("/logout")
    public Result logout(@RequestHeader("Authorization") String token) {
        // JWT是无状态的,登出一般在前端删除Token即可
        // 如果需要服务端控制,可以将Token加入黑名单(存Redis)
        return Result.success("退出成功");
    }
}

四、Token的传递方式

4.1 方式一:HTTP Header(最常用)✅

/**
 * 前端传递方式(JavaScript)
 */
// 方式1:在请求头中添加Authorization字段
fetch('/api/user/info', {
    method: 'GET',
    headers: {
        'Authorization': 'Bearer ' + token  // Bearer是标准前缀
    }
})

// 方式2:自定义Header名称
fetch('/api/user/info', {
    method: 'GET',
    headers: {
        'X-Access-Token': token,
        'token': token  // 或者直接叫token
    }
})

/**
 * 后端接收方式(Java)
 */
@RestController
public class UserController {
    
    // 方式1:通过@RequestHeader注解获取
    @GetMapping("/user/info")
    public Result getUserInfo(@RequestHeader("Authorization") String authHeader) {
        // 去除"Bearer "前缀
        String token = authHeader.replace("Bearer ", "");
        Long userId = TokenUtils.getUserIdFromToken(token);
        User user = userService.getById(userId);
        return Result.success(user);
    }
    
    // 方式2:通过HttpServletRequest获取
    @GetMapping("/user/profile")
    public Result getUserProfile(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        // 处理Token...
        return Result.success();
    }
}

4.2 方式二:Cookie

/**
 * 后端设置Cookie
 */
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request, HttpServletResponse response) {
    // 验证用户...
    String token = TokenUtils.generateToken(user.getId(), user.getUsername());
    
    // 将Token写入Cookie
    Cookie cookie = new Cookie("token", token);
    cookie.setHttpOnly(true);      // 防止XSS攻击
    cookie.setSecure(true);        // 仅HTTPS传输
    cookie.setPath("/");           // Cookie作用路径
    cookie.setMaxAge(7 * 24 * 60 * 60); // 7天有效期
    response.addCookie(cookie);
    
    return Result.success("登录成功");
}

/**
 * 后端从Cookie读取
 */
@GetMapping("/user/info")
public Result getUserInfo(@CookieValue("token") String token) {
    Long userId = TokenUtils.getUserIdFromToken(token);
    // 处理业务...
    return Result.success();
}

4.3 方式三:URL参数(不推荐)

/**
 * 通过URL传递Token(安全性差,不推荐)
 */
// 前端请求
fetch('/api/user/info?token=xxx')

// 后端接收
@GetMapping("/user/info")
public Result getUserInfo(@RequestParam("token") String token) {
    // 处理Token...
    return Result.success();
}

// ⚠️ 不推荐的原因:
// 1. Token会出现在浏览器历史记录中
// 2. Token会出现在服务器日志中
// 3. 容易被他人看到

4.4 方式四:Request Body(POST/PUT)

/**
 * 在请求体中传递Token
 */
// 前端
fetch('/api/user/update', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        token: token,
        username: 'newName'
    })
})

// 后端
@PostMapping("/user/update")
public Result updateUser(@RequestBody UpdateRequest request) {
    String token = request.getToken();
    // 处理Token...
    return Result.success();
}

五、Token拦截器(统一验证)

5.1 创建Token拦截器

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Token认证拦截器
 */
@Component
public class TokenInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        
        // 1. 从请求头获取Token
        String token = request.getHeader("Authorization");
        
        // 2. 判断Token是否存在
        if (token == null || token.isEmpty()) {
            response.setStatus(401);
            response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}");
            return false;
        }
        
        // 3. 去除Bearer前缀
        if (token.startsWith("Bearer ")) {
            token = token.substring(7);
        }
        
        // 4. 验证Token
        if (!TokenUtils.validateToken(token)) {
            response.setStatus(401);
            response.getWriter().write("{\"code\":401,\"message\":\"Token无效或已过期\"}");
            return false;
        }
        
        // 5. 从Token中获取用户信息,存入request中
        Long userId = TokenUtils.getUserIdFromToken(token);
        request.setAttribute("userId", userId);
        
        // 6. 放行
        return true;
    }
}

5.2 注册拦截器

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Web配置
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Resource
    private TokenInterceptor tokenInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenInterceptor)
                // 拦截所有请求
                .addPathPatterns("/**")
                // 排除不需要认证的路径
                .excludePathPatterns(
                    "/auth/login",      // 登录接口
                    "/auth/register",   // 注册接口
                    "/public/**",       // 公开接口
                    "/swagger-ui/**",   // Swagger文档
                    "/doc.html"         // 接口文档
                );
    }
}

六、Spring Security + Token(更专业)

6.1 Token过滤器

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Token认证过滤器(Spring Security版本)
 */
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                  HttpServletResponse response,
                                  FilterChain filterChain) throws ServletException, IOException {
        
        // 1. 获取Token
        String token = request.getHeader("Authorization");
        
        // 2. 验证Token
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            
            // 3. 解析Token,获取用户信息
            if (TokenUtils.validateToken(token)) {
                Long userId = TokenUtils.getUserIdFromToken(token);
                
                // 4. 构建认证对象
                // 这里简化处理,实际应该查询用户权限
                UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(
                        userId,      // principal:用户标识
                        null,        // credentials:凭证(密码)
                        null         // authorities:权限列表
                    );
                
                // 5. 将认证信息放入Security上下文
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        
        // 6. 继续过滤链
        filterChain.doFilter(request, response);
    }
}

6.2 在Controller中获取当前用户

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

@RestController
public class UserController {
    
    /**
     * 方式1:从SecurityContext获取
     */
    @GetMapping("/user/info")
    public Result getUserInfo() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Long userId = (Long) authentication.getPrincipal();
        User user = userService.getById(userId);
        return Result.success(user);
    }
    
    /**
     * 方式2:通过@AuthenticationPrincipal注解
     */
    @GetMapping("/user/profile")
    public Result getUserProfile(@AuthenticationPrincipal Long userId) {
        User user = userService.getById(userId);
        return Result.success(user);
    }
    
    /**
     * 方式3:自定义注解获取当前用户
     */
    @GetMapping("/user/settings")
    public Result getUserSettings(@CurrentUser Long userId) {
        // @CurrentUser是自定义注解,配合ArgumentResolver使用
        return Result.success();
    }
}

七、Token的最佳实践

7.1 Token刷新机制

/**
 * Token刷新策略
 */
public class TokenRefreshStrategy {
    
    /**
     * 双Token机制:Access Token + Refresh Token
     */
    public static class TokenPair {
        private String accessToken;   // 短期有效(2小时)
        private String refreshToken;  // 长期有效(7天)
        
        // getters and setters...
    }
    
    /**
     * 登录时生成双Token
     */
    public TokenPair login(String username, String password) {
        // 验证用户...
        
        // 生成Access Token(2小时)
        String accessToken = TokenUtils.generateToken(userId, username, 2 * 60 * 60 * 1000);
        
        // 生成Refresh Token(7天)
        String refreshToken = TokenUtils.generateToken(userId, username, 7 * 24 * 60 * 60 * 1000);
        
        // 将Refresh Token存入Redis,方便管理
        redisTemplate.opsForValue().set(
            "refresh_token:" + userId, 
            refreshToken, 
            7, 
            TimeUnit.DAYS
        );
        
        return new TokenPair(accessToken, refreshToken);
    }
    
    /**
     * 刷新Access Token
     */
    @PostMapping("/auth/refresh")
    public Result refreshToken(@RequestBody RefreshTokenRequest request) {
        String refreshToken = request.getRefreshToken();
        
        // 1. 验证Refresh Token
        if (!TokenUtils.validateToken(refreshToken)) {
            return Result.error("Refresh Token无效");
        }
        
        // 2. 从Redis验证
        Long userId = TokenUtils.getUserIdFromToken(refreshToken);
        String storedToken = redisTemplate.opsForValue().get("refresh_token:" + userId);
        if (!refreshToken.equals(storedToken)) {
            return Result.error("Refresh Token不匹配");
        }
        
        // 3. 生成新的Access Token
        String newAccessToken = TokenUtils.generateToken(userId, username, 2 * 60 * 60 * 1000);
        
        return Result.success(newAccessToken);
    }
}

7.2 Token安全建议

/**
 * Token安全最佳实践
 */
public class TokenSecurityBestPractices {
    
    /**
     * 1. Token存储位置(前端)
     */
    // ✅ 推荐:存储在内存中(React State、Vue Data)
    const token = useState('');
    
    // ⚠️ 谨慎:localStorage(容易受XSS攻击)
    localStorage.setItem('token', token);
    
    // ✅ 可选:httpOnly Cookie(防XSS,但要防CSRF)
    // 服务端设置httpOnly的Cookie
    
    
    /**
     * 2. Token传输安全
     */
    // ✅ 必须使用HTTPS
    // ✅ 添加请求签名
    // ✅ 限制Token有效期
    
    
    /**
     * 3. Token黑名单(登出场景)
     */
    @PostMapping("/auth/logout")
    public Result logout(@RequestHeader("Authorization") String token) {
        // 将Token加入黑名单(Redis)
        Long userId = TokenUtils.getUserIdFromToken(token);
        redisTemplate.opsForSet().add("token_blacklist", token);
        redisTemplate.expire("token_blacklist", 7, TimeUnit.DAYS);
        return Result.success();
    }
    
    /**
     * 4. Token续期策略
     */
    // 方式1:自动续期(Token快过期时自动刷新)
    // 方式2:双Token机制(Access Token + Refresh Token)
    // 方式3:滑动过期(每次请求都延长Token有效期)
    
    
    /**
     * 5. Token防重放攻击
     */
    // 添加nonce(随机数)+ timestamp(时间戳)
    String nonce = UUID.randomUUID().toString();
    Long timestamp = System.currentTimeMillis();
}

八、完整示例:Token全流程

一、整体流程图

┌─────────────────────────────────────────────────────────────────┐
│                      Token身份验证流程                             │
└─────────────────────────────────────────────────────────────────┘

┌─────────┐          ┌─────────┐          ┌─────────┐
│  客户端  │          │  后端    │          │  数据库  │
└────┬────┘          └────┬────┘          └────┬────┘
     │                    │                    │
     │ 1. 登录请求         │                    │
     │ POST /auth/login   │                    │
     │ {username,password}│                    │
     ├───────────────────>│                    │
     │                    │ 2. 验证用户信息      │
     │                    ├───────────────────>│
     │                    │ 3. 返回用户数据      │
     │                    │<───────────────────┤
     │                    │                    │
     │                    │ 4. 生成Token        │
     │                    │ (JWT签名)          │
     │ 5. 返回Token        │                    │
     │<───────────────────┤                    │
     │                    │                    │
     │ 6. 存储Token        │                    │
     │ (localStorage)     │                    │
     │                    │                    │
     │ 7. 业务请求         │                    │
     │ GET /api/user/info │                    │
     │ Header: Authorization: Bearer {token}   │
     ├───────────────────>│                    │
     │                    │ 8. 验证Token        │
     │                    │ (解析+验证签名)     │
     │                    │                    │
     │                    │ 9. 查询数据         │
     │                    ├───────────────────>│
     │                    │ 10. 返回数据        │
     │                    │<───────────────────┤
     │ 11. 返回业务数据    │                    │
     │<───────────────────┤                    │
     │                    │                    │

二、数据库设计

-- 用户表
CREATE TABLE `sys_user` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
    `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
    `password` VARCHAR(100) NOT NULL COMMENT '密码(加密)',
    `nickname` VARCHAR(50) COMMENT '昵称',
    `email` VARCHAR(100) COMMENT '邮箱',
    `phone` VARCHAR(20) COMMENT '手机号',
    `status` TINYINT DEFAULT 1 COMMENT '状态(1:正常 0:禁用)',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) COMMENT '用户表';

-- Token黑名单表(可选,用于登出)
CREATE TABLE `token_blacklist` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `token` VARCHAR(500) NOT NULL COMMENT 'Token值',
    `user_id` BIGINT NOT NULL COMMENT '用户ID',
    `expire_time` DATETIME NOT NULL COMMENT '过期时间',
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    INDEX idx_token(token(255)),
    INDEX idx_user_id(user_id)
) COMMENT 'Token黑名单';

三、核心实体类

package com.example.auth.entity;

import lombok.Data;
import java.time.LocalDateTime;

/**
 * 用户实体类
 */
@Data
public class User {
    private Long id;
    private String username;
    private String password;
    private String nickname;
    private String email;
    private String phone;
    private Integer status;  // 1:正常 0:禁用
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

/**
 * 登录请求DTO
 */
@Data
public class LoginRequest {
    private String username;
    private String password;
}

/**
 * 登录响应DTO
 */
@Data
public class LoginResponse {
    private String token;           // 访问令牌
    private String refreshToken;    // 刷新令牌
    private UserInfo userInfo;      // 用户信息
    
    @Data
    public static class UserInfo {
        private Long id;
        private String username;
        private String nickname;
        private String email;
    }
}

/**
 * 统一响应结果
 */
@Data
public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("success");
        result.setData(data);
        return result;
    }
    
    public static <T> Result<T> error(String message) {
        Result<T> result = new Result<>();
        result.setCode(500);
        result.setMessage(message);
        return result;
    }
    
    public static <T> Result<T> error(Integer code, String message) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
}

四、Token工具类

package com.example.auth.utils;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JWT Token 工具类
 */
@Slf4j
@Component
public class JwtTokenUtils {
    
    // 从配置文件读取密钥
    @Value("${jwt.secret:mySecretKeyForJWT1234567890123456789012}")
    private String secret;
    
    // Access Token 过期时间(2小时)
    @Value("${jwt.access-token-expiration:7200000}")
    private Long accessTokenExpiration;
    
    // Refresh Token 过期时间(7天)
    @Value("${jwt.refresh-token-expiration:604800000}")
    private Long refreshTokenExpiration;
    
    /**
     * 生成 Access Token
     */
    public String generateAccessToken(Long userId, String username) {
        return generateToken(userId, username, accessTokenExpiration, "access");
    }
    
    /**
     * 生成 Refresh Token
     */
    public String generateRefreshToken(Long userId, String username) {
        return generateToken(userId, username, refreshTokenExpiration, "refresh");
    }
    
    /**
     * 生成 Token 的核心方法
     */
    private String generateToken(Long userId, String username, Long expiration, String tokenType) {
        Date now = new Date();
        Date expirationDate = new Date(now.getTime() + expiration);
        
        // 构建Claims(载荷数据)
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);
        claims.put("username", username);
        claims.put("tokenType", tokenType);
        claims.put("createTime", now.getTime());
        
        // 获取密钥
        SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
        
        // 生成JWT
        return Jwts.builder()
                .setClaims(claims)                          // 自定义载荷
                .setSubject(username)                       // 主题(一般是用户标识)
                .setIssuedAt(now)                          // 签发时间
                .setExpiration(expirationDate)             // 过期时间
                .signWith(key, SignatureAlgorithm.HS256)  // 签名算法和密钥
                .compact();
    }
    
    /**
     * 解析 Token
     */
    public Claims parseToken(String token) {
        try {
            SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
            return Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            log.error("Token已过期: {}", e.getMessage());
            throw new RuntimeException("Token已过期");
        } catch (JwtException e) {
            log.error("Token解析失败: {}", e.getMessage());
            throw new RuntimeException("Token无效");
        }
    }
    
    /**
     * 验证 Token 是否有效
     */
    public boolean validateToken(String token) {
        try {
            parseToken(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
    
    /**
     * 从 Token 中获取用户ID
     */
    public Long getUserIdFromToken(String token) {
        Claims claims = parseToken(token);
        return Long.valueOf(claims.get("userId").toString());
    }
    
    /**
     * 从 Token 中获取用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = parseToken(token);
        return claims.get("username").toString();
    }
    
    /**
     * 判断 Token 是否即将过期(剩余时间少于30分钟)
     */
    public boolean isTokenExpiringSoon(String token) {
        try {
            Claims claims = parseToken(token);
            Date expiration = claims.getExpiration();
            long remainingTime = expiration.getTime() - System.currentTimeMillis();
            return remainingTime < 30 * 60 * 1000; // 30分钟
        } catch (Exception e) {
            return true;
        }
    }
}

五、用户Service层

package com.example.auth.service;

import com.example.auth.entity.User;
import com.example.auth.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * 用户服务类
 */
@Slf4j
@Service
public class UserService {
    
    @Resource
    private UserMapper userMapper;
    
    @Resource
    private BCryptPasswordEncoder passwordEncoder;
    
    /**
     * 验证用户登录
     */
    public User validateUser(String username, String password) {
        // 1. 根据用户名查询用户
        User user = userMapper.findByUsername(username);
        if (user == null) {
            log.warn("用户不存在: {}", username);
            return null;
        }
        
        // 2. 检查用户状态
        if (user.getStatus() == 0) {
            log.warn("用户已被禁用: {}", username);
            throw new RuntimeException("用户已被禁用");
        }
        
        // 3. 验证密码
        if (!passwordEncoder.matches(password, user.getPassword())) {
            log.warn("密码错误: {}", username);
            return null;
        }
        
        return user;
    }
    
    /**
     * 根据ID查询用户
     */
    public User getById(Long userId) {
        return userMapper.selectById(userId);
    }
    
    /**
     * 根据用户名查询用户
     */
    public User getByUsername(String username) {
        return userMapper.findByUsername(username);
    }
}

六、认证Controller

package com.example.auth.controller;

import com.example.auth.entity.*;
import com.example.auth.service.UserService;
import com.example.auth.utils.JwtTokenUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

/**
 * 认证控制器
 */
@Slf4j
@RestController
@RequestMapping("/auth")
public class AuthController {
    
    @Resource
    private UserService userService;
    
    @Resource
    private JwtTokenUtils jwtTokenUtils;
    
    @Resource
    private StringRedisTemplate redisTemplate;
    
    /**
     * 步骤1: 用户登录
     */
    @PostMapping("/login")
    public Result<LoginResponse> login(@RequestBody LoginRequest request) {
        log.info("用户登录: {}", request.getUsername());
        
        try {
            // 1. 验证用户名和密码
            User user = userService.validateUser(request.getUsername(), request.getPassword());
            if (user == null) {
                return Result.error(401, "用户名或密码错误");
            }
            
            // 2. 生成 Access Token
            String accessToken = jwtTokenUtils.generateAccessToken(user.getId(), user.getUsername());
            
            // 3. 生成 Refresh Token
            String refreshToken = jwtTokenUtils.generateRefreshToken(user.getId(), user.getUsername());
            
            // 4. 将 Refresh Token 存入 Redis(用于后续刷新和登出控制)
            String refreshTokenKey = "refresh_token:" + user.getId();
            redisTemplate.opsForValue().set(
                refreshTokenKey, 
                refreshToken, 
                7, 
                TimeUnit.DAYS
            );
            
            // 5. 构建响应数据
            LoginResponse response = new LoginResponse();
            response.setToken(accessToken);
            response.setRefreshToken(refreshToken);
            
            // 用户信息(不包含密码)
            LoginResponse.UserInfo userInfo = new LoginResponse.UserInfo();
            userInfo.setId(user.getId());
            userInfo.setUsername(user.getUsername());
            userInfo.setNickname(user.getNickname());
            userInfo.setEmail(user.getEmail());
            response.setUserInfo(userInfo);
            
            log.info("用户登录成功: {}", user.getUsername());
            return Result.success(response);
            
        } catch (Exception e) {
            log.error("登录失败", e);
            return Result.error("登录失败: " + e.getMessage());
        }
    }
    
    /**
     * 步骤2: 刷新 Token
     */
    @PostMapping("/refresh")
    public Result<String> refreshToken(@RequestBody RefreshTokenRequest request) {
        String refreshToken = request.getRefreshToken();
        
        try {
            // 1. 验证 Refresh Token 的有效性
            if (!jwtTokenUtils.validateToken(refreshToken)) {
                return Result.error(401, "Refresh Token无效");
            }
            
            // 2. 从 Token 中获取用户信息
            Long userId = jwtTokenUtils.getUserIdFromToken(refreshToken);
            String username = jwtTokenUtils.getUsernameFromToken(refreshToken);
            
            // 3. 从 Redis 中验证 Refresh Token
            String storedToken = redisTemplate.opsForValue().get("refresh_token:" + userId);
            if (storedToken == null || !storedToken.equals(refreshToken)) {
                return Result.error(401, "Refresh Token已失效");
            }
            
            // 4. 生成新的 Access Token
            String newAccessToken = jwtTokenUtils.generateAccessToken(userId, username);
            
            log.info("Token刷新成功: userId={}", userId);
            return Result.success(newAccessToken);
            
        } catch (Exception e) {
            log.error("Token刷新失败", e);
            return Result.error("Token刷新失败");
        }
    }
    
    /**
     * 步骤3: 用户登出
     */
    @PostMapping("/logout")
    public Result<Void> logout(HttpServletRequest request) {
        try {
            // 1. 从请求头获取 Token
            String token = request.getHeader("Authorization");
            if (token != null && token.startsWith("Bearer ")) {
                token = token.substring(7);
            }
            
            // 2. 解析 Token 获取用户ID
            Long userId = jwtTokenUtils.getUserIdFromToken(token);
            
            // 3. 将 Token 加入黑名单(Redis)
            String blacklistKey = "token_blacklist:" + token;
            redisTemplate.opsForValue().set(blacklistKey, "1", 2, TimeUnit.HOURS);
            
            // 4. 删除用户的 Refresh Token
            redisTemplate.delete("refresh_token:" + userId);
            
            log.info("用户登出成功: userId={}", userId);
            return Result.success(null);
            
        } catch (Exception e) {
            log.error("登出失败", e);
            return Result.error("登出失败");
        }
    }
    
    /**
     * 验证 Token 是否有效(可选接口)
     */
    @GetMapping("/validate")
    public Result<Boolean> validateToken(@RequestParam String token) {
        // 1. 检查是否在黑名单中
        if (Boolean.TRUE.equals(redisTemplate.hasKey("token_blacklist:" + token))) {
            return Result.success(false);
        }
        
        // 2. 验证 Token 本身
        boolean valid = jwtTokenUtils.validateToken(token);
        return Result.success(valid);
    }
}

/**
 * 刷新Token请求DTO
 */
@Data
class RefreshTokenRequest {
    private String refreshToken;
}

七、Token过滤器(核心验证逻辑)

package com.example.auth.filter;

import com.example.auth.utils.JwtTokenUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * Token 认证过滤器
 * 在每个请求到达Controller之前执行
 */
@Slf4j
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    
    @Resource
    private JwtTokenUtils jwtTokenUtils;
    
    @Resource
    private StringRedisTemplate redisTemplate;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                  HttpServletResponse response,
                                  FilterChain filterChain) throws ServletException, IOException {
        
        // 1. 排除不需要认证的路径
        String path = request.getRequestURI();
        if (isExcludePath(path)) {
            filterChain.doFilter(request, response);
            return;
        }
        
        try {
            // 2. 从请求头获取 Token
            String token = getTokenFromRequest(request);
            
            // 3. Token 为空,返回未登录
            if (token == null || token.isEmpty()) {
                sendErrorResponse(response, 401, "未登录,请先登录");
                return;
            }
            
            // 4. 检查 Token 是否在黑名单中(已登出)
            if (Boolean.TRUE.equals(redisTemplate.hasKey("token_blacklist:" + token))) {
                sendErrorResponse(response, 401, "Token已失效,请重新登录");
                return;
            }
            
            // 5. 验证 Token 有效性
            if (!jwtTokenUtils.validateToken(token)) {
                sendErrorResponse(response, 401, "Token无效或已过期");
                return;
            }
            
            // 6. 从 Token 中提取用户信息
            Long userId = jwtTokenUtils.getUserIdFromToken(token);
            String username = jwtTokenUtils.getUsernameFromToken(token);
            
            // 7. 将用户信息存入 Request,供后续使用
            request.setAttribute("userId", userId);
            request.setAttribute("username", username);
            request.setAttribute("token", token);
            
            // 8. 记录日志
            log.debug("Token验证成功: userId={}, username={}, path={}", userId, username, path);
            
            // 9. 继续执行后续过滤器和Controller
            filterChain.doFilter(request, response);
            
        } catch (Exception e) {
            log.error("Token验证失败", e);
            sendErrorResponse(response, 401, "认证失败: " + e.getMessage());
        }
    }
    
    /**
     * 从请求中获取 Token
     */
    private String getTokenFromRequest(HttpServletRequest request) {
        // 方式1: 从 Header 中获取(推荐)
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        
        // 方式2: 从 Query 参数获取(不推荐,但支持)
        String tokenParam = request.getParameter("token");
        if (tokenParam != null) {
            return tokenParam;
        }
        
        return null;
    }
    
    /**
     * 判断是否是排除路径(不需要Token验证)
     */
    private boolean isExcludePath(String path) {
        String[] excludePaths = {
            "/auth/login",      // 登录
            "/auth/register",   // 注册
            "/auth/refresh",    // 刷新Token
            "/public",          // 公开接口
            "/swagger-ui",      // Swagger文档
            "/v3/api-docs",     // API文档
            "/error"            // 错误页面
        };
        
        for (String excludePath : excludePaths) {
            if (path.startsWith(excludePath)) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * 发送错误响应
     */
    private void sendErrorResponse(HttpServletResponse response, int code, String message) throws IOException {
        response.setStatus(code);
        response.setContentType("application/json;charset=UTF-8");
        
        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("code", code);
        errorResponse.put("message", message);
        errorResponse.put("data", null);
        
        ObjectMapper objectMapper = new ObjectMapper();
        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
    }
}

八、注册过滤器

package com.example.auth.config;

import com.example.auth.filter.TokenAuthenticationFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * 过滤器配置
 */
@Configuration
public class FilterConfig {
    
    @Resource
    private TokenAuthenticationFilter tokenAuthenticationFilter;
    
    /**
     * 注册 Token 认证过滤器
     */
    @Bean
    public FilterRegistrationBean<TokenAuthenticationFilter> tokenFilterRegistration() {
        FilterRegistrationBean<TokenAuthenticationFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(tokenAuthenticationFilter);
        registration.addUrlPatterns("/*");  // 拦截所有请求
        registration.setOrder(1);           // 过滤器顺序
        return registration;
    }
}

九、业务Controller(使用Token)

package com.example.auth.controller;

import com.example.auth.entity.Result;
import com.example.auth.entity.User;
import com.example.auth.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

/**
 * 用户业务Controller
 * 所有接口都需要Token认证
 */
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserController {
    
    @Resource
    private UserService userService;
    
    /**
     * 获取当前用户信息
     */
    @GetMapping("/info")
    public Result<User> getUserInfo(HttpServletRequest request) {
        // 从 Request 中获取用户ID(由过滤器设置)
        Long userId = (Long) request.getAttribute("userId");
        
        // 查询用户信息
        User user = userService.getById(userId);
        user.setPassword(null);  // 不返回密码
        
        return Result.success(user);
    }
    
    /**
     * 更新用户信息
     */
    @PutMapping("/update")
    public Result<Void> updateUser(@RequestBody User user, HttpServletRequest request) {
        Long userId = (Long) request.getAttribute("userId");
        
        // 只能更新自己的信息
        if (!userId.equals(user.getId())) {
            return Result.error("只能更新自己的信息");
        }
        
        // 更新逻辑...
        return Result.success(null);
    }
    
    /**
     * 修改密码
     */
    @PostMapping("/change-password")
    public Result<Void> changePassword(@RequestBody ChangePasswordRequest request, 
                                      HttpServletRequest httpRequest) {
        Long userId = (Long) httpRequest.getAttribute("userId");
        
        // 修改密码逻辑...
        return Result.success(null);
    }
}

@Data
class ChangePasswordRequest {
    private String oldPassword;
    private String newPassword;
}

十、前端完整示例(Vue3 + Axios)

// api/auth.js - 认证相关API
import axios from 'axios'

/**
 * 步骤1: 用户登录
 */
export function login(username, password) {
  return axios.post('/auth/login', {
    username,
    password
  })
}

/**
 * 步骤2: 刷新Token
 */
export function refreshToken(refreshToken) {
  return axios.post('/auth/refresh', {
    refreshToken
  })
}

/**
 * 步骤3: 用户登出
 */
export function logout() {
  return axios.post('/auth/logout')
}

/**
 * 获取用户信息
 */
export function getUserInfo() {
  return axios.get('/api/user/info')
}
// utils/request.js - Axios配置
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'

// 创建axios实例
const service = axios.create({
  baseURL: 'http://localhost:8080', // API基础URL
  timeout: 10000 // 请求超时时间
})

// ============================================
// 请求拦截器:自动添加Token
// ============================================
service.interceptors.request.use(
  config => {
    // 从localStorage获取Token
    const token = localStorage.getItem('access_token')
    
    if (token) {
      // 在请求头中添加Token
      config.headers['Authorization'] = `Bearer ${token}`
    }
    
    return config
  },
  error => {
    console.error('请求错误', error)
    return Promise.reject(error)
  }
)

// ============================================
// 响应拦截器:处理Token过期
// ============================================
service.interceptors.response.use(
  response => {
    const res = response.data
    
    // 根据后端返回的code判断
    if (res.code !== 200) {
      ElMessage.error(res.message || '请求失败')
      return Promise.reject(new Error(res.message || 'Error'))
    }
    
    return res
  },
  async error => {
    const status = error.response?.status
    
    // Token过期或无效
    if (status === 401) {
      // 尝试使用Refresh Token刷新
      const refreshToken = localStorage.getItem('refresh_token')
      
      if (refreshToken) {
        try {
          // 调用刷新Token接口
          const res = await axios.post('/auth/refresh', {
            refreshToken
          })
          
          // 保存新的Access Token
          const newAccessToken = res.data.data
          localStorage.setItem('access_token', newAccessToken)
          
          // 重新发起原来的请求
          error.config.headers['Authorization'] = `Bearer ${newAccessToken}`
          return axios.request(error.config)
          
        } catch (refreshError) {
          // Refresh Token也失效了,跳转登录页
          ElMessage.error('登录已过期,请重新登录')
          localStorage.removeItem('access_token')
          localStorage.removeItem('refresh_token')
          router.push('/login')
          return Promise.reject(refreshError)
        }
      } else {
        // 没有Refresh Token,直接跳转登录页
        ElMessage.error('请先登录')
        router.push('/login')
      }
    }
    
    return Promise.reject(error)
  }
)

export default service
<!-- views/Login.vue - 登录页面 -->
<template>
  <div class="login-container">
    <el-form :model="loginForm" :rules="rules" ref="loginFormRef">
      <h2>用户登录</h2>
      
      <el-form-item prop="username">
        <el-input 
          v-model="loginForm.username" 
          placeholder="请输入用户名"
          prefix-icon="User"
        />
      </el-form-item>
      
      <el-form-item prop="password">
        <el-input 
          v-model="loginForm.password" 
          type="password"
          placeholder="请输入密码"
          prefix-icon="Lock"
          @keyup.enter="handleLogin"
        />
      </el-form-item>
      
      <el-button 
        type="primary" 
        @click="handleLogin" 
        :loading="loading"
        style="width: 100%"
      >
        登录
      </el-button>
    </el-form>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { login, getUserInfo } from '@/api/auth'

const router = useRouter()
const loading = ref(false)
const loginFormRef = ref(null)

// 表单数据
const loginForm = reactive({
  username: '',
  password: ''
})

// 验证规则
const rules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { min: 6, message: '密码长度至少6位', trigger: 'blur' }
  ]
}

/**
 * 登录处理
 */
const handleLogin = async () => {
  // 表单验证
  const valid = await loginFormRef.value.validate()
  if (!valid) return
  
  loading.value = true
  
  try {
    // 调用登录接口
    const res = await login(loginForm.username, loginForm.password)
    
    // 保存Token到localStorage
    localStorage.setItem('access_token', res.data.token)
    localStorage.setItem('refresh_token', res.data.refreshToken)
    
    // 保存用户信息
    localStorage.setItem('userInfo', JSON.stringify(res.data.userInfo))
    
    ElMessage.success('登录成功')
    
    // 跳转到首页
    router.push('/')
    
  } catch (error) {
    ElMessage.error(error.message || '登录失败')
  } finally {
    loading.value = false
  }
}
</script>

<style scoped>
.login-container {
  width: 400px;
  margin: 100px auto;
  padding: 40px;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
</style>
<!-- views/Home.vue - 首页 -->
<template>
  <div class="home">
    <div class="user-info">
      <h2>欢迎,{{ userInfo.nickname || userInfo.username }}</h2>
      <p>用户ID: {{ userInfo.id }}</p>
      <p>邮箱: {{ userInfo.email }}</p>
      
      <el-button type="danger" @click="handleLogout">退出登录</el-button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getUserInfo, logout } from '@/api/auth'

const router = useRouter()
const userInfo = ref({})

/**
 * 加载用户信息
 */
const loadUserInfo = async () => {
  try {
    const res = await getUserInfo()
    userInfo.value = res.data
  } catch (error) {
    ElMessage.error('获取用户信息失败')
  }
}

/**
 * 退出登录
 */
const handleLogout = async () => {
  try {
    await logout()
    
    // 清除本地Token
    localStorage.removeItem('access_token')
    localStorage.removeItem('refresh_token')
    localStorage.removeItem('userInfo')
    
    ElMessage.success('退出成功')
    router.push('/login')
    
  } catch (error) {
    ElMessage.error('退出失败')
  }
}

onMounted(() => {
  loadUserInfo()
})
</script>

十一、配置文件

# application.yml
spring:
  # Redis配置
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    timeout: 5000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0

# JWT配置
jwt:
  secret: mySecretKeyForJWT1234567890123456789012  # 密钥(至少32字节)
  access-token-expiration: 7200000    # Access Token过期时间(2小时)
  refresh-token-expiration: 604800000  # Refresh Token过期时间(7天)

# 日志配置
logging:
  level:
    com.example.auth: DEBUG

十二、完整流程时序图

用户端                    前端                      后端                     Redis/DB
  │                        │                         │                         │
  │ 1.输入用户名密码         │                         │                         │
  │───────────────────────>│                         │                         │
  │                        │ 2.POST /auth/login      │                         │
  │                        │────────────────────────>│                         │
  │                        │                         │ 3.查询用户               │
  │                        │                         │────────────────────────>│
  │                        │                         │ 4.返回用户数据           │
  │                        │                         │<────────────────────────│
  │                        │                         │ 5.验证密码               │
  │                        │                         │ 6.生成Token              │
  │                        │ 7.返回Token & UserInfo  │                         │
  │                        │<────────────────────────│                         │
  │ 8.登录成功              │                         │ 9.存储RefreshToken       │
  │<───────────────────────│                         │────────────────────────>│
  │                        │ 10.保存Token到localStorage                         │
  │                        │                         │                         │
  │ 11.访问用户信息         │                         │                         │
  │───────────────────────>│                         │                         │
  │                        │ 12.GET /api/user/info   │                         │
  │                        │ Header: Authorization   │                         │
  │                        │────────────────────────>│                         │
  │                        │                         │ 13.TokenFilter拦截       │
  │                        │                         │ 14.验证Token             │
  │                        │                         │ 15.查询用户数据          │
  │                        │                         │────────────────────────>│
  │                        │                         │ 16.返回用户数据          │
  │                        │                         │<────────────────────────│
  │                        │ 17.返回用户信息          │                         │
  │                        │<────────────────────────│                         │
  │ 18.显示用户信息         │                         │                         │
  │<───────────────────────│                         │                         │

十三、总结

Token验证流程核心要点:
  1. 登录阶段:验证用户 → 生成Token → 返回Token
  2. 请求阶段:携带Token → 过滤器验证 → 提取用户信息 → 执行业务
  3. 刷新阶段:RefreshToken → 生成新AccessToken
  4. 登出阶段:Token加入黑名单 → 删除RefreshToken
🔐 安全建议:
  • 使用HTTPS传输
  • Token设置合理过期时间
  • 敏感操作二次验证
  • 实现Token刷新机制
  • 登出时清理Token

九、常见问题总结

/**
 * Q1: Token过期了怎么办?
 */
// A: 使用Refresh Token机制,或者前端自动刷新Token

/**
 * Q2: Token能被篡改吗?
 */
// A: JWT有签名机制,篡改后签名验证会失败

/**
 * Q3: Token能主动失效吗?
 */
// A: JWT本身不支持,需要配合Redis黑名单实现

/**
 * Q4: Token和Session的区别?
 */
// Token: 无状态,服务器不存储,天然支持分布式
// Session: 有状态,服务器存储,分布式需要Session共享

/**
 * Q5: Token应该设置多长有效期?
 */
// Access Token: 2小时 - 24小时
// Refresh Token: 7天 - 30天

十、依赖配置(Maven)

<!-- 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>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
</dependency>
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐