再学习spring cloud系列8-微服务网关安全认证-JWT
网关JWT认证流程
1、网关认证文件的认证流程
目前,结合微服务网关和JWT Token开发用户认证和服务访问认证的主流流程如下:

-
用户认证过程:用户向网关发送登录认证请求,网关将请求转发给认证服务。鉴权服务对用户的登录信息(用户密码、短信和图片验证码)进行验证后,如果验证成功,会向用户发放一个token token(这个token可以是JWT token)
-
网关级访问认证:用户访问系统中其他业务服务接口时,需要携带登录认证时下发的JWT token。网关验证 JWT 令牌的合法性。如果token非法,则返回接口的访问权限不足。如果令牌合法,请求会根据网关的路由规则转发到相应的服务。
-
服务级访问认证:网关级访问认证只是验证JWT token的合法性,初步确定你是系统的用户,但作为系统的用户并不意味着你可以访问所有的服务接口。一般来说,根据用户的角色分类,访问权限的划分更为严格。
令牌的发行和验证需要基于相同的密钥,即JWT令牌的签名和设计必须具有相同的密钥。谜底的答案一定是“塔镇河妖”。如果无法匹配到密钥,则令牌的验证将失败。
因此,网关除了转发请求外,还需要做两件事:一是验证JWT令牌的合法性,二是从JWT令牌中解析出用户身份,并在转发请求时携带用户身份信息.这样,当系统中的其他业务服务收到转发请求时,它们会根据用户的身份信息确定用户可以访问哪些接口。
2、工艺优化方案
从上面的过程我们可以看出
-
令牌由认证服务颁发
-
令牌的验证由网关完成
也就是说,与JWT key相关的基本配置必须在“认证服务”和“网关服务”上都进行配置。这种分散的配置不利于维护和密钥管理。那么我们来优化一下流程:在网关服务网关的服务上开发登录认证功能。优化后的流程如下:

3、学习本章所需的基础知识
从上面的流程可以看出,JWT认证流程的实现并不是很复杂,但是要真正做好服务接口的认证流程,却涉及到很多基础知识。
3.1.在网关上实现登录认证
-
因为gateway网关的基本框架是Spring WebFlux,而不是Spring MVC。所以你需要对 WebFlux 开发有一定的了解。
-
目前Spring WebFlux对关系型数据库响应式编程的支持非常有限。笔者多次测试过mybatis。目前,不得使用。 JPA兼容性比较好。所以你应该有JPA的知识。 (WebFlux不支持MySQL数据库访问的响应式编程,不代表不支持mysql,但依然可以使用MySQL数据库)
3.2. Spring 安全基金会
系统中的其他业务服务收到转发请求后,根据用户的身份信息确定用户可以访问哪些接口。如何实现?你应该具备 Spring Security 和 RBAC 权限管理模型的基础知识
附录——上面的时序图代码
在线序列图编辑工具:https://www.websequencediagrams.com/
用户->+网关:登录请求
网关-->-用户:返回令牌
user->+gateway:携带token接入业务
gateway->gateway:检查token的合法性
网关->+其他服务:转发带有用户身份信息的请求
其他服务-->-网关:返回数据
网关-->-用户:返回数据
登录认证JWT令牌发放
本节要实现的需求是:用户发起登录认证请求,在网关服务上对用户进行认证(用户名和密码),认证成功后将JWT token返回给用户客户端。

实施后的项目结构如下:

1、maven核心依赖
在上一章代码的基础上,添加如下maven依赖
<依赖>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<版本>0.9.0</版本>
</依赖>
<依赖>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</依赖>
<依赖>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</依赖>
<依赖>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</依赖>
<依赖>
<groupId>org.projectlombok</groupId>
<artifactId>龙目岛</artifactId>
<可选>真</可选>
</依赖>
-
jjwt是实现JWT令牌的核心类库
-
Spring boot starter data JPA是一个持久层框架,因为我们需要从数据库中加载用户信息。之所以不用mybatis,是因为目前webFlux下mybatis的兼容性不好。
-
Spring security crypto是spring框架下的加密、解密、签名和解组的通用类库
2、核心控制器
2个核心功能:
-
Authentication 实现登录认证。认证成功后,返回 JWT 令牌
-
refreshtoken 刷新token,将旧token换成新token(因为JWT token有有效期,超过有效期的token是非法的)
注意下面的Mono是WebFlux结果响应数据回调的方法,不是我自定义的。
/**
* JWT 获取token和刷新token接口
*/
@RestController
@ConditionalOnProperty(name u003d "zimug.gateway.jwt.useDefaultController", havingValue u003d "true")
公共类 JwtAuthController {
@资源
私有 JwtProperties jwtProperties;
@资源
私有 SysUserRepository sysUserRepository;
@资源
私有 JwtTokenUtil jwtTokenUtil;
@资源
私人密码编码器密码编码器;
/**
* 交换 JWT 令牌的用户名和密码
*/
@RequestMapping("/认证")
公共 Mono<AjaxResponse> 身份验证(@RequestBody Map<String,String> 映射){
//从请求体中获取用户名和密码
字符串用户名 u003d map.get(jwtProperties.getUserParamName());
字符串密码 u003d map.get(jwtProperties.getPwdParamName());
if(StringUtils.isEmpty(用户名)
|| StringUtils.isEmpty(密码)){
return buildErrorResponse("用户名或密码不能为空");
}
//根据用户名(user Id)在数据库中查找用户
SysUser sysUser u003d sysUserRepository.findByUsername(username);
如果(系统用户!u003d 空){
//将数据库的加密密码与用户的明文密码匹配
boolean isAuthenticated u003d passwordEncoder.matches(password,sysUser.getPassword());
if(isAuthenticated){ //如果匹配成功
//通过jwtTokenUtil生成JWT令牌并返回
返回 buildSuccessResponse(jwtTokenUtil.generateToken(username,null));
} else{ //如果密码匹配失败
return buildErrorResponse("请确保您输入的用户名或密码正确!");
}
}其他{
return buildErrorResponse("请确保您输入的用户名或密码正确!");
}
}
/**
* 刷新 JWT 令牌并用新令牌替换旧令牌
*/
@RequestMapping("/refreshtoken")
public Mono<AjaxResponse> refreshtoken(@RequestHeader("${zimug.gateway.jwt.header}") String oldToken){
if(!jwtTokenUtil.isTokenExpired(oldToken)){
返回 buildSuccessResponse(jwtTokenUtil.refreshToken(oldToken));
}
返回 Mono.empty();
}
私有 Mono<AjaxResponse> buildErrorResponse(字符串消息){
return Mono.create(callback -> callback.success( //成功请求结果的回调
AjaxResponse.error( //响应信息为Error,返回异常信息
新的 CustomException(CustomExceptionType.USER_INPUT_ERROR,消息)
)
));
}
私有 Mono<AjaxResponse> buildSuccessResponse(对象数据){
return Mono.create(callback -> callback.success( //成功请求结果的回调
AjaxResponse.success(data) //响应成功并返回数据
));
}
}
后面会介绍四个核心服务代码类
-
JwtProperties,JWT配置加载类,包括JWT key配置,过期时间等参数配置
-
sys User Repository,数据库sys_JPARepository对应user表。因为这个表是一个用户信息表,里面包含了用户名和密码。
-
jwttil类封装了token操作。核心方法:根据用户id生成JWT token、验证token合法性、刷新token等工具类
-
PasswordEncoder是Spring Security的加解密工具类。核心方法是encode,用于密码加密; Matches 用于密码验证。 (用户注册时使用encode加密,用户登录认证时使用matches验证密码)
3、JwtProperties
需要在网关配置文件中配置以下配置属性。如果未配置,将使用默认值。
@数据
@ConfigurationProperties(前缀 u003d "zimug.gateway.jwt")
@组件
公共类 JwtProperties {
//是否开启JWT,即注入相关类对象
启用私有布尔值;
//JWT 密钥
私有字符串秘密;
//JWT 生效时间
私人多头到期;
//前端向后端传输JWT时,使用HTTP的头部名称,前后端统一
私有字符串头;
//用户登录-用户名参数名
私有字符串 userParamName u003d "用户名";
//用户登录-密码参数名称
私人字符串 pwdParamName u003d "密码";
//使用默认的JWTAuthController
私有布尔 useDefaultController u003d false;
}
齐穆格:
网关:
jwt:
enabled: true #是否开启JWT登录认证功能
secret: fjkfaf;afa # JWT 私钥用于验证 JWT 令牌的合法性
expire: 3600000 #JWT令牌的有效期,用于验证JWT令牌的合法性
header: JWTHeaderName #HTTP请求的Header名称。 Header 将 JWT 令牌作为参数传递
userParamName: username #用户登录认证用户名参数名
pwdParamName:password #用户认证密码名称
useDefaultController: true # 使用默认的JwtAuthController
这些配置会影响代码中程序的组件加载和运行逻辑。例如,当 ConditionalOnProperty - zimug 网关。 jwt。 JwtAuthController 类的 Bean 只有在 usedefaultcontroller u003d true 时才会被初始化。这样做的目的是,我计划将来的网关不仅支持 JWT,还支持 OAuth,以避免冲突或冗余。我们添加开关来影响 bean 的初始化行为。
! zoz100077](https://programming.vip/images/doc/dcacf697cf2bdeb9e985e5e14ae18502.jpg)
4、SysUserRepository
sysUser实体类对应数据库的sys_user表,根据JPA规则定义
@数据
@AllArgs 构造函数
@NoArgs构造函数
@实体
@Table(nameu003d"sys_user")
公共类 SysUser {
@身份证
@GeneratedValue(策略 u003d GenerationType.IDENTITY)
私人字符串用户名;
@专栏
私人字符串密码;
@专栏
私有整数 orgId;
@专栏
启用私有布尔值;
@专栏
私人字符串电话;
@专栏
私人字符串电子邮件;
@专栏
私人日期创建时间;
}
根据 sys_user 表的用户名字段,用于查询 SysUser 用户信息。
公共接口 SysUserRepository 扩展 JpaRepository<SysUser,Long> {
//注意这个方法的名字。 jPA会根据方法名自动生成SQL执行,不用自己写SQL
SysUser findByUsername(String username);
}
jpa和数据源相关配置需要添加到配置文件中
弹簧:
数据源:
网址:jdbc:mysql://ip:3306/linnadb?useUnicodeu003dtrue&characterEncodingu003dutf-8&useSSLu003dfalse
用户名:
密码:
驱动程序类名:com.mysql.cj.jdbc.Driver
jpa:
休眠:
ddl-auto:验证
数据库:mysql
显示-sql: 真
5、密码编码器
我们需要使用PasswordEncoder来设计和验证密码,所以初始化一个Bean:BCryptPasswordEncoder。需要注意的是,我们使用 BCryptPasswordEncoder 匹配设计的前提是用户注册时存储在数据库中的密码也是通过 BCryptPasswordEncoder 编码加密的。

6、JwtTokenUtil
基于 jsonwebtoken jjwt 类库、工具类的 Io Code 封装。
@组件
公共类 JwtTokenUtil {
@资源
私有 JwtProperties jwtProperties;
/**
* 生成令牌令牌
*
* @param userId 用户ID或用户名
* @param payloads 令牌中携带的附加信息
* @return 令牌卡
*/
公共字符串生成令牌(字符串用户 ID,
Map<String,String> 有效载荷){
int 有效载荷大小 u003d 有效载荷 u003du003d 空? 0 : 有效载荷.size();
Map<String, Object> 声明 u003d new HashMap<>(payloadSizes + 2);
claim.put("sub", userId);
claims.put("创建", new Date());
如果(有效载荷大小 > 0){
for(Map.Entry<String,String> entry:payloads.entrySet()){
声明.put(entry.getKey(),entry.getValue());
}
}
返回生成令牌(声明);
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
公共字符串 getUsernameFromToken(字符串令牌) {
字符串用户名;
试试 {
声明声明 u003d getClaimsFromToken(token);
用户名 u003d 声明.getSubject();
} 捕捉(异常 e){
用户名 u003d 空;
}
返回用户名;
}
/**
* 判断token是否过期
*
* @param token 令牌
* @return 已过期
*/
public Boolean isTokenExpired(String token) {
试试 {
声明声明 u003d getClaimsFromToken(token);
到期日期 u003d claim.getExpiration();
return expire.before(new Date());
} 捕捉(异常 e){
//验证JWT签名失败相当于token过期
返回真;
}
}
/**
* 刷新令牌
*
* @param 令牌 原始令牌
* @return 新令牌
*/
公共字符串刷新令牌(字符串令牌){
字符串刷新令牌;
试试 {
声明声明 u003d getClaimsFromToken(token);
claims.put("创建", new Date());
refreshedToken u003d generateToken(声明);
} 捕捉(异常 e){
刷新令牌 u003d 空;
}
返回刷新的令牌;
}
/**
* 身份验证令牌
*
* @param token 令牌
* @param userId 用户ID 用户名
* @return 是否有效
*/
公共布尔验证令牌(字符串令牌,字符串 userId){
字符串用户名 u003d getUsernameFromToken(token);
return (username.equals(userId) && !isTokenExpired(token));
}
/**
* 从声明中生成一个令牌。看不懂就看谁叫了
*
* @param 声明数据声明
* @return 令牌
*/
私有字符串 generateToken(Map<String, Object> 声明) {
日期过期日期 u003d 新日期(System.currentTimeMillis() + jwtProperties.getExpiration());
返回 Jwts.builder().setClaims(声明)
.setExpiration(过期日期)
.signWith(SignatureAlgorithm.HS512, jwtProperties.getSecret())
.compact();
}
/**
* 从token中获取数据声明并验证JWT签名
*
* @param token 令牌
* @return 数据声明
*/
私人声明 getClaimsFromToken(字符串令牌){
索赔索赔;
试试 {
声明 u003d Jwts.parser().setSigningKey(jwtProperties.getSecret()).parseClaimsJws(token).getBody();
} 捕捉(异常 e){
声明 u003d 空;
}
退货索赔;
}
}
7、访问测试
本机启动网关进行http://127.0.0.1:8777/authentication 登录认证,返回如下结果,说明我们的实现是ok的。

刷新测试令牌

带全局过滤器的 JWT 认证

在上一节中,我们已经实现了用户登录认证。如果认证成功,用户会返回一个token给用户客户端,即JWT。本节需要从网关分析 JWT 服务的合法性,并从 JWT 服务转发其他用户的信息。
1、带全局过滤器的JWT认证
对于Gateway的所有请求,都必须验证JWT的合法性(“/认证”除外),所以使用Gateway全局过滤器GlobalFilter最为合适。根据上一节的代码添加全局过滤器
@配置
公共类 JWTAuthCheckFilter {
@资源
私有 JwtProperties jwtProperties;
@资源
私有 JwtTokenUtil jwtTokenUtil;
@豆
@Order(-101)
公共全局过滤器 jwtAuth 全局过滤器()
{
返回(交换,链)-> {
ServerHttpRequest serverHttpRequest u003d exchange.getRequest();
ServerHttpResponse serverHttpResponse u003d exchange.getResponse();
String requestUrl u003d serverHttpRequest.getURI().getPath();
if(!requestUrl.equals("/authentication")){
//从HTTP请求头中获取JWT令牌
字符串 jwtToken u003d serverHttpRequest
.getHeaders()
.getFirst(jwtProperties.getHeader());
//设计Token并验证Token是否过期
boolean isJwtValid u003d jwtTokenUtil.isTokenExpired(jwtToken);
if(isJwtNotValid){ //如果JWT令牌不合法
return writeUnAuthorizedMessageAsJson(serverHttpResponse,"请先登录再访问服务!");
}
//从JWT中解析出当前用户的身份(userId),继续执行过滤链并转发请求
ServerHttpRequest mutableReq u003d serverHttpRequest
.mutate()
.header("userId", jwtTokenUtil.getUsernameFromToken(jwtToken))
.build();
ServerWebExchange mutableExchange u003d exchange.mutate().request(mutableReq).build();
返回链过滤器(可变交换);
}else{ //如果是登录认证请求,不做JWT权限验证直接执行
返回链过滤器(交换);
}
};
}
//向客户端响应JWT认证失败的消息
私有 Mono<Void> writeUnAuthorizedMessageAsJson(ServerHttpResponse serverHttpResponse,String message) {
serverHttpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
serverHttpResponse.getHeaders().add("Content-Type", "application/json;charsetu003dUTF-8");
AjaxResponse ajaxResponse u003d AjaxResponse.error(CustomExceptionType.USER_INPUT_ERROR,message);
DataBuffer dataBuffer u003d serverHttpResponse.bufferFactory()
.wrap(JSON.toJSONStringWithDateFormat(ajaxResponse,JSON.DEFFAULT_DATE_FORMAT)
.getBytes(StandardCharsets.UTF_8));
return serverHttpResponse.writeWith(Flux.just(dataBuffer));
}
}
过滤器核心代码做了两件事
-
验证JWT的合法性。非法请求将被直接退回,不会转发。
-
如果JWT合法,则从JWT中解析出userId(用户身份信息),放入HTTP头中。 (下一节会用到网关后面的服务)

请结合以上注释了解全局JWT认证的实现。如果难以理解,结合下面的测试过程来理解上面的代码。
2、测试
- 无 JWT 令牌访问 http://127.0.0.1:8777/sysuser/pwd/reset 。

- 登录 http://127.0.0.1:8777/authentication ,获取 JWT token

- 将JWT token 添加到http://127.0.0.1:8777/sysuser/pwd/reset 在访问请求的Header中,再次发起请求
! swz 100101 swz 100102 swz 100100
给出结果如下
! swz 100104 swz 100105 swz 100103
我们修改一下 JWT 令牌字符串,再次访问 http://127.0.0.1:8777/sysuser/pwd/reset ,结果如下:
! swz 100107 swz 100108 swz 100106
微服务内部权限管理
1、再看流程
! swz 100110 swz 100111 swz 100109
按照上面的流程,我们就完成了
-
在网关上开发了登录认证功能。登录认证后,用户将JWT token返回给客户端
-
在网关上创建了一个全局过滤器。当请求发送到网关时,过滤器会验证 JWT 令牌的合法性。只有合法的令牌请求才会被转发到特定的业务服务。在过滤器中,我们解析 JWT 令牌中的 userId(用户身份信息),并将其传递给网关后面的服务。

其他服务分为两种:
-
一项服务对系统内所有用户开放,即用户通过网关的JWT认证后,服务本身将不再限制用户的权限,所有接口均可访问。
-
另一个服务有权限要求,比如根据你的角色判断你是否有权限访问某些接口。例如,作为系统管理员用户,可以访问“系统日志”、“系统管理”等功能界面;作为系统操作员,您只能访问部分业务操作界面。
3、微服务内的权限管理
已知:我们可以得到userId(用户身份信息),其他的我们什么都不知道。我们可以使用 RBAC 权限模型来管理用户权限。

-
用户信息可以通过查询userId获取
-
可根据用户信息查询角色信息(一个用户有多个角色)
-
根据角色信息可以找到接口权限信息(一个角色有多个权限)
最终服务通过userId(用户身份信息)获取用户可以访问的接口权限列表X。用户正在访问的接口在X列表中,表示用户可以访问该接口,否则没有权限。
数据库模型
我们可以用下图中的数据库设计模型来描述这样的关系。
-
一个用户有一个或多个角色
-
一个角色包含多个用户
-
一个角色有多个权限
-
一个权限属于多个角色

-
sys_user为用户信息表,用于存放用户的基本信息,如用户名和密码
-
sys_role是一个角色信息表,用来存放系统中的所有角色
-
sys_menu是系统的菜单信息表,用于存放系统中的所有菜单。维护一个菜单树结构,其中包含 id 和 parent id 之间的字段关系。
-
sys_user_role 是一个用户角色多对多关系表。 userid 和 roleid 之间的关系记录表明该用户具有该角色,并且该角色包含该用户。
-
sys_role_menu 为角色菜单(权限)关系表。 roleid 和 menuid 的关系记录表明该角色是由一个菜单授权的,并且该菜单权限可以被一个角色访问。
你可以结合Spring Security、shiro来实现这个过程,也可以不用任何框架来判断实现。微服务内部的权限管理知识已经超出了 Spring Cloud 的范畴。具体实现我就不和大家一一讲解了。
更多推荐
所有评论(0)