32 图 | 手摸手 Spring Cloud Gateway + JWT 实现登录认证

网站建设4年前发布
33 0 0

20230305203709a283b2b72b556cd3f9e0682548c5ac904e44a0501,目录,通过本文你会掌握以下知识点:,本篇还是基于我的开源项目 PassJava 作为讲解。,PassJava 开源地址:https://github.com/Jackson0714/PassJava-Platform,在讲解之前有必要澄清下什么是认证、授权、凭证,这三个方面是一个系统中最基础的安全设计。,认证表示你是谁。系统如何正确分辨出操作用户的真实身份,比如通过输入用户名和密码来辨别身份。,授权表示你能干什么。系统如何控制一个用户能看到哪些数据和操作哪些功能,也就是具有哪些权限。,表示你如何证明你的身份。系统如何保证它与用户之间的承诺是双方当时真实意图的体现,是准确、完整和不可抵赖的。,接下来我们看下使用 JWT 作为凭证完成认证的原理。,在如下的认证时序图中,有以下几种角色:,认证和校验身份的流程如下所示:,20230305203709c9f2ade414d0efe8da16059d07c9478e45c1f9384,认证和校验身份流程,Github 项目地址:https://github.com/Jackson0714/PassJava-Platform,Gitee 项目地址:https://toscode.gitee.com/jayh2018/PassJava-Platform,20230305203854674d73c98f5f5f53e7f891f378e9131d2bfe4d245,PassJava-Platform 框架,20230305203711041264052b880210e8a044c66a2c138541de67915passjava-auth 服务,核心类就是 JwtAuthController 类,里面有登录接口和刷新令牌的接口。,20230305204102c9a6f013074fd4995661925d8fb213574b2e0c891,passjava-gateway 服务,核心类就是 JwtAuthCheckFilter 全局过滤器。,如果不需要在服务端保存刷新令牌,可以不需要 redis 配置。,20230305203711f388f707462ff6bb810711020cef87e8c8e845334,passjava-jwt 服务,核心类就是 PassJavaJWTTokenUtil 工具类。认证服务引入 JWT 项目后用来生成 token,网关服务引入 JWT 项目后用来校验 token 合法性。,这里我选择了会员微服务作为本次演示的业务微服务。,它从网关转发的请求 Header 中拿到 userId, 根据 userId 查询 member 信息。,20230305203712b58874654748b2a544e346f060b3198d2518ec100,passjava-member 服务,核心文件是 MemberController 类、MemberEntity实体类、MemberService服务类、MemberDao 类和 mapper 文件。,Nacos 注册配置中心,首先启动 Nacos 服务。和 PassJava 项目配套使用的 Nacos 工具已经上传到网盘,下载后直接运行启动脚本就可以将 Nacos 在本地启动。,启动教程:,网关、会员、认证服务,启动以下三个微服务,分别为网关、会员、认证服务。,20230305203712c63cee5322d83838419843d39633cabe74cc4d838,检查下 nacos 注册中心上是否注册了这三个服务:可以看到确实有上面的三个微服务。,20230305203713895e94268ab2934a45e2123db47e96fbff7234610,登录认证就是校验下用户提交的账户名和密码与本地数据库中的是否完全匹配,如果匹配,就认证通过。就是下方这个流程的 1、2、3 步。,20230305203855c9436a8353b15e76d51856a9fe4dc0e7f81d88638,这里用 Postman 工具模拟前端发起登录请求,请求的 URL 如下:,20230305203714c971b966494d6f9e2a8885152affcb00fb4692873,请求是向网关服务 passjava-gateway 发起的,所以可以看到上面的 URL 中 localhost 和 8060 是网关的 host 和 port。,然后 API 地址为 /api/auth/login,这个地址经过网关的路由匹配后会转发到 passjava-auth 服务的登录 API。,关于网关转发的原理可以参考这篇:深入理解 Spring Cloud Gateway 的原理,请求参数如下:,账号和密码都是密文的,转发到认证服务后,会根据 userId 查询出系统用户,然后将 password 参数加密后对比系统用户的密码。,所以为了让用户登录成功,还需要在数据库插入一条系统用户,用户 id 为 wukong,密码是对 123456 加密后的密码。,20230305203714590a7b7645ad7f8f9c2831c4dfbdd854bc7bf5821,在线加密工具地址:,转发登录请求是网关服务做的,所以我们来看下做了哪些事情。,在 Gateway 项目的 application-routers.yml 中配置路由规则:,在 application.properties 引入 application-routers.yml,第三步:验证用户账号和密码,这一步是认证服务的登录 API 里面做的。在 AuthController 中定义 login 接口,核心步骤就是查找系统用户和比对密码。,20230305203715c90ebf29233e667b87d557a47dbbe6d3b9b123462,登录 API,用户名和密码匹配成功后,就会生成 JWT 令牌。,生成令牌就是通过工具类 PassJavaJwtTokenUtil 生成 JWT Token,也就是流程图中的第四步。,20230305203715a83540280153f21183642629b423b262c4128a476,流程图-生成 JWT 令牌,生成令牌的核心代码如下:,202303052038579230ebc090d9ca3cbfd5142649bb8f09a88f10553,生成 JWT 的核心代码,使用这个工具类的前提是我们需要先引入 jjwt 依赖。这个在 passjava-jwt 项目的 pom 文件中引入。,20230305203716845b00299e7a7c188b382731daec5139754eac732,引入 jjwt 依赖,用 Postman 工具调用后,可以看到生成的令牌如下:,20230305203717259ea3286976093047d176bd262c799cba4ed3683,生成令牌,用 base64 解码后,可以看到 token 中的 PAYLOAD 里面包含了用户 id 和用户名。,2023030520371734e4ec4008543be82431280dd1340facabe282470,生成 JWT 的加密密钥一般都是写到配置文件中。这里我是配置在 passjava-jwt 项目的 application-jwt.yml 配置文件中的。,20230305203718c30ff9f66049af494ec6539e9875f9acbd6c25239,JWT 配置项,然后认证服务就会将 JWT 令牌返回给客户端了。当客户端想要查询这个 userId 对应的会员信息时,就可以在请求的 Header 中带上 JWT 令牌。,20230305203857316c87b8512654e67768724b328a9afb40b427595,客户端(浏览器或 APP)拿到 JWT 后,可以将 JWT 存放在浏览器的 Cookie 或 LocalStorage(本地存储) 或者内存中。,发送请求时在请求 Header 的 Authorization 字段中设置 JWT,这个字段其实可以自定义,但是我建议用 Authorization,因为这是一种业界标准。,另外告诉大家一个小技巧,在 Postman 工具中有个地方专门配置 Authorization,然后自动加到 Header 中,不用自己手动加 Header。,20230305203719b95ec9464247b1a6c98477e246f71db733af80626,20230305203719d121d53630c214d454b4581556296b183cd629446,还有一个点需要注意,这里配置的 Authorization 的认证类型为 Bearer Token。它表示令牌可以是任意字符串格式的令牌。然后会在 Authorization 字段中加上一个前缀 Bearer。所以我们在网关服务解析 Header 中的 Authorization 时,需要去掉这个前缀 Bearer,代码如下所示:,2023030520385872afbff31085818df3d338961758b6fa800a76576,去掉 Bearer 前缀,20230305203720594e07a78092a50ff6838833520a34e5e272fd332,网关验证 Token和转发请求,网关接收到前端发起的业务请求后,会先验证请求的 Header 中是否携带 Authorization 字段,以及里面的 Token 是否合法。然后解析 Token 中的 userId 和 username,放到 header 中再进行转发,也就是流程图中的第七步和第八步。,网关是通过多个​​过滤器 Filter​​对请求进行串行拦截处理的,所以我们可以自定义一个全局过滤器,对所有请求进行校验,当然对于一些特殊请求比如登录请求就不需要校验了,因为调用登录请求的时候还没有生成 Token。,网关的全局过滤器 JwtAuthCheckFilter 的核心代码如下所示:,20230305203721d898b04756efa96563b304f8a08341a6790db3761,网关的全局过滤器 JwtAuthCheckFilter,2023030520372209e9dc463dd59252ee721468ac4328e7d22a2b798,会员服务接收到网关转发的请求后,就从 Header 中拿到用户身份信息,然后通过 userId 获取会员信息。,注意:有的时候业务逻辑并不需要身份信息,更多的时候是需要检验用户的操作权限是否足够。其实 Token 里面也是可以携带权限信息的,不过这是下一篇讲解授权的部分。,获取 userId 的方式其实可以通过加一个​​拦截器​​,由拦截器将 Header 中的 userId 和 username 放到线程中,后续的 controller,service,dao 类都可以从线程里面拿到 userId 和 username,不用通过传参的方式。,获取 userId 的方式:,代码示例如下:,202303052037223674bcb08e68e0bced71347d52ce12b71cb77d736,下面介绍如何使用拦截器方式将 userId 存入线程变量的方式。,在 passjava-common 模块中新增一个拦截器,获取请求头中的身份信息,加入到线程变量中。文件名为 HeaderInterceptor。,20230305213658b9d69dd905d9359044211460d928bb05663ba6827,将拦截器注册到 WebMvcConfigurer。文件名为 WebMvcConfig.java。,20230305213402299e33197746ab2e3176618f1e0b55a059e7eb730,配置文件中需要定义一个配置项:,然后 passjava-member 服务引入这个拦截器配置。,通过上面两种方式中的任意一种拿到 userId 后,通过 userId 查询会员的详情。这里需要注意的是这个 user 既是系统用户也是系统中的会员。关于查询会员的数据库操作就不在此展开了。,执行结果如下图所示:,20230305203723030274a11b8c73fb0db401a56ffd294c6fe8a0432,还有一个内容是关于如何刷新令牌的。当认证服务返回给客户端的 JWT 也就是 access_token 过期后,客户端是通过发送登录请求重新拿到 access_token 吗?,这种重新登录的操作如果很频繁(因 JWT 过期时间较短),对于用户来说体验就很差了。客户端需要跳转到登录页面,让用户重新提交用户名和密码,即使客户端有记住用户名和密码,但是这种跳转到登录页的操作会大幅度降低用户的体验,甚至导致用户不想再用第二次。,有没有一种比较优雅的方式让客户端重新拿到 access_token 或者说延长 access_token 有效期呢?,我们知道 JWT 生成后是不能篡改里面的内容,即使是 JWT 的有效期也不行。所以延长 access_token 有效期的做法并不适合,而且如果长期保持一个 access_token 有效,也是不安全的。,那就只能重新生成 access_token 了。方案其实挺简单,客户端拿之前生成的 JWT 调用后端一个接口,然后后端校验这个 JWT 是否合法,如果是合法的就重新生成一个新的返回给客户端。客户端自行替换掉之前本地保存的 access_token 就可以了。,2023030520372475ea8b719c5d06fed2189028dd7f7b6a302f94868,生成 access_token 和 refresh_token,这里有一个巧妙的设计,就是生成 JWT 时,返回了两个 JWT token,一个 access_token,一个 refresh_token,这两个 token 其实都可以用来刷新 token,但是我们把 refresh_token 设置的过期时间稍微长一点,比如两倍于 access_token,当 access_token 过期后,refresh_token 如果还没有过期,就可以利用两者的过期时间差进行重新生成令牌的操作,也就是刷新令牌,这里的刷新指的是客户端重置本地保存的令牌,以后都用新的令牌。,当然,在 access_token 过期之前,客户端提前刷新令牌也是可以的,我称这种提前刷新的模式为​​饥饿模式​​​(单例模式中也有这种叫法),而过期后再刷新令牌的模式我称之为​​懒模式​​。两种模式都可以用,前者需要客户端定期检查过期时间,增加了复杂性;后者则会出现短暂的请求失败的情况,得拿到新的令牌后才会成功。,刷新令牌的操作完全是通过客户端自己控制的,而且客户端也不仅限于浏览器,还有可能是第三方服务。,通常情况下,我们会将刷新令牌 refresh_token 设置为只能用一次,来保证刷新令牌的安全性。而这种就需要服务端来缓存刷新令牌了,当用过一次后,就从缓存里面主动剔除掉。但这样就违背了 JWT 无状态的特性,这个完全看业务需求来决定是否使用这种缓存方式。,如下图所示,生成令牌时我将刷新令牌缓存到了 Redis 里面。当我用 refresh_token 调用刷新 API 时,会主动剔除掉这个 key,下次再用相同的 refresh_token 刷新令牌时,因 Redis 中不存在这个 key,就会提示刷新刷新失败了。,202303052037247443f9937cbe669eeee279378900f3d7cf8f41835,缓存令牌,留两个小问题:,虽然本篇是讲实战内容的,但是里面又涉及了很多原理性内容,比如网关、JWT 的原理。,结合实战讲解,相信大家对如何使用 Spring Cloud Gateway + JWT 实现登录认证有了充分的理解。,本篇只讲解了认证和凭证,授权部分还没有触及,所以这也是下篇要讲解的内容,来追更吧~,最后再说一句,别白嫖,点赞转发下哦~

© 版权声明

相关文章