JWT入门以及常见的登录问题

在这里插入图片描述

本文主要介绍了JWT是什么,如何使用JWT以及实际开发中可能会遇到的有关JWT登录问题,比如token的续签、续期和登出问题等。

1 JWT概念

1.1 什么是JWT

JWT官方网址

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

1.2 JWT有什么用

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

1.3 JWT的组成结构

JWT由Header(头信息),PayLoad (用户信息),Signature(签名)三个部分组成请添加图片描述

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

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

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

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

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

PayLoad(载荷)
指定了七个默认字段供选择

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

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

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

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

eyJ1c2VybmFtZSI6InpoYW5nc2FuIiwibmFtZSI6IuW8oOS4iSIsImFnZSI6MTgsInNleCI6IuWlsyIsImV4cCI6MTY0NzE0NTA1MSwianRpIjoiMTIxMjEyMTIxMiJ9

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

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

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

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InpoYW5nc2FuIiwibmFtZSI6IuW8oOS4iSIsImFnZSI6MTgsInNleCI6IuWlsyIsImV4cCI6MTY0NzE0NTA1MSwianRpIjoiMTIxMjEyMTIxMiJ9.5tmHCpcsS_VuZ2_z5Rydf2OpsviBGwB-fJE5aS7gKqE

1.4 JWT的优点和缺点

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

优点:

  • jwt基于json,非常方便解析。

  • 可以在令牌中自定义丰富的内容,易扩展。

  • 资源服务使用JWT可不依赖认证服务即可完成授权。

  • 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。

缺点:

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

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

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

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

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

2 使用JWT

2.1 引入jwt坐标

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

2.2 生成token

//1.声明加密的算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//2.声明过期时间
long expMillis = System.currentTimeMillis() + 过期毫秒值;
Date exp = new Date(expMillis);
//3.配置jwt中要存入的信息
Map claims = new HashMap();
claims.put("id",1001);
claims.put("name","admin");
//4.创建密钥
String secretKey = "miyao"    

//5.生成jwt
String token = Jwts.builder()
    //配置jwt中需要保存的数据
    .setClaims(claims)
    //配置加密算法&加密的key
    .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
    //配置过期时间
    .setExpiration(exp)
    //根据配置信息生成jwt
    .compact();

2.3 解析token

Claims claims = Jwts.parser()
    // 设置签名的秘钥
    .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
    // 设置需要解析的jwt
    .parseClaimsJws(token).getBody();

int id = claims.get("id");

2.4 jwt工具类

/**
 * jwt工具类
 */
public class JwtUtils {

    /**
     * 生成token加密令牌
     * @param params    =>  存入的数据
     * @param secretKey =>  签名的密钥
     * @param ttlMills  =>  过期时间(毫秒)
     * @return 生成的jwt token
     */
    public static String createToken(Map<String, Object> params, String secretKey, long ttlMills){

        //1.声明加密的算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //2.声明过期时间
        long expMillis = System.currentTimeMillis() + ttlMills;
        Date exp = new Date(expMillis);
        //3.生成jwt
        String token = Jwts.builder()
                //配置jwt中需要保存的数据
                .setClaims(params)
                //配置加密算法&加密的key
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                //配置过期时间
                .setExpiration(exp)
                //根据配置信息生成jwt
                .compact();

        return token;
    }

    /**
     * 验证jwt, 解析token
     * @param token      =>  加密令牌
     * @param secretKey  =>  密钥
     * @return 解析的结果集
     */
    public static Claims parseToken(String token, String secretKey){

        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();

        return claims;
    }

}

3 登录问题

3.1 有无状态登录

3.1.1 有状态登录

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

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

3.1.2 无状态的登录

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

优点:分布式支持好

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

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

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

3.2 单体项目登录问题

单体项目,可以使用session来维持会话,但是生产环境为了避免单点故障,一般都会部署多个节点,为了避免session不同步的问题,可以在nginx上开启ip_hash这种负载均衡算法,让来自同一个ip的客户端请求始终落到同一个tomcat上,就是所谓的session粘连

ip_hash

nginx提供ip_hash指令可实现session粘连需求,根据用户ip计算hash值,从而每次都分配到指定机器上,ip_hash支持配置weight权重

但是ip_hash也有个不足,要求nginx必须是最前端的机器,即用户不经过其他服务中转,直接请求nginx,这样ip_hash才能生效。如果在nginx和用户中间有别的服务,比如SLB、LVS等负载集群,nginx的ip_hash指令可能会失效。

Nginx配置如下:

worker_processes  1;
 
events {
    worker_connections  1024;
}
 
http {
	
	upstream ipHashDemo{
		
		ip_hash;
		server 127.0.0.1:8081;
		server 127.0.0.1:8082;
	}
	
	server{
	
		listen 8888;
		server_name 127.0.0.1;
		location / {
		
			proxy_pass http://ipHashDemo;
		}
	}
}
  • worker_processes 1; 表示 Nginx 使用单个 worker 进程。

  • events { worker_connections 1024; } 表示每个 worker 进程可以同时处理的最大连接数为 1024。

  • upstream ipHashDemo{…} 定义了一个名为 “ipHashDemo” 的负载均衡组,使用 IP 哈希算法,并将请求转发到两台后端服务器(分别监听 8081 和 8082 端口)。

  • server {…} 定义了一个虚拟主机,监听在本地 8888 端口,请求经过负载均衡组 “ipHashDemo” 处理并代理到后端服务器。

  • location / {…} 表示该虚拟主机对所有请求都进行代理。

根据上面的配置文件会启动一个 Nginx 实例,在监听 8888 端口时对所有请求进行负载均衡,ip_hash在上面的配置文件中用于定义一个名为 “ipHashDemo” 的负载均衡组,并使用 IP 哈希算法对请求进行路由。这样做的好处是在多个请求中,相同 IP 的请求总是会被路由到同一台后端服务器上,避免了一些请求需要重复地建立连接和发送状态信息的情况,提高了性能。

upstream hash

既然自带的ip_hash指令不好使,那么能不能自己指定一个字段来计算hash值,替代ip值,比如cookie或者request uri里面的一个能标识用户id的字段。

有个第三方的nignx模块nginx_upstream_hash-0.3.1可以实现该效果,该模块提供hash指令,可以指定一个变量用于hash,示例如下:

hash指令后面的变量可以设置为其他有效的nginx变量。

http {
    upstream backend {
        hash $arg_user_id consistent;
        server backend1.example.com;
        server backend2.example.com;
        server backend3.example.com;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://backend;
        }
    }
}

在这个配置中,我们定义了一个名为 “backend” 的负载均衡后端,指定了哈希指令 “hash $arg_user_id consistent”,即使用 GET 请求参数中的 “user_id” 变量作为哈希键,并且启用一致性哈希算法。然后我们将三个后端服务器添加到 upstream 中。

最后在 server 块中,我们将请求转发到 “backend” 负载均衡组。由于我们在 upstream 中启用了哈希算法并指定了要使用的变量,因此同一用户的请求会被分配到同一台后端服务器上。

3.3 分布式项目登录问题

redis存储session信息

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

3.3.1 解析token并向下游的微服务传递

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

如何传递?

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

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

3.3.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.3.3 Token续签问题

单token方案

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

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

双token方案

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

1, 登录成功,生成两个token存到cookie或者local storage (一长一短)

2, 发起请求,网关检验token(短),验证通过则添加到请求头并放行路由到微服务,不通过则说明过期或者被篡改, 封装响应码通知前端

3, 前端接收通过,将token(长)发送给后端请求刷新token的请求,后端检验token(长),验证通过则重新生成两个token返回给前端,然后添加到请求头并放行路由到微服务,不通过则说明过期或者被篡改

3.3.4 Token续期处理

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

3.3.5 Token登出处理

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

3.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);
}

在这里插入图片描述

  • 35
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
好的,我可以为您提供一个简单的示例代码来演示如何使用 Express 和 JSON Web Token (JWT) 创建登录验证功能。 首先,您需要安装 Express、jsonwebtoken、body-parser 和 cookie-parser 模块。您可以使用以下命令进行安装: ``` npm install express jsonwebtoken body-parser cookie-parser ``` 接下来,创建一个 `app.js` 文件并添加以下代码: ``` const express = require('express'); const jwt = require('jsonwebtoken'); const bodyParser = require('body-parser'); const cookieParser = require('cookie-parser'); const app = express(); const port = 3000; const secretKey = 'your_secret_key'; // 使用 body-parser 和 cookie-parser 中间件解析请求体和 Cookie app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); app.use(cookieParser()); // 处理登录请求 app.post('/login', (req, res) => { const { username, password } = req.body; // 检查用户名和密码 if (username !== 'your_username' || password !== 'your_password') { res.status(401).send('Invalid username or password'); return; } // 生成 JWT const token = jwt.sign({ username }, secretKey, { expiresIn: '1h' }); // 将 JWT 存储在 Cookie 中 res.cookie('jwt', token, { httpOnly: true, maxAge: 3600000 }); res.send('Login successful'); }); // 处理受保护的路由 app.get('/protected', (req, res) => { const token = req.cookies.jwt; // 检查 JWT 是否存在 if (!token) { res.status(401).send('Access denied'); return; } try { // 验证 JWT const decoded = jwt.verify(token, secretKey); // 在这里检查用户权限等 res.send('Protected resource'); } catch (err) { console.error(err); res.status(401).send('Access denied'); } }); // 启动服务器 app.listen(port, () => { console.log(`Server started on port ${port}`); }); ``` 在上面的代码中,我们首先创建了一个 `secretKey`,它将用于生成和验证 JWT。然后我们使用 `body-parser` 和 `cookie-parser` 中间件解析请求体和 Cookie。然后我们定义了一个 `/login` 路由来处理登录请求。在路由处理程序中,我们首先检查用户名和密码是否正确。如果正确,我们生成一个 JWT,并将其存储在名为 `jwt` 的 Cookie 中。然后我们返回成功登录的消息。 接下来,我们定义了一个 `/protected` 路由来处理受保护的资源请求。在路由处理程序中,我们首先检查名为 `jwt` 的 Cookie 是否存在。如果不存在,我们返回 401 响应。否则,我们验证 JWT 是否有效,如果有效,我们返回受保护的资源。请注意,在这里,您还可以检查用户权限等。 最后,我们启动服务器并监听端口 3000。 请注意,上面的代码仅供参考,您需要根据自己的实际需求进行调整。同时,为了安全起见,建议使用 HTTPS 协议来保护登录和保护资源的请求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

霁晨晨晨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值