我们本节要实现的需求是:用户发起登录认证请求,网关服务上对该用户进行认证(用户名密码),认证成功之后将JWT令牌返回给用户客户端。

实现完成之后的项目结构如下:

## 一、maven核心依赖
在上一章代码的基础上,加上如下的一些maven依赖
~~~
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
~~~
* jjwt是实现JWT 令牌的核心类库
* spring-boot-starter-data-jpa是持久层框架,因为我们需要去数据库加载用户信息。之所以不用mybatis,因为webFlux下mybatis目前兼容性不好。
* spring-security-crypto是Spring 框架下进行加密解密、加签解签操作的常用类库
## 二、核心Controller
2个核心函数:
* authentication实现登录认证,认证成功之后返回JWT令牌
* refreshtoken实现令牌刷新,使用旧的令牌换取新的令牌(因为JWT令牌是有有效期的,超过有效期令牌非法)
> 注意下文中的Mono是WebFlux结果响应数据回调的做法,不是我的自定义。
~~~
/**
* JWT获取令牌和刷新令牌接口
*/
@RestController
@ConditionalOnProperty(name = "zimug.gateway.jwt.useDefaultController", havingValue = "true")
public class JwtAuthController {
@Resource
private JwtProperties jwtProperties;
@Resource
private SysUserRepository sysUserRepository;
@Resource
private JwtTokenUtil jwtTokenUtil;
@Resource
private PasswordEncoder passwordEncoder;
/**
* 使用用户名密码换JWT令牌
*/
@RequestMapping("/authentication")
public Mono<AjaxResponse> authentication(@RequestBody Map<String,String> map){
//从请求体中获取用户名密码
String username = map.get(jwtProperties.getUserParamName());
String password = map.get(jwtProperties.getPwdParamName());
if(StringUtils.isEmpty(username)
|| StringUtils.isEmpty(password)){
return buildErrorResponse("用户名或者密码不能为空");
}
//根据用户名(用户Id)去数据库查找该用户
SysUser sysUser = sysUserRepository.findByUsername(username);
if(sysUser != null){
//将数据库的加密密码与用户明文密码match
boolean isAuthenticated = passwordEncoder.matches(password,sysUser.getPassword());
if(isAuthenticated){ //如果匹配成功
//通过jwtTokenUtil生成JWT令牌并return
return buildSuccessResponse(jwtTokenUtil.generateToken(username,null));
} else{ //如果密码匹配失败
return buildErrorResponse("请确定您输入的用户名或密码是否正确!");
}
}else{
return buildErrorResponse("请确定您输入的用户名或密码是否正确!");
}
}
/**
* 刷新JWT令牌,用旧的令牌换新的令牌
*/
@RequestMapping("/refreshtoken")
public Mono<AjaxResponse> refreshtoken(@RequestHeader("${zimug.gateway.jwt.header}") String oldToken){
if(!jwtTokenUtil.isTokenExpired(oldToken)){
return buildSuccessResponse(jwtTokenUtil.refreshToken(oldToken));
}
return Mono.empty();
}
private Mono<AjaxResponse> buildErrorResponse(String message){
return Mono.create(callback -> callback.success( //请求结果成功的回调
AjaxResponse.error( //响应信息是Error的,携带异常信息返回
new CustomException(CustomExceptionType.USER_INPUT_ERROR, message)
)
));
}
private Mono<AjaxResponse> buildSuccessResponse(Object data){
return Mono.create(callback -> callback.success( //请求结果成功的回调
AjaxResponse.success(data) //成功响应,携带数据返回
));
}
}
~~~
四个核心服务代码类,后文会介绍
* JwtProperties,JWT配置加载类,包含JWT密钥配置、过期时间等参数配置
* SysUserRepository,数据库sys\_user表对应的JPA Repository。因为该表是用户信息表,包含用户名密码。
* JwtTokenUtil,JWT令牌操作工具封装类。核心方法如:根据用户id生成JWT令牌,校验令牌合法性,刷新令牌等工具类
* PasswordEncoder,是Spring Security的加解密工具类。核心方法是encode用于密码加密;matches用于密码校验。(在用户注册的时候用encode加密,在用户登录认证的时候用matches进行密码校验)
## 三、 JwtProperties
以下的这些配置属性,需要在gateway的配置文件中配置,不配置的话将使用默认值。
~~~
@Data
@ConfigurationProperties(prefix = "zimug.gateway.jwt")
@Component
public class JwtProperties {
//是否开启JWT,即注入相关的类对象
private Boolean enabled;
//JWT密钥
private String secret;
//JWT有效时间
private Long expiration;
//前端向后端传递JWT时使用HTTP的header名称,前后端要统一
private String header;
//用户登录-用户名参数名称
private String userParamName = "username";
//用户登录-密码参数名称
private String pwdParamName = "password";
//是否使用默认的JWTAuthController
private Boolean useDefaultController = false;
}
~~~
~~~
zimug:
gateway:
jwt:
enabled: true #是否开启JWT登录认证功能
secret: fjkfaf;afa # JWT私钥,用于校验JWT令牌的合法性
expiration: 3600000 #JWT令牌的有效期,用于校验JWT令牌的合法性
header: JWTHeaderName #HTTP请求的Header名称,该Header作为参数传递JWT令牌
userParamName: username #用户登录认证用户名参数名称
pwdParamName: password #用户登录认证密码参数名称
useDefaultController: true # 是否使用默认的JwtAuthController
~~~
这些配置在代码中会影响程序的组件加载及运行逻辑,比如:当ConditionalOnProperty---`zimug.gateway.jwt.useDefaultController=true`的时候,才初始化JwtAuthController 这个类的Bean。这样做的目的是,我规划的gateway未来不仅支持JWT还支持OAuth,为了避免二者冲突或者冗余。我们加上开关去影响Bean的初始化行为。

## 四、SysUserRepository
SysUser 实体类对应数据库的sys\_user表,遵循JPA规则定义
~~~
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name="sys_user")
public class SysUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private String username;
@Column
private String password;
@Column
private Integer orgId;
@Column
private Boolean enabled;
@Column
private String phone;
@Column
private String email;
@Column
private Date createTime;
}
~~~
根据sys\_user表的username字段去查询SysUser用户信息。
~~~
public interface SysUserRepository extends JpaRepository<SysUser,Long> {
//注意这个方法的名称,jPA会根据方法名自动生成SQL执行,完全不用自己写SQL
SysUser findByUsername(String username);
}
~~~
需要在配置文件中加入jpa及数据源相关的配置
~~~
spring:
datasource:
url: jdbc:mysql://ip:3306/linnadb?useUnicode=true&characterEncoding=utf-8&useSSL=false
username:
password:
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate
database: mysql
show-sql: true
~~~
## 五、PasswordEncoder
我们需要通过PasswordEncoder进行密码的解签名校验,所以初始化一个Bean:BCryptPasswordEncoder。需要注意的是:我们使用BCryptPasswordEncoder.matches解签名的前提是,用户注册的时候存放到数据库里面的password也是经过BCryptPasswordEncoder.encode加密的。

## 六、JwtTokenUtil
基于io.jsonwebtoken-jjwt类库的代码封装,工具类。
~~~
@Component
public class JwtTokenUtil {
@Resource
private JwtProperties jwtProperties;
/**
* 生成token令牌
*
* @param userId 用户Id或用户名
* @param payloads 令牌中携带的附加信息
* @return 令token牌
*/
public String generateToken(String userId,
Map<String,String> payloads) {
int payloadSizes = payloads == null? 0 : payloads.size();
Map<String, Object> claims = new HashMap<>(payloadSizes + 2);
claims.put("sub", userId);
claims.put("created", new Date());
if(payloadSizes > 0){
for(Map.Entry<String,String> entry:payloads.entrySet()){
claims.put(entry.getKey(),entry.getValue());
}
}
return generateToken(claims);
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
//验证JWT签名失败等同于令牌过期
return true;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put("created", new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 验证令牌
*
* @param token 令牌
* @param userId 用户Id用户名
* @return 是否有效
*/
public Boolean validateToken(String token, String userId) {
String username = getUsernameFromToken(token);
return (username.equals(userId) && !isTokenExpired(token));
}
/**
* 从claims生成令牌,如果看不懂就看谁调用它
*
* @param claims 数据声明
* @return 令牌
*/
private String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + jwtProperties.getExpiration());
return Jwts.builder().setClaims(claims)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, jwtProperties.getSecret())
.compact();
}
/**
* 从令牌中获取数据声明,验证JWT签名
*
* @param token 令牌
* @return 数据声明
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(jwtProperties.getSecret()).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
}
~~~
## 七、访问测试
本机启动网关,进行`http://127.0.0.1:8777/authentication`登录认证,返回如下结果说明我们的实现是ok的。

测试令牌的刷新

- 文档简介
- 模块与代码分支说明
- dongbb-cloud项目核心架构
- 微服务架构进化论
- SpringBoot与Cloud选型兼容
- Spring Cloud组件的选型
- 单体应用拆分微服务
- 单体应用与微服务对比
- 微服务设计拆分原则
- 新建父工程及子模块框架
- 通用微服务初始化模块构建
- 持久层模块单独拆分
- 拆分rbac权限管理微服务
- Hello-microservice
- 构建eureka服务注册中心
- 向服务注册中心注册服务
- 第一个微服务调用
- 远程服务调用
- HttpClient远程服务调用
- RestTemplate远程服务调用
- RestTemplate多实例负载均衡
- Ribbon调用流程源码解析
- Ribbon负载均衡策略源码解析
- Ribbon重试机制与饥饿加载
- Ribbon自定义负载均衡策略
- Feign与OpenFeign
- Feign设计原理源码解析
- Feign请求压缩与超时等配置
- 服务注册与发现
- 白话服务注册与发现
- DiscoveryClient服务发现
- Eureka集群环境构建(linux)
- Eureka集群多网卡环境ip设置
- Eureka集群服务注册与安全认证
- Eureka自我保护与健康检查
- 主流服务注册中心对比(含nacos)
- zookeeper概念及功能简介
- zookeeper-linux集群安装
- zookeeper服务注册与发现
- consul概念及功能介绍
- consul-linux集群安装
- consul服务注册与发现
- 通用-auatator导致401问题
- 分布式配置中心-apollo
- 服务配置中心概念及使用场景
- apollo概念功能简介
- apollo架构详解
- apollo分布式部署之Portal
- apollo分布式部署之环境区分
- apollo项目权限管理实战
- apollo-java客户端基础
- apollo与SpringCloud服务集成
- apollo实例配置热更新
- apollo命名空间与集群
- apollo灰度发布(日志热更新为例)
- SpringCloudConfig配置中心
- config-git配置文件仓库
- config配置中心搭建与测试
- config客户端基础
- config配置安全认证
- config客户端配置刷新
- config配置中心高可用
- BUS消息总线
- bus消息总线简介
- docker安装rabbitMQ
- 基于rabbitMQ的消息总线
- bus实现批量配置刷新
- alibaba-nacos
- nacos介绍与单机部署
- nacos集群部署方式(linux)
- nacos服务注册与发现
- nacos服务注册中心详解
- nacos客户端配置加载
- nacos客户端配置刷新
- nacos服务配置隔离与共享
- nacos配置Beta发布
- 服务熔断降级hystrix
- 服务降级&熔断&限流
- Hystrix集成并实现服务熔断
- Jemter模拟触发服务熔断
- Hystrix服务降级fallback
- Hystrix结合Feign服务降级
- 远程服务调用异常传递的问题
- Hystrix-Feign异常拦截与处理
- Hystrix-DashBoard单服务监控
- Hystrix-dashboard集群监控
- 分布式系统流量卫兵sentinel
- sentinel简介与安装
- 客户端集成与实时监控
- 实战流控规则-QPS限流
- 实战流控规则-线程数限流
- 实战流控规则-关联限流
- 实战流控规则-链路限流
- 实战流控效果-WarmUp
- 实战流控效果-匀速排队
- BlockException处理
- 实战熔断降级-RT
- 实战熔断降级-异常数与比例
- DegradeException处理
- 注解与异常的归纳总结
- Feign降级及异常传递拦截
- 动态规则nacos集中存储
- 热点参数限流
- 系统自适应限流
- 微服务网关-GateWay
- 还有必要学习Zuul么?
- 简介与非阻塞异步IO模型
- GateWay概念与流程
- 新建一个GateWay项目
- 通用Predicate的使用
- 自定义PredicateFactory
- 编码方式构建静态路由
- Filter过滤器介绍与使用
- 自定义过滤器Filter
- 网关请求转发负载均衡
- 结合nacos实现动态路由配置
- 整合Sentinel实现资源限流
- 跨域访问配置
- 网关层面全局异常处理
- 微服务网关安全认证-JWT篇
- Gateway-JWT认证鉴权流程
- 登录认证JWT令牌颁发
- 全局过滤器实现JWT鉴权
- 微服务自身内部的权限管理
