当前位置: 首页 > 科技观察

32图-实战SpringCloudGateway+JWT实现登录认证

时间:2023-03-14 19:08:22 科技观察

目录通过本文,您将掌握以下知识点:如何使用认证服务进行登录认证。如何生成JWT令牌(Token)以及如何使用Gateway验证Token。Gateway如何从Token中获取用户信息,转发给业务服务。业务服务如何从请求中获取身份信息并处理业务逻辑。如何刷新令牌。本文仍然以我的开源项目PassJava作为说明。PassJava开源地址:https://github.com/Jackson0714/PassJava-Platform在讲解之前,有必要先弄清楚什么是认证、授权、凭证。这三个方面是一个系统中最基本的安全设计。身份验证、授权、凭证1.1身份验证(Authentication)身份验证表明你是谁。系统如何正确识别操作用户的真实身份,如通过输入用户名和密码来识别身份。1.2授权授权就是你可以做什么。系统如何控制用户可以看到哪些数据和功能,即用户有哪些权限。1.3凭证是指您如何证明您的身份。系统如何保证其与用户之间的承诺是双方当时真实意图的反映,是准确、完整、不可否认的。接下来我们看看使用JWT作为证书完成认证的原理。认证原理如下图认证时序图所示,有以下作用:Client:APP或PC的前端页面。Gateway:表示SpringCloudGateway网关服务,这里。认证服务:用于接收客户端的登录请求、注销请求、刷新令牌操作。业务服务:与系统业务相关的微服务。认证和身份验证流程如下:认证和身份验证流程①用户登录:客户端在登录页面输入用户名和密码,提交表单,调用登录接口。②转发请求:这里会先将登录请求发送给网关服务passjava-gateway,网关会直接将登录请求转发给认证服务passjava-auth。(网关对登录请求不校验token,可以配置哪些请求url不校验。)③认证:认证服务会将请求参数中的用户名+密码与数据库中的用户进行比对。如果它们完全匹配,则认证通过。④生成token:生成两个token:access_token和refresh_token(刷新令牌),刷新令牌后面会说到,其实这里只能生成一个tokenaccess_token。令牌将包含用户的身份信息。如果需要进行权限控制,token中还必须包含用户的权限信息。权限本文不再展开,下篇文章会进行说明。⑤客户端缓存token:客户端获取两个token,缓存在cookie或LocalStorage中。⑥发起带有token的请求:当客户端下次要调用业务服务时,将access_token放在请求的header中。⑦网关校验token:请求还是先到网关服务,然后由网关服务校验access_token是否合法。如果access_token没有过期并且能够被正确解析,说明它是一个合法的access_token。⑧转发带有用户身份信息的请求:网关将access_token中携带的用户user_id放入请求头中,转发给真正的业务服务。⑨处理业务逻辑:业务服务从header中获取用户的user_id,然后对业务逻辑进行处理,处理后按原路返回给客户端。接下来,我们来看一下项目的整体结构。项目整体结构Github项目地址:https://github.com/Jackson0714/PassJava-PlatformGitee项目地址:https://toscode.gitee.com/jayh2018/PassJava-Platform认证服务:passjava-auth网关服务:passjava-gatewayJWT公共项目:passjava-jwt,认证服务和网关服务都会引用这个公共项目。业务服务:passjava-member,会员服务就是本案例的业务服务。Nacos注册配置中心PassJava-Platform框架认证服务:passjava-authpassjava-auth核心服务类是JwtAuthController类,有登录接口和刷新令牌接口。网关服务:passjava-gatewaypassjava-gateway服务核心类是JwtAuthCheckFilter全局过滤器。如果不需要在服务端保存refreshtoken,则不需要redis配置。JWT公共项目passjava-jwt服务的核心类是PassJavaJWTTokenUtil工具类。认证服务用于引入JWT项目后生成token,网关服务用于引入JWT项目后验证token的合法性。业务服务这里我选择会员微服务作为本次演示的业务微服务。从网关转发的请求头中获取userId,根据userId查询成员信息。passjava-member服务的核心文件有MemberController类、MemberEntity实体类、MemberService服务类、MemberDao类和mapper文件。启动服务Nacos注册配置中心首先启动Nacos服务。PassJava项目使用的Nacos工具已经上传到网盘。下载后直接运行启动脚本,在本地启动Nacos。启动教程:www.passjava.cn/#/01.项目介绍/7.本地部署项目Mac版网关、会员、认证服务启动下面三个微服务,分别是网关、会员、认证服务。查看nacos注册中心是否注册了这三个服务:可以看到确实有以上三个微服务。如何进行登录认证登录认证是检查用户提交的帐号和密码是否与本地数据库中的完全匹配,如果匹配则认证通过。这是以下过程的第1、2和3步。第一步:提交用户名和密码这里使用Postman工具模拟前端发起登录请求。请求的URL如下:http://localhost:8060/api/auth/login请求是向网关服务passjava-gateway发起的,所以可以看到在上面的URL中,localhost和8060是host和网关的端口。然后API地址为/api/auth/login,通过网关的路由匹配后会转发到passjava-auth服务的登录API。http://localhost:10001/auth/login网关转发的原理可以参考这篇文章:深入理解SpringCloudGateway原理请求参数如下:{"userId":"wukong","password":"123456"}accountpassword和password都是密文。转发到认证服务后,根据userId查询系统用户,然后将password参数加密后与系统用户的密码进行比对。所以为了让用户登录成功,还需要在数据库中插入一个系统用户,用户id为wukong,密码为加密后的密码123456。在线加密工具地址:https://www.bejson.com/encrypt/bcrpyt_encode/第二步:转发登录请求转发登录请求是由网关服务完成的,让我们看看它做了什么。在Gateway项目的application-routers.yml中配置路由规则:spring:cloud:gateway:routes:-id:route_auth#认证微服务路由规则uri:lb://passjava-auth#负载均衡,转发请求到注册的passjava-auth中心注册的服务谓词:#Assertion-Path=/api/auth/**#如果前端请求路径包含api/auth,应用此路由规则过滤器:#filter-RewritePath=/api/(?.*),/$\{segment}#将跳转路径中包含的api替换为空,并在application.properties中引入application-routers.ymlspring:profiles:include:routers,jwt第三步:验证用户账号password和password这一步是在认证服务的登录API中完成的。在AuthController中定义登录接口,核心步骤是查找系统用户并比对密码。一旦登录API用户名和密码匹配成功,将生成一个JWT令牌。如何生成token生成token就是通过工具类PassJavaJwtTokenUtil生成JWTToken,也就是流程图中的第四步。流程图-生成JWTtoken生成token的核心代码如下:生成JWT的核心代码使用该工具类的前提是我们需要先引入jjwt依赖。这个在passjava-jwt项目的pom文件中有介绍。引入jjwt依赖,使用Postman工具调用后,可以看到生成的token如下:生成的token经过base64解码后,可以看到token中的PAYLOAD中包含了userid和username。生成JWT的加密密钥一般写在配置文件中。这里我是在passjava-jwt项目的application-jwt.yml配置文件中配置的。JWT配置项,然后认证服务将JWTtoken返回给客户端。当客户端想要查询这个userId对应的会员信息时,可以在请求的header中带上JWTtoken。如何携带JWT发送请求客户端(浏览器或APP)拿到JWT后,可以将JWT存储在浏览器的Cookie或LocalStorage(本地存储)或内存中。发送请求时,在请求头的Authorization字段中设置JWT。这个字段其实是可以自定义的,但是我推荐使用Authorization,因为它是一个行业标准。另外告诉大家一个小技巧,在Postman工具中有个地方可以配置Authorization,然后自动添加到Header中,不用手动添加Header。还有一点需要注意的是这里配置的Authorization的认证类型是BearerToken。表示该token可以是任意字符串格式的token。授权字段然后以Bearer为前缀。所以我们在分析网关服务中Header中的Authorization时,需要去掉前缀Bearer。代码如下:去掉Bearer前缀。网关如何验证JWT并转发请求?首先验证请求头中是否携带了Authorization字段,里面的Token是否合法。然后解析Token中的userId和username,放到header中转发,也就是流程图中的第七步和第八步。网关通过多个过滤器串行拦截请求,所以我们可以自定义一个全局过滤器来验证所有请求。当然,对于一些特殊的请求,比如登录请求,是不需要验证的。已经验证过了,因为调用登录请求的时候还没有生成Token。网关全局过滤器JwtAuthCheckFilter核心代码如下:网关全局过滤器JwtAuthCheckFilter会员服务处理业务逻辑会员服务收到网关转发的请求后,从header中获取用户身份信息,进而获取会员信息通过userId。注意:有时候业务逻辑不需要身份信息,更多的时候需要检查用户的操作权限是否足够。其实Token中也可以携带权限信息,不过这是下一篇讲解授权的部分。获取userId的方式其实可以通过添加拦截器来实现。拦截器将Header中的userId和username放入线程中,后续的controller、service、dao类可以从线程中获取到userId和username。不需要传递参数。获取userId的方法:方法一:从请求头中获取userId。代码很简单,但是如果其他地方也用到userId,则需要通过方法参数传递userId。方法二:从线程变量中获取userId。代码复杂,使用简单。好处是所有地方统一从一个地方获取。Request中获取userId的方法代码示例如下:下面介绍如何使用拦截器方法将userId存储到线程变量中。拦截器方法在passjava-common模块中添加拦截器,获取请求头中的身份信息,并添加到线程变量中。文件名为HeaderInterceptor。使用WebMvcConfigurer注册拦截器。文件名为WebMvcConfig.java。配置文件中需要定义一个配置项:文件名;org.springframework.boot.autoconfigure.AutoConfiguration.imports配置项:com.jackson0714.passjava.common.config.WebMvcConfig然后passjava-member服务引入这个拦截器配置。@Import({WebMvcConfig.class})通过以上两种方法中的任意一种获取到userId后,通过userId查询成员的详细信息。这里要注意,这个用户既是系统用户,又是系统成员。关于查询成员的数据库操作这里不再展开。执行结果如下图所示:如何刷新token还有一个内容是关于如何刷新token。当认证服务返回给客户端的JWT,即access_token过期后,客户端是否通过发送登录请求重新获取access_token?如果这种重新登录操作非常频繁(由于JWT过期时间短),用户体验会很差。客户端需要跳转到登录页面,要求用户重新提交用户名和密码。即使客户端记住了用户名和密码,这种跳转到登录页面的操作也会大大降低用户体验,甚至导致用户不想再次登录。第二次使用。有没有更优雅的方式让客户端重新获取access_token或者延长access_token的有效期?我们知道JWT生成后,里面的内容是不可篡改的,甚至连JWT的有效期都不能。因此,不宜延长access_token的有效期,长期保持access_token有效也不安全。那么只能重新生成access_token了。解决方法其实很简单。客户端使用之前生成的JWT调用后台接口,后台验证JWT是否合法。如果合法,则生成一个新的并将其返回给客户端。客户端可以在本地替换之前保存的access_token。生成access_token和refresh_token这里有一个巧妙的设计,就是在生成JWT的时候,返回两个JWTtoken,一个access_token和一个refresh_token。这两个token其实是可以用来刷新token的,只是我们把refresh_token的过期时间设置的稍微长一些,比如是access_token的两倍,当access_token过期的时候,如果refresh_token还没有过期,就可以利用两者之间的过期时间差两者重新生成token,即刷新token,这里的刷新是指客户端重置本地保存的token,以后使用新的token。饥饿模式和懒惰模式当然,在access_token过期之前,客户端也可以提前刷新token。我把这种提前刷新的模式称为饥饿模式(单例模式也叫这个),而令牌过期后刷新的模式就是我所说的惰性模式。两种模式都可以使用。前者需要客户端定期检查过期时间,增加了复杂度;后者会造成短暂的请求失败,需要获取新的token后才会成功。刷新令牌的操作完全由客户端自己控制,客户端不限于浏览器,也可能是第三方服务。一次性通常,我们会将刷新令牌refresh_token设置为只使用一次,以保证刷新令牌的安全。而这就需要服务端缓存refreshtoken,当使用一次后,会主动从缓存中移除。但这违背了JWT的无状态特性,完全取决于业务需求来决定是否使用这种缓存方式。如下图所示,我在生成token的时候把refreshtoken缓存在了Redis中。当我使用refresh_token调用刷新API时,密钥将被主动删除。当我下次使用同样的refresh_token刷新token时,由于redis中不存在该key,会提示刷新失败。关于cachetoken有两个小问题:有没有办法主动让access_token失效?场景问题:如何保证一个用户只能登录一台设备?小结本文虽然是实践性的内容,但是也涉及到很多理论性的内容,比如网关原理和JWT。结合实战讲解,相信大家对如何使用SpringCloudGateway+JWT实现登录认证有了充分的了解。本篇只讲解鉴权和证书,授权部分没有接触,所以这也是下一篇要讲解的内容,赶快补上吧~最后,不要当妓女,点赞转发吧~