当前位置: 首页 > article >正文

【Jwt】详解认证登录的数字签名

【Jwt】详解认证登录的数字签名

  • 【一】jwt概念
    • 【1】什么是jwt
    • 【2】jwt有什么用
    • 【3】jwt的组成结构
      • (1)Header:头信息
      • (2)PayLoad:用户信息
      • (3)Signature:签名
    • 【4】JWT的优点和缺点
      • (1)优点
      • (2)缺点:
  • 【二】使用jwt
    • 【0】引入依赖
    • 【1】生成token
    • 【2】解析token
    • 【3】验证逻辑
    • 【4】完整工具类
    • 【5】登录逻辑
  • 【三】登录问题
    • 【1】有无状态登录
      • (1)有状态登录
      • (2)无状态的登录
    • 【2】分布式项目登录问题
      • (1)解析token并向下游的微服务传递
      • (2)完整的登录和鉴权过程
      • (3)Token续签问题
        • (1)单token方案
        • (2)双token方案
          • (3)方案
      • (4)Token续期处理
      • (5)Token登出处理
    • 【4】如何获取客户端的各种信息

【一】jwt概念

【1】什么是jwt

Json Web Token是通过数字签名的方式,以json为载体,在不同的服务之间安全的传输信息的技术。

【2】jwt有什么用

一般使用在授权认证的过程中,一旦用户登录,后端返回一个token给前端,相当于后端给了前端返回了一个授权码,之后前端向后端发送的每一个请求都需要包含这个token,后端在执行方法前会校验这个token(安全校验),校验通过才执行具体的业务逻辑。

【3】jwt的组成结构

JWT由Header(头信息),PayLoad (用户信息),Signature(签名)三个部分组成
在这里插入图片描述

(1)Header:头信息

Header头信息主要声明加密算法:(具体算法对称不对称加密不作为研究内容)

通常直接使用 HMAC HS256这样的算法

{
  "typ":"jwt"
  "alg":"HS256" //加密算法
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分

(2)PayLoad:用户信息

指定了七个默认字段供选择

iss:发行人
exp:到期时间
sub:主体
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

也可以自定义私有字段,但是不建议在此存放密码之类的敏感信息,因为此部分可以解码还原出原始内容。虽然它可以解码,但是也不能修改这个内容。

{
  "username":"zhangsan",
  "name":"张三",
}

对其进行base64加密,得到Jwt的第二部分

eyJ1c2VybmFtZSI6InpoYW5nc2FuIiwibmFtZSI6IuW8oOS4iSIsImFnZSI6MTgsInNleCI6IuWlsyIsImV4cCI6MTY0NzE0NTA1MSwianRpIjoiMTIxMjEyMTIxMiJ9

(3)Signature:签名

此部分用于防止jwt内容被篡改。这个签证信息由三部分组成(由加密后的Header,加密后的PayLoad,加密后的签名三部分组成)

header (base64后的)
payload (base64后的)
secret

base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐加密,然后就构成了jwt的第三部分,每个部分直接使用"."来进行拼接

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InpoYW5nc2FuIiwibmFtZSI6IuW8oOS4iSIsImFnZSI6MTgsInNleCI6IuWlsyIsImV4cCI6MTY0NzE0NTA1MSwianRpIjoiMTIxMjEyMTIxMiJ9.5tmHCpcsS_VuZ2_z5Rydf2OpsviBGwB-fJE5aS7gKqE

【4】JWT的优点和缺点

基于session和基于jwt的方式的主要区别就是用户的状态保存的位置,session是保存在服务端的,而jwt是保存在客户端的。

(1)优点

1-jwt基于json,非常方便解析。
2-可以在令牌中自定义丰富的内容,易扩展。
3-资源服务使用JWT可不依赖认证服务即可完成授权。
4-通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。

(2)缺点:

1-JWT 中的信息可以在客户端解码,因此敏感信息不应该存储在 JWT 中,尤其是不加密的情况下。

2-如果使用对称加密算法并且密钥被泄漏,攻击者可以使用该密钥签发有效的 JWT。为了防止这种情况发生,应该使用安全的加密算法,并妥善保管密钥。

3-JWT 不支持会话管理,也不能主动使令牌失效。因此,在某些情况下,需要实现其他机制来管理用户会话和授权状态。

4-JWT 一旦签发,就无法撤回或修改,除非到了过期时间。因此,如果令牌被盗用,攻击者可以使用它来获得未经授权的访问权限。

5-JWT 的长度相对较长,可能会影响网络传输性能。

【二】使用jwt

【0】引入依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
    <scope>provided</scope>
</dependency>

【1】生成token

    // 秘钥
    private static final String SECRET_KEY = "123456654321";  // 建议存储于环境变量

    // 过期时间
    private static final long EXPIRATION_MS = 86400000;  // 24小时


    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    // 创建一个token,参数为claims和subject
    private String createToken(Map<String, Object> claims, String subject) {
        // 使用Jwts.builder()创建一个token
        return Jwts.builder()
                // 设置token的claims
                .setClaims(claims)
                // 设置token的主题
                .setSubject(subject)
                // 设置token的签发时间
                .setIssuedAt(new Date(System.currentTimeMillis()))
                // 设置token的过期时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS * 1000))
                // 使用HS256算法和SECRET_KEY对token进行签名
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                // 将token压缩成一个字符串
                .compact();
    }

【2】解析token

    // 从token中提取所有声明
    public Claims extractAllClaims(String token) {
        // 使用SECRET_KEY解析token,并返回声明
        Claims claims = null;
        try {
            claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
        } catch (SignatureException e) {
            log.info("无效的JWT签名:{}",e.getMessage());
            throw new CustomRuntimeException("无效的JWT签名,请重新登录");
        }

        return claims;
    }

【3】验证逻辑

    // 验证token是否有效,参数为token和userDetails
    public Boolean validateToken(String token, UserDetails userDetails) {
        // 从token中提取用户名
        final String username = extractUsername(token);
        // 返回用户名是否与userDetails中的用户名相等,并且token是否未过期
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    // 从token中提取用户名
    public String extractUsername(String token) {
        // 从token中提取声明,并返回用户名
        return extractClaim(token, Claims::getSubject);
    }

    // 从token中提取指定类型的声明
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        // 从token中提取所有的claims
        final Claims claims = extractAllClaims(token);
        // 使用claimsResolver函数处理claims,并返回结果
        return claimsResolver.apply(claims);
    }

    // 从token中提取所有声明
    public Claims extractAllClaims(String token) {
        // 使用SECRET_KEY解析token,并返回声明
        Claims claims = null;
        try {
            claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
        } catch (SignatureException e) {
            log.info("无效的JWT签名:{}",e.getMessage());
            throw new CustomRuntimeException("无效的JWT签名,请重新登录");
        }

        return claims;
    }

【4】完整工具类

@Component
@Slf4j
public class JwtUtils {
    // 秘钥
    private static final String SECRET_KEY = "123456654321";  // 建议存储于环境变量

    // 过期时间
    private static final long EXPIRATION_MS = 86400000;  // 24小时


    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    // 创建一个token,参数为claims和subject
    private String createToken(Map<String, Object> claims, String subject) {
        // 使用Jwts.builder()创建一个token
        return Jwts.builder()
                // 设置token的claims
                .setClaims(claims)
                // 设置token的主题
                .setSubject(subject)
                // 设置token的签发时间
                .setIssuedAt(new Date(System.currentTimeMillis()))
                // 设置token的过期时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS * 1000))
                // 使用HS256算法和SECRET_KEY对token进行签名
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                // 将token压缩成一个字符串
                .compact();
    }

    // 验证token是否有效,参数为token和userDetails
    public Boolean validateToken(String token, UserDetails userDetails) {
        // 从token中提取用户名
        final String username = extractUsername(token);
        // 返回用户名是否与userDetails中的用户名相等,并且token是否未过期
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    // 从token中提取用户名
    public String extractUsername(String token) {
        // 从token中提取声明,并返回用户名
        return extractClaim(token, Claims::getSubject);
    }

    // 从token中提取过期时间
    public Date extractExpiration(String token) {
        // 调用extractClaim方法,传入token和Claims::getExpiration方法引用
        return extractClaim(token, Claims::getExpiration);
    }

    // 从token中提取指定类型的声明
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        // 从token中提取所有的claims
        final Claims claims = extractAllClaims(token);
        // 使用claimsResolver函数处理claims,并返回结果
        return claimsResolver.apply(claims);
    }

    // 从token中提取所有声明
    public Claims extractAllClaims(String token) {
        // 使用SECRET_KEY解析token,并返回声明
        Claims claims = null;
        try {
            claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
        } catch (SignatureException e) {
            log.info("无效的JWT签名:{}",e.getMessage());
            throw new CustomRuntimeException("无效的JWT签名,请重新登录");
        }

        return claims;
    }

    // 判断token是否过期
    public Boolean isTokenExpired(String token) {
        // 提取token中的过期时间
        return extractExpiration(token).before(new Date());
    }


    /**
     * 根据令牌获取用户标识
     *
     * @param token 令牌
     * @return 用户ID
     */
    public String getUserKey(String token)
    {
        Claims claims = extractAllClaims(token);
        return getValue(claims, SecurityConstants.USER_KEY);
    }

    /**
     * 根据身份信息获取键值
     *
     * @param claims 身份信息
     * @param key 键
     * @return 值
     */
    public String getValue(Claims claims, String key)
    {
        return Convert.toStr(claims.get(key), "");
    }


    /**
     * 根据令牌获取用户标识
     *
     * @param claims 身份信息
     * @return 用户ID
     */
    public String getUserKey(Claims claims)
    {
        return getValue(claims, SecurityConstants.USER_KEY);
    }

    /**
     * 根据身份信息获取用户ID
     *
     * @param claims 身份信息
     * @return 用户ID
     */
    public String getUserId(Claims claims)
    {
        return getValue(claims, SecurityConstants.DETAILS_USER_ID);
    }

    /**
     * 根据令牌获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUserName(String token)
    {
        Claims claims = extractAllClaims(token);
        return getValue(claims, SecurityConstants.DETAILS_USERNAME);
    }

    /**
     * 根据身份信息获取用户名
     *
     * @param claims 身份信息
     * @return 用户名
     */
    public String getUserName(Claims claims)
    {
        return getValue(claims, SecurityConstants.DETAILS_USERNAME);
    }

}

【5】登录逻辑

jwt整合到springSecurity使用案例再做详细整理

【三】登录问题

【1】有无状态登录

(1)有状态登录

cookie+session,所谓的状态就是在服务端存储session信息。客户端访问服务端的时候,在cookie中携带sessionId,服务端根据sessionId就能才找到对应的session信息。

缺点:分布式支持不好,默认是内存存储,一般用的时候:把sessionId存到redis中

(2)无状态的登录

服务端不存储session信息,就需要token机制,客户端访问服务端的时候,需要在header或者是cookie中传递token信息,并且token中是携带了用户信息的。一旦生成,不能修改。

优点:分布式支持好

缺点:实现比较麻烦,续约、登出

续约:每次访问重新生成一个新的token(不推荐);使用两个token,accesstoken refreshtoken(有效期长); 临近过期生成新的token

登出: 登出之后,把token的id存入redis的黑名单

【2】分布式项目登录问题

redis存储session信息

用户登录成功以后,服务端生成一个uuid(token),同时把uuid作为key,用户信息作为value存储到redis中,然后把uuid返回给客户端。客户端在访问服务端接口的时候,可以在cookie或者header中传递这个uuid,服务端收到请求,首先读取这个uuid,然后根据uuid去redis中查找用户。这种方式也可以避免session的不同步问题,因为现在是存储到了多个节点都可以访问的redis中。

(1)解析token并向下游的微服务传递

以admin为例,admin中所有的接口(除了登录),都需要登陆以后才能访问。admin服务接口中需要知道是哪个用户的请求。所以网关直接把解析出来的userId传递给下游的微服务即可。

如何传递?

// 把userId传递给下游的微服务
request.mutate().header("userId", "" + userId);

使用的是spring5提供webflux的api向request中追加请求头。

(2)完整的登录和鉴权过程

(1)前端用户传递的用户名和密码,服务端收到请求,根据用户名到db查询记录,根据db中记录的password与前端传递的password做匹配,如果能匹配成功,则生成jwt token返回给前端,token中携带了userId。
(2)前端会把token保存到local storage(本地存储器)中,在随后的访问中,在header中携带上这个token。
(3)前端的请求首先是到网关,网关中有一个全局的过滤器,会从请求中读取header中的token,解析,把解析出来的userId放入http的header中继续向下游的微服务传递,header的key=userId。
(4)微服务中有一个拦截器,在拦截器中拦截请求,从header中解析出userId,然后保存到ThreadLocal中。
(5)在微服务的接口中,就可以使用ThreadLocal来获取用户信息。

(3)Token续签问题

(1)单token方案

(1)将 token 过期时间设置为15分钟;
(2)前端发起请求,后端验证 token 是否过期;如果过期,前端发起刷新token请求,后端为前端返回一个新的token;
(3)前端用新的token发起请求,请求成功;
(4)如果要实现每隔72小时,必须重新登录,后端需要记录每次用户的登录时间;用户每次请求时,检查用户最后一次登录日期,如超过72小时,则拒绝刷新token的请求,请求失败,跳转到登录页面。

另外后端还可以记录刷新token的次数,比如最多刷新50次,如果达到50次,则不再允许刷新,需要用户重新授权。

(2)双token方案

(1)登录成功以后,后端返回 access_token 和 refresh_token,客户端缓存此两种token;
(2)使用 access_token 请求接口资源,成功则调用成功;如果token超时,客户端携带 refresh_token 调用token刷新接口获取新的 access_token;
(3)后端接受刷新token的请求后,检查 refresh_token 是否过期。如果过期,拒绝刷新,客户端收到该状态后,跳转到登录页;如果未过期,生成新的 access_token 返回给客户端。
(4)客户端携带新的 access_token 重新调用上面的资源接口。
(5)客户端退出登录或修改密码后,注销旧的token,使 access_token 和 refresh_token 失效,同时清空客户端的 access_token 和 refresh_toke。

(3)方案

(1)登录成功,生成两个token存到cookie或者local storage (一长一短)
(2)发起请求,网关检验token(短),验证通过则添加到请求头并放行路由到微服务,不通过则说明过期或者被篡改, 封装响应码通知前端
(3)前端接收通过,将token(长)发送给后端请求刷新token的请求,后端检验token(长),验证通过则重新生成两个token返回给前端,然后添加到请求头并放行路由到微服务,不通过则说明过期或者被篡改

(4)Token续期处理

用户登录完成以后,服务端会生成两个token,一个是有效期比较短的accesstoken,比如2小时,还有一个是有效期长的refreshToken,比如30天,客户端会把这两个token都保存到客户端本地存储
客户端在随后访问服务端接口的时候,需要在header中携带accesstoken。请求首先是到网关,网关会做token过期时间的判断,如果token没过期则正常放行到后端的微服务,如果token已经过期,网关会返回一个特殊的代表token过期的响应码4001,客户端收到4001状态码的响应以后,不会向客户端提示失败,而目继续访问服务端提供的refresh token的请求,这个请求会返回一个新的accesstoken和refreshtoken,客户端会继续使用这个新的accesstoken去访问服务端的接口。

(5)Token登出处理

生成token的时候,会给token设置一个token id
当用户退出的时候,把token id存放到redis中,key: token id,value:userld,有效期设置2小时
网关收到请求的时候,首先是根据token id去redis中查询,如果能查到值,说明token已经退出了,则返回token已经失效,如果查不到,则说明token是有效的,继续后续的业务逻辑外理。

【4】如何获取客户端的各种信息

HttpServletRequest request = ServletActionContext.getRequest();
System.out.println("浏览器基本信息:"+request.getHeader("user-agent"));
System.out.println("客户端系统名称:"+System.getProperty("os.name"));
System.out.println("客户端系统版本:"+System.getProperty("os.version"));
System.out.println("客户端操作系统位数:"+System.getProperty("os.arch"));
System.out.println("HTTP协议版本:"+request.getProtocol());
System.out.println("请求编码格式:"+request.getCharacterEncoding());
System.out.println("Accept:"+request.getHeader("Accept"));
System.out.println("Accept-语言:"+request.getHeader("Accept-Language"));
System.out.println("Accept-编码:"+request.getHeader("Accept-Encoding"));
System.out.println("Connection:"+request.getHeader("Connection"));
System.out.println("Cookie:"+request.getHeader("Cookie"));
System.out.println("客户端发出请求时的完整URL"+request.getRequestURL());
System.out.println("请求行中的资源名部分"+request.getRequestURI());
System.out.println("请求行中的参数部分"+request.getRemoteAddr());
System.out.println("客户机所使用的网络端口号"+request.getRemotePort());
System.out.println("WEB服务器的IP地址"+request.getLocalAddr());
System.out.println("WEB服务器的主机名"+request.getLocalName());
System.out.println("客户机请求方式"+request.getMethod());
System.out.println("请求的文件的路径"+request.getServerName());
System.out.println("请求体的数据流"+request.getReader());
BufferedReader br=request.getReader();
String res = ""; 
while ((res = br.readLine()) != null) {  
    System.out.println("request body:" + res);   
}
System.out.println("请求所使用的协议名称"+request.getProtocol());
System.out.println("请求中所有参数的名字"+request.getParameterNames());
Enumeration enumNames= request.getParameterNames();
while (enumNames.hasMoreElements()) {
    String key = (String) enumNames.nextElement();
    System.out.println("参数名称:"+key);
}


http://www.kler.cn/a/598241.html

相关文章:

  • 牛客网【模板】二维差分(详解)c++
  • 【JavaEE】网络编程socket
  • Java学习路线(便于理解)
  • PostgreSQL_数据使用与日数据分享
  • C语言-访问者模式详解与实践
  • Enovia许可分析的自动化解决方案
  • 程序代码篇---Pyqt的密码界面
  • Agent TARS开源多模态 AI 代理的革命性突破
  • B树和 B+树
  • Security如何复制粘贴
  • Scikit-learn模型构建全流程解析:从数据预处理到超参数调优
  • 矩阵键盘原理与单片机驱动设计详解—端口反转法(下) | 零基础入门STM32第七十八步
  • 可视化操作界面,工程项目管理软件让复杂项目管理变简单
  • AWS SAP学习笔记-概念
  • 2025最新docker教程(四)
  • WPF 样式和模板的区别
  • 从零开始上手huggingface
  • 遇见东方微笑·畅游如意甘肃——“天水文化旅游嘉年华”2025年春季文旅宣传推广活动侧记
  • 利用 Agent TARS 技术实现互联网舆情监测与事件自动化创建的可行性与前景
  • 给语言模型增加知识逻辑校验智能,识别网络信息增量的垃圾模式