网关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 的范畴。具体实现我就不和大家一一讲解了。

Logo

云原生社区为您提供最前沿的新闻资讯和知识内容

更多推荐