找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1180

积分

1

好友

161

主题
发表于 3 天前 | 查看: 7| 回复: 0

在Java后端、前端与测试面试中,“Token是什么?有什么用?怎么用?”是高频基础考题,尤其在登录、权限控制等场景下,Token是绕不开的核心组件。不少开发者仅知道“Token是登录后返回的一串字符串”,却难以阐明其本质、分类与工程实践细节。本文将从本质剖析、类型对比到实战编码,系统性地讲解Token。

一、Token的本质:不只是字符串

1. 核心定义

Token(令牌)本质上是一串经过加密的、具有唯一性的字符串,是服务器颁发给客户端的“数字身份凭证”。客户端在后续请求中携带此Token,服务器通过验证该Token即可确认客户端身份与权限,从而避免每次请求都校验用户名和密码。

2. 生活化类比:景区门票

  • 购票(登录):你(客户端)用身份证和现金(账号密码)在售票处(登录接口)兑换一张门票(Token)。
  • 入园(访问):之后进入景区内各个景点(业务接口),只需出示门票(Token),无需反复核验身份证。
  • 门票规则(Token属性):门票上印有有效期(Token过期时间)和允许进入的区域(权限范围),过期或无效的门票将被拒绝使用。

3. Token解决的核心痛点

与传统基于Session的“账号密码”鉴权相比,Token方案针对以下痛点提供了更优解:

痛点 Session方案 Token方案
跨域问题 依赖Cookie,跨域访问需复杂CORS配置及Cookie共享 可置于请求头或参数中,天然支持跨域
分布式部署 需要Session共享(如用Redis),增加了系统复杂度和维护成本 Token自包含信息,服务器无状态,轻松支持分布式集群
多端适配 移动端(APP、小程序)处理Cookie较为麻烦 Token可统一存放于请求头,适配Web、APP、小程序等所有终端

二、主流Token类型详解

1. 自定义Token

适用于小型项目或内部系统,实现简单灵活。

  • 特点:生成规则由开发者自定义(如用户ID+时间戳+随机数+签名),服务器需存储Token(如存入Redis)以验证其有效性。
  • 优点:完全可控,易于理解。
  • 缺点:服务器需维护Token存储,有状态。

Java生成自定义Token示例

// 生成自定义Token
public String generateToken(Long userId) {
    // 1. 拼接核心信息
    String baseStr = userId + "_" + System.currentTimeMillis() + "_" + RandomStringUtils.randomAlphanumeric(8);
    // 2. 加盐签名(防止篡改)
    String sign = DigestUtils.md5Hex(baseStr + "my_secret_key");
    // 3. 拼接最终Token
    String token = baseStr + "_" + sign;
    // 4. 存入Redis,设置2小时过期
    redisTemplate.opsForValue().set("token:" + token, userId, 2, TimeUnit.HOURS);
    return token;
}

// 验证自定义Token
public boolean verifyToken(String token) {
    // 1. 检查格式
    if (token == null || !token.contains("_")) {
        return false;
    }
    // 2. 拆分并验证签名
    String[] parts = token.split("_");
    String baseStr = String.join("_", Arrays.copyOf(parts, parts.length-1));
    String sign = parts[parts.length-1];
    String realSign = DigestUtils.md5Hex(baseStr + "my_secret_key");
    if (!sign.equals(realSign)) {
        return false;
    }
    // 3. 检查Redis中是否存在(未过期)
    return redisTemplate.hasKey("token:" + token);
}

2. JWT (JSON Web Token)

当前最主流的无状态令牌标准,适合中大型分布式项目。

  • 结构Header.Payload.Signature,三段之间用点(.)分隔。
    • Header:声明令牌类型和签名算法,如 {"alg":"HS256","typ":"JWT"},经Base64编码形成第一段。
    • Payload:携带声明(Claims),如用户ID、过期时间(exp)、签发时间(iat),经Base64编码形成第二段。注意:Payload仅编码,未加密,切勿存放敏感信息。
    • Signature:对前两段的签名,用于防篡改。例如使用HMACSHA256算法:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

示例JWT

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImV4cCI6MTcyODA3MjAwMCwiaWF0IjoxNzI4MDY4NDAwfQ.5Z8n7k8X9m0P1s2l3k4j5h6g7f8d9s0a

Java生成与验证JWT示例(使用jjwt库)

// 生成JWT
public String generateJwt(Long userId) {
    Date expireTime = new Date(System.currentTimeMillis() + 2 * 3600 * 1000); // 2小时后过期
    String jwt = Jwts.builder()
            .setSubject(userId.toString()) // 主题存放用户ID
            .setExpiration(expireTime)
            .setIssuedAt(new Date())
            .signWith(SignatureAlgorithm.HS256, "my_jwt_secret") // 使用密钥签名
            .compact();
    return jwt;
}

// 验证JWT
public Long verifyJwt(String jwt) {
    try {
        Claims claims = Jwts.parser()
                .setSigningKey("my_jwt_secret")
                .parseClaimsJws(jwt) // 会自动验证签名和过期时间
                .getBody();
        return Long.parseLong(claims.getSubject()); // 解析出用户ID
    } catch (Exception e) {
        // 签名无效或过期
        return null;
    }
}

3. OAuth 2.0 Token

专为第三方应用授权设计,如实现“微信登录”。

  • 核心令牌
    • access_token:用于访问受保护资源,有效期较短(如2小时)。
    • refresh_token:用于在access_token过期后获取新的access_token,有效期较长(如30天),避免用户反复授权。
  • 典型流程(以微信登录为例)
    1. 前端引导用户跳转至微信授权页。
    2. 用户授权后,微信重定向回应用并携带code
    3. 后端使用code、AppID和AppSecret调用微信接口,换取access_tokenopenid
    4. 后端根据openid处理业务逻辑(查询或创建用户),并生成自有的系统Token(如JWT)返回给前端。

三、Token实战:前后端完整交互流程

1. 整体流程(以JWT为例)

前端                         后端                         存储/第三方
  |                            |                            |
  | 1. 登录请求(账号密码)       |                            |
  |--------------------------->|                            |
  |                            | 2. 验证账号密码             |
  |                            | 3. 生成JWT Token           |
  | 4. 返回JWT Token          |                            |
  |<---------------------------|                            |
  | 5. 存储Token(localStorage) |                            |
  |                            |                            |
  | 6. 接口请求(携带Token)     |                            |
  |--------------------------->| 7. 验证Token有效性         |
  |                            | 8. 处理业务逻辑            |
  | 9. 返回接口数据           |                            |
  |<---------------------------|                            |
  | 10. Token过期             |                            |
  |--------------------------->| 11. 返回401错误           |
  |<---------------------------|                            |
  | 12. 跳转到登录页          |                            |

2. 前端携带Token的三种方式

// 方式1:放在请求头(推荐,安全规范)
axios.interceptors.request.use(config => {
    const token = localStorage.getItem('token');
    if (token) {
        config.headers['Authorization'] = 'Bearer ' + token; // Bearer是标准前缀
    }
    return config;
});

// 方式2:放在URL参数(不推荐,易泄露于日志或浏览器历史)
axios.get('/api/user?token=' + token);

// 方式3:放在请求体(适用于POST/PUT等请求)
axios.post('/api/user', {
    token: token,
    name: '张三'
});

3. 后端统一拦截验证(Spring Boot示例)

Spring Boot项目中,通常通过拦截器(Interceptor)统一处理Token验证。

定义Token拦截器

@Component
public class TokenInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 排除登录接口等无需认证的路径
        String requestURI = request.getRequestURI();
        if (requestURI.contains("/api/login")) {
            return true;
        }

        // 2. 从请求头获取Token
        String token = request.getHeader("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            response.setStatus(401);
            response.getWriter().write("Token不存在");
            return false;
        }
        token = token.substring(7); // 去除"Bearer "前缀

        // 3. 验证Token (以JWT为例)
        Long userId = verifyJwt(token);
        if (userId == null) {
            response.setStatus(401);
            response.getWriter().write("Token无效或已过期");
            return false;
        }

        // 4. 将用户ID存入请求属性,供后续Controller使用
        request.setAttribute("userId", userId);
        return true;
    }
}

注册拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private TokenInterceptor tokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/api/**")   // 拦截所有/api开头的请求
                .excludePathPatterns("/api/login"); // 排除登录接口
    }
}

四、Token安全防护八大措施

Token作为身份凭证,其安全性至关重要,必须实施多层防护。

  1. 传输加密:所有涉及Token传输的接口(登录、业务)必须使用HTTPS,防止网络嗅探。
  2. 存储安全
    • 前端:优先使用localStoragesessionStorage,避免使用易受CSRF攻击的Cookie。
    • 后端:若需存储(如自定义Token),应在Redis等存储中为Key添加前缀(如token:),并设置合理的过期时间。
  3. 防篡改
    • 自定义Token必须加盐签名。
    • JWT必须使用足够强度的密钥进行签名。
    • 切勿在Token的Payload中存储明文密码、手机号等敏感信息。
  4. 防御XSS:对用户输入进行严格过滤和转义,防止恶意脚本窃取localStorage中的Token。
  5. 防御CSRF:验证请求头中的OriginReferer;对于敏感操作(如支付、改密),应增加二次验证(验证码、短信)。
  6. 缩短有效期:根据业务敏感度设置合理的过期时间(如普通业务2小时,支付操作15分钟)。提供“记住我”功能时,可使用refresh_token机制。
  7. 支持主动注销
    • 自定义Token:退出登录时直接从存储中删除。
    • JWT:可结合Redis维护一个短期的“黑名单”,注销时将JWT标识加入黑名单,验证时优先检查。
  8. 限流防护:对登录、Token验证等接口实施限流(如每分钟10次),防止暴力破解。

五、面试高频真题解析

真题1:Token 和 Session 的区别是什么?

核心区别在于“服务器是否有状态”

  • 存储位置:Session数据存储在服务器端(内存/Redis);Token由客户端存储和携带。
  • 扩展性:Session在分布式环境中需要共享,增加复杂度;Token无状态,天然支持分布式。
  • 跨域支持:Session基于Cookie,跨域限制多;Token可灵活置于请求头,跨域友好。
  • 安全性:Session易受CSRF攻击;Token需重点防范XSS窃取和传输泄露。

真题2:JWT的优缺点是什么?如何解决JWT无法主动作废的问题?

  • 优点:无状态、跨域/分布式友好、自包含信息、签发后无需存储。
  • 缺点:一旦签发,在有效期内无法主动作废;Payload可解码,不能存敏感信息;体积相对较大。
  • 解决作废问题
    1. 维护黑名单:将需要作废的JWT ID(jti)或整个签名存入Redis黑名单,验证时检查。
    2. 缩短有效期+刷新机制:使用短效access_token配合长效refresh_token
    3. 引入版本号:在Payload中加入用户信息版本号,用户关键信息变更时更新版本号,验证时比对。

真题3:Token的安全防护措施有哪些?

(参考答案可整合第四节内容,结构化列出)核心包括:HTTPS传输、安全存储(前端LocalStorage/后端Redis加过期)、添加签名防篡改、防御XSS/CSRF攻击、设置合理有效期、支持主动注销机制、对接口进行限流。

六、总结

  1. 本质:Token是服务端颁发的数字身份凭证,用以解决Session在跨域、分布式场景下的局限。
  2. 选型:小型项目可用自定义Token;中大型分布式项目推荐JWT;第三方授权集成需采用OAuth 2.0。
  3. 实践:前端妥善存储并规范携带Token;后端通过拦截器统一验证;安全防护需贯穿传输、存储、验证全流程。
  4. 安全:始终将安全性置于首位,综合运用加密、签名、防攻击、限流等多种手段构建防护体系。

深入理解Token的原理与实战,不仅能从容应对技术面试,更能为构建安全、可靠的现代应用鉴权体系打下坚实基础。




上一篇:TongWeb中间件安全审计实战:后台文件上传与任意文件下载漏洞复现
下一篇:朴素贝叶斯与逻辑回归深度对比:原理差异、应用场景与Python实战案例
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 20:45 , Processed in 0.134440 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表