1. 概述

老话说的好:善待他人就是善待自己,虽然可能有所付出,但也能得到应有的收获。

言归正传,之前我们聊了 Gateway 组件,今天来聊一下如何使用 JWT 技术给用户授权,以及如果在 Gateway 工程使用自定义 filter 验证用户权限。

闲话不多说,直接上代码。

2. 开发 授权鉴权服务接口层 my-auth-api

2.1 主要依赖

    <artifactId>my-auth-api</artifactId>

    <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>

2.2 实体类

/**
* 账户实体类
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Account implements java.io.Serializable { // 用户名
private String userName; // token
private String token; // 刷新token
private String refreshToken;
}
/**
* 响应实体类
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthResponse implements java.io.Serializable { // 账户
private Account account; // 响应码
private Integer code;
}

2.3 授权鉴权 Service 接口

/**
* 授权鉴权 Service 接口
*/
@FeignClient("my-auth-service")
public interface AuthService { /**
* 登录接口
* @param userName 用户名
* @param password 密码
* @return
*/
@PostMapping("/login")
AuthResponse login(@RequestParam("userName") String userName,
@RequestParam("password") String password); /**
* 校验token
* @param token token
* @param userName 用户名
* @return
*/
@GetMapping("/verify")
AuthResponse verify(@RequestParam("token") String token,
@RequestParam("userName") String userName); /**
* 刷新token
* @param refreshToken 刷新token
*/
@PostMapping("/refresh")
AuthResponse refresh(@RequestParam("refreshToken") String refreshToken);
}

3. 开发 授权鉴权服务 my-auth-service

3.1 主要依赖

        <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.2</version>
</dependency> <dependency>
<groupId>cn.zhuifengren</groupId>
<artifactId>my-auth-api</artifactId>
<version>${project.version}</version>
</dependency>

3.2 主要配置

server:
port: 45000
spring:
application:
name: my-auth-service
redis:
database: 0
host: 192.168.1.22
port: 6379
password: zhuifengren eureka:
client:
service-url:
defaultZone: http://zhuifengren1:35000/eureka/,http://zhuifengren2:35001/eureka/ # Eureka Server的地址

3.3 启动类添加注解

@SpringBootApplication
@EnableDiscoveryClient

3.4 JWT 核心Service方法

   /**
* 获得 token
* @param account 账户实体
* @return
*/
public String token(Account account) { log.info("获取token"); Date now = new Date(); // 指定算法,KEY是自定义的秘钥
Algorithm algorithm = Algorithm.HMAC256(KEY); // 生成token
String token = JWT.create()
.withIssuer(ISSUER) // 发行人,自定义
.withIssuedAt(now)
.withExpiresAt(new Date(now.getTime() + TOKEN_EXPIRES)) // 设置token过期时间
.withClaim("userName", account.getUserName()) // 自定义属性
.sign(algorithm); log.info(account.getUserName() + " token 生成成功");
return token;
} /**
* 验证token
* @param token
* @param userName
* @return
*/
public boolean verify(String token, String userName) { log.info("验证token"); try {
// 指定算法,KEY是自定义的秘钥
Algorithm algorithm = Algorithm.HMAC256(KEY); // 验证token
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUER) // 发行人,自定义
.withClaim("userName", userName) // 自定义属性
.build(); verifier.verify(token); return true;
} catch (Exception ex) {
log.error("验证失败", ex);
return false;
}
}

3.5 授权鉴权业务Service

/**
* 授权鉴权 Service
*/
@RestController
@Slf4j
public class AuthServiceImpl implements AuthService { @Autowired
private JwtService jwtService; @Autowired
private RedisTemplate redisTemplate; /**
* 登录
* @param userName 用户名
* @param password 密码
* @return
*/
public AuthResponse login(@RequestParam("userName") String userName,
@RequestParam("password") String password) { Account account = Account.builder()
.userName(userName)
.build(); String token = jwtService.token(account); account.setToken(token);
account.setRefreshToken(UUID.randomUUID().toString()); redisTemplate.opsForValue().set(account.getRefreshToken(), account); return AuthResponse.builder()
.account(account)
.code(200) // 200 代表成功
.build();
} /**
* 刷新token
* @param refreshToken 刷新token
* @return
*/
public AuthResponse refresh(@RequestParam("refreshToken") String refreshToken) { Account account = (Account)redisTemplate.opsForValue().get(refreshToken);
if(account == null) {
return AuthResponse.builder()
.code(-1) // -1 代表用户未找到
.build();
} String newToken = jwtService.token(account);
account.setToken(newToken);
account.setRefreshToken(UUID.randomUUID().toString()); redisTemplate.delete(refreshToken);
redisTemplate.opsForValue().set(account.getRefreshToken(), account); return AuthResponse.builder()
.account(account)
.code(200) // 200 代表成功
.build();
} /**
* 验证token
* @param token token
* @param userName 用户名
* @return
*/public AuthResponse verify(@RequestParam("token") String token,
@RequestParam("userName") String userName) { log.info("verify start");
boolean isSuccess = jwtService.verify(token, userName); log.info("verify result:" + isSuccess); return AuthResponse.builder()
.code(isSuccess ? 200 : -2) // -2 代表验证不通过
.build();
}
}

4. 在网关层(Gateway工程)添加鉴权过滤器

4.1 增加依赖

        <dependency>
<groupId>cn.zhuifengren</groupId>
<artifactId>my-auth-api</artifactId>
<version>${project.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency> <dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>

4.2 启动类增加注解

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(clients = AuthService.class)

4.3 鉴权过滤器

@Slf4j
@Component
public class AuthFilter implements GatewayFilter, Ordered { private static final String AUTH = "Authorization"; private static final String USER_NAME = "userName"; @Autowired
private AuthService authService; @Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("开始验证"); // 从 header 中得到 token 和 用户名
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
String token = headers.getFirst(AUTH);
String userName= headers.getFirst(USER_NAME); ServerHttpResponse response = exchange.getResponse(); if(StringUtils.isBlank(token)) {
log.error("token没有找到");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
} // 验证用户名
log.info("执行验证方法");
AuthResponse resp = authService.verify(token, userName);
log.info("执行验证方法完毕");

if(resp == null || resp.getCode() != 200) {
log.error("无效的token");
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
} return chain.filter(exchange);
} @Override
public int getOrder() {
return 0;
}
}

4.4 在路由规则中配置鉴权过滤器

这里我们随便找一个接口实验

@Configuration
public class GatewayConfig { @Bean
@Order
public RouteLocator myRoutes(RouteLocatorBuilder builder, AuthFilter authFilter) { return builder.routes()
.route(r -> r.path("/business/**")
.and()
.method(HttpMethod.GET)
.filters(f -> f.stripPrefix(1) .filter(authFilter) )
.uri("lb://MY-EUREKA-CLIENT"))
.build();
}
}

4.5 block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3 错误解决

此时,启动 Gateway 工程,调用实验接口:

GET  http://Gateway IP:端口/business/eurekaClient/hello

此时 Gateway 工程会报如下错误:

java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83) ~[reactor-core-3.4.11.jar:3.4.11]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ HTTP GET "/business/eurekaClient/hello" [ExceptionHandlingWebHandler]

这是因为在自定义过滤器 AuthFilter 的 filter 方法中,不能同步的调用 Feign 接口,需要异步去调。

我们修改 AuthFilter 中的代码

将 AuthResponse resp = authService.verify(token, userName); 这行代码改为如下代码:

CompletableFuture<AuthResponse> completableFuture = CompletableFuture.supplyAsync
(()-> { return authService.verify(token, userName);
}); AuthResponse resp = null;
try {
resp = completableFuture.get();
} catch (Exception ex) {
log.error("调用验证接口错误", ex);
}

4.6 feign.codec.DecodeException: No qualifying bean of type 'org.springframework.boot.autoconfigure.http.HttpMessageConverters' available 错误解决

我们重启 Gateway 服务,再次调用实验接口:

GET  http://Gateway IP:端口/business/eurekaClient/hello

此时 Feign 接口调通了,但 Gateway 工程报了如下错误:

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.boot.autoconfigure.http.HttpMessageConverters' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1790) ~[spring-beans-5.3.12.jar:5.3.12]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1346) ~[spring-beans-5.3.12.jar:5.3.12]
at org.springframework.beans.factory.support.DefaultListableBeanFactory$DependencyObjectProvider.getObject(DefaultListableBeanFactory.java:1979) ~[spring-beans-5.3.12.jar:5.3.12]

似乎是 HttpMessageConverters 这个 Bean 没有找到,经查阅资料,我们在启动类中添加如下代码

    @Bean
@ConditionalOnMissingBean
public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
}

4.7 实验授权鉴权

1)再次重启 Gateway 工程

2)调用登录接口获取 token

POST http://Gateway IP:端口/my-auth-service/login?userName=zhangsan&password=12345

3)调用业务接口,将 token 和用户名放到 header 中,可以正常访问接口

5. 综述

今天聊了一下 JWT用户鉴权,希望可以对大家的工作有所帮助。

欢迎帮忙点赞、评论、转发、加关注 :)

关注追风人聊Java,每天更新Java干货。

6. 个人公众号

追风人聊Java,欢迎大家关注

AuthFilter

SpringCloud 2020.0.4 系列之 JWT用户鉴权的更多相关文章

  1. SpringCloud 2020.0.4 系列之 Feign

    1. 概述 老话说的好:任何问题都有不止一种的解决方法,当前的问题没有解决,只是还没有发现解决方法,而并不是无解. 言归正传,之前我们聊了 SpringCloud 的服务治理组件 Eureka,今天我 ...

  2. SpringCloud 2020.0.4 系列之 Stream 延迟消息 的实现

    1. 概述 老话说的好:对待工作要有责任心,不仅要完成自己的部分,还要定期了解整体的进展. 言归正传,我们在开发产品时,常常会遇到一段时间后检查状态的场景,例如:用户下单场景,如果订单生成30分钟后, ...

  3. SpringCloud 2020.0.4 系列之 Stream 消息广播 与 消息分组 的实现

    1. 概述 老话说的好:事情太多,做不过来,就先把事情记在本子上,然后理清思路.排好优先级,一件一件的去完成. 言归正传,今天我们来聊一下 SpringCloud 的 Stream 组件,Spring ...

  4. SpringCloud 2020.0.4 系列之 Stream 消息出错重试 与 死信队列 的实现

    1. 概述 老话说的好:出错不怕,怕的是出了错,却不去改正.如果屡次出错,无法改对,就先记下了,然后找援军解决. 言归正传,今天来聊一下 Stream 组件的 出错重试 和 死信队列. RabbitM ...

  5. spring cloud jwt用户鉴权及服务鉴权

    用户鉴权 客户端请求服务时,根据提交的token获取用户信息,看是否有用户信息及用户信息是否正确 服务鉴权 微服务中,一般有多个服务,服务与服务之间相互调用时,有的服务接口比较敏感,比如资金服务,不允 ...

  6. SpringCloud 2020.0.4 系列之Eureka

    1. 概述 老话说的好:遇见困难,首先要做的是积极的想解决办法,而不是先去泄气.抱怨或生气. 言归正传,微服务是当今非常流行的一种架构方式,其中 SpringCloud 是我们常用的一种微服务框架. ...

  7. SpringCloud 2020.0.4 系列之服务降级

    1. 概述 老话说的好:做人要正直,做事要正派,胸怀坦荡.光明磊落,才会赢得他人的信赖与尊敬. 言归正传,之前聊了服务间通信的组件 Feign,今天我们来聊聊服务降级. 服务降级简单的理解就是给一个备 ...

  8. SpringCloud 2020.0.4 系列之 Bus

    1. 概述 老话说的好:会休息的人才更会工作,身体是革命的本钱,身体垮了,就无法再工作了. 言归正传,之前我们聊了 SpringCloud 的 分布式配置中心 Config,文章里我们聊了config ...

  9. SpringCloud 2020.0.4 系列之 Gateway入门

    1. 概述 老话说的好:做人要有幽默感,懂得幽默的人才会活的更开心. 言归正传,今天我们来聊聊 SpringCloud 的网关组件 Gateway,之前我们去访问 SpringCloud 不同服务的接 ...

随机推荐

  1. ecshop调用指定广告的方法

    在include/lib_goods.php文件下面新增:function getads($cat,$num){ $time = gmtime();$sql = "SELECT * FROM ...

  2. Jmeter系列(14)- Setup与tearDown线程组

    与普通线程组区别 #Setup线程组:在普通线程组执⾏前触发 #tearDown线程组:在普通线程组执⾏后触发 线程组属性配置详情完全⼀致 使⽤策略建议 #Setup 线程组 – 压测执⾏准备阶段,准 ...

  3. centos查找大文件

    首先到相当的目录下面,按下面方式查找 find . -type f -size +800M  -print0 | xargs -0 ls -lah或者从根目录(/)开始查找find / -type f ...

  4. YbtOJ#903-染色方案【拉格朗日插值,NTT,分治】

    正题 题目链接:https://www.ybtoj.com.cn/contest/115/problem/3 题目大意 两个长度为\(n+1\)的序列\(a,b\) \(a_i\)表示涂了\(i\)个 ...

  5. 计算机网络-4-2-ARP地址解析协议以及IP数据报不可变组成部分

    地址解析协议ARP ​ 在实际的应用中,我们会经常遇见这样的一个问题:我们已知一个机器(主机或者路由器的),我们怎么获取相应的硬件地址?,地址解析协议就是用来解决这个问题的. ARP协议的作用: 由上 ...

  6. 【深度学习】线性回归(Linear Regression)——原理、均方损失、小批量随机梯度下降

    1. 线性回归 回归(regression)问题指一类为一个或多个自变量与因变量之间关系建模的方法,通常用来表示输入和输出之间的关系. 机器学习领域中多数问题都与预测相关,当我们想预测一个数值时,就会 ...

  7. Redis基础数据结构-基于2.8

    SDS SDS是Redis中String的底层数据结构,数据结构如下,SDS保留了传统的C字符串表达方式即数组的最后一个元素是'/0'结尾.此外还添加了两个字段len和free,其中len表示字符串长 ...

  8. Codeforces Round #747 (Div. 2)

    比赛地址 A(水题) 题目链接 题目: 给出指定\(n\),求解出一段区间\([l,r]\)使得\(\sum\limits_{i=l}^ri=n\) 解析: 从点0,1两点作为起点分别向左右延伸长度, ...

  9. SpringIOC 理论推导

    IOC理论实现 UserDao接口 public interface UserDao { void say(); } UserDaoImpl实现类 public class UserDaoImpl i ...

  10. 题解 「JOISC 2016 Day 3」电报

    题目传送门 题目大意 给出一个\(n\)个点\(n\)条边的图,每个点有且仅有一个出边,改变每条边都会有对应的花费.求最小的花费使得整个图强连通. 思路 很显然,最后的图就是一个环.那我们要求的答案实 ...