1. 概述

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

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

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

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

2.1 主要依赖

  1. <artifactId>my-auth-api</artifactId>
  2.  
  3. <dependencies>
  4. <dependency>
  5. <groupId>org.springframework.boot</groupId>
  6. <artifactId>spring-boot-starter-web</artifactId>
  7. </dependency>
  8. <dependency>
  9. <groupId>org.springframework.cloud</groupId>
  10. <artifactId>spring-cloud-starter-openfeign</artifactId>
  11. </dependency>
  12. </dependencies>

2.2 实体类

  1. /**
  2. * 账户实体类
  3. */
  4. @Data
  5. @Builder
  6. @NoArgsConstructor
  7. @AllArgsConstructor
  8. public class Account implements java.io.Serializable {
  9.  
  10. // 用户名
  11. private String userName;
  12.  
  13. // token
  14. private String token;
  15.  
  16. // 刷新token
  17. private String refreshToken;
  18. }
  1. /**
  2. * 响应实体类
  3. */
  4. @Data
  5. @Builder
  6. @AllArgsConstructor
  7. @NoArgsConstructor
  8. public class AuthResponse implements java.io.Serializable {
  9.  
  10. // 账户
  11. private Account account;
  12.  
  13. // 响应码
  14. private Integer code;
  15. }

2.3 授权鉴权 Service 接口

  1. /**
  2. * 授权鉴权 Service 接口
  3. */
  4. @FeignClient("my-auth-service")
  5. public interface AuthService {
  6.  
  7. /**
  8. * 登录接口
  9. * @param userName 用户名
  10. * @param password 密码
  11. * @return
  12. */
  13. @PostMapping("/login")
  14. AuthResponse login(@RequestParam("userName") String userName,
  15. @RequestParam("password") String password);
  16.  
  17. /**
  18. * 校验token
  19. * @param token token
  20. * @param userName 用户名
  21. * @return
  22. */
  23. @GetMapping("/verify")
  24. AuthResponse verify(@RequestParam("token") String token,
  25. @RequestParam("userName") String userName);
  26.  
  27. /**
  28. * 刷新token
  29. * @param refreshToken 刷新token
  30. */
  31. @PostMapping("/refresh")
  32. AuthResponse refresh(@RequestParam("refreshToken") String refreshToken);
  33. }

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

3.1 主要依赖

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-web</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.springframework.boot</groupId>
  11. <artifactId>spring-boot-starter-actuator</artifactId>
  12. </dependency>
  13. <!-- redis -->
  14. <dependency>
  15. <groupId>org.springframework.boot</groupId>
  16. <artifactId>spring-boot-starter-data-redis</artifactId>
  17. </dependency>
  18. <!-- jwt -->
  19. <dependency>
  20. <groupId>com.auth0</groupId>
  21. <artifactId>java-jwt</artifactId>
  22. <version>3.18.2</version>
  23. </dependency>
  24.  
  25. <dependency>
  26. <groupId>cn.zhuifengren</groupId>
  27. <artifactId>my-auth-api</artifactId>
  28. <version>${project.version}</version>
  29. </dependency>

3.2 主要配置

  1. server:
  2. port: 45000
  3. spring:
  4. application:
  5. name: my-auth-service
  6. redis:
  7. database: 0
  8. host: 192.168.1.22
  9. port: 6379
  10. password: zhuifengren
  11.  
  12. eureka:
  13. client:
  14. service-url:
  15. defaultZone: http://zhuifengren1:35000/eureka/,http://zhuifengren2:35001/eureka/ # Eureka Server的地址

3.3 启动类添加注解

@SpringBootApplication
@EnableDiscoveryClient

3.4 JWT 核心Service方法

  1. /**
  2. * 获得 token
  3. * @param account 账户实体
  4. * @return
  5. */
  6. public String token(Account account) {
  7.  
  8. log.info("获取token");
  9.  
  10. Date now = new Date();
  11.  
  12. // 指定算法,KEY是自定义的秘钥
  13. Algorithm algorithm = Algorithm.HMAC256(KEY);
  14.  
  15. // 生成token
  16. String token = JWT.create()
  17. .withIssuer(ISSUER) // 发行人,自定义
  18. .withIssuedAt(now)
  19. .withExpiresAt(new Date(now.getTime() + TOKEN_EXPIRES)) // 设置token过期时间
  20. .withClaim("userName", account.getUserName()) // 自定义属性
  21. .sign(algorithm);
  22.  
  23. log.info(account.getUserName() + " token 生成成功");
  24. return token;
  25. }
  26.  
  27. /**
  28. * 验证token
  29. * @param token
  30. * @param userName
  31. * @return
  32. */
  33. public boolean verify(String token, String userName) {
  34.  
  35. log.info("验证token");
  36.  
  37. try {
  38. // 指定算法,KEY是自定义的秘钥
  39. Algorithm algorithm = Algorithm.HMAC256(KEY);
  40.  
  41. // 验证token
  42. JWTVerifier verifier = JWT.require(algorithm)
  43. .withIssuer(ISSUER) // 发行人,自定义
  44. .withClaim("userName", userName) // 自定义属性
  45. .build();
  46.  
  47. verifier.verify(token);
  48.  
  49. return true;
  50. } catch (Exception ex) {
  51. log.error("验证失败", ex);
  52. return false;
  53. }
  54. }

3.5 授权鉴权业务Service

  1. /**
  2. * 授权鉴权 Service
  3. */
  4. @RestController
  5. @Slf4j
  6. public class AuthServiceImpl implements AuthService {
  7.  
  8. @Autowired
  9. private JwtService jwtService;
  10.  
  11. @Autowired
  12. private RedisTemplate redisTemplate;
  13.  
  14. /**
  15. * 登录
  16. * @param userName 用户名
  17. * @param password 密码
  18. * @return
  19. */
  20. public AuthResponse login(@RequestParam("userName") String userName,
  21. @RequestParam("password") String password) {
  22.  
  23. Account account = Account.builder()
  24. .userName(userName)
  25. .build();
  26.  
  27. String token = jwtService.token(account);
  28.  
  29. account.setToken(token);
  30. account.setRefreshToken(UUID.randomUUID().toString());
  31.  
  32. redisTemplate.opsForValue().set(account.getRefreshToken(), account);
  33.  
  34. return AuthResponse.builder()
  35. .account(account)
  36. .code(200) // 200 代表成功
  37. .build();
  38. }
  39.  
  40. /**
  41. * 刷新token
  42. * @param refreshToken 刷新token
  43. * @return
  44. */
  45. public AuthResponse refresh(@RequestParam("refreshToken") String refreshToken) {
  46.  
  47. Account account = (Account)redisTemplate.opsForValue().get(refreshToken);
  48. if(account == null) {
  49. return AuthResponse.builder()
  50. .code(-1) // -1 代表用户未找到
  51. .build();
  52. }
  53.  
  54. String newToken = jwtService.token(account);
  55. account.setToken(newToken);
  56. account.setRefreshToken(UUID.randomUUID().toString());
  57.  
  58. redisTemplate.delete(refreshToken);
  59. redisTemplate.opsForValue().set(account.getRefreshToken(), account);
  60.  
  61. return AuthResponse.builder()
  62. .account(account)
  63. .code(200) // 200 代表成功
  64. .build();
  65. }
  66.  
  67. /**
  68. * 验证token
  69. * @param token token
  70. * @param userName 用户名
  71. * @return
  72. */public AuthResponse verify(@RequestParam("token") String token,
  73. @RequestParam("userName") String userName) {
  74.  
  75. log.info("verify start");
  76. boolean isSuccess = jwtService.verify(token, userName);
  77.  
  78. log.info("verify result:" + isSuccess);
  79.  
  80. return AuthResponse.builder()
  81. .code(isSuccess ? 200 : -2) // -2 代表验证不通过
  82. .build();
  83. }
  84. }

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

4.1 增加依赖

  1. <dependency>
  2. <groupId>cn.zhuifengren</groupId>
  3. <artifactId>my-auth-api</artifactId>
  4. <version>${project.version}</version>
  5. <exclusions>
  6. <exclusion>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-web</artifactId>
  9. </exclusion>
  10. </exclusions>
  11. </dependency>
  12.  
  13. <dependency>
  14. <groupId>org.apache.commons</groupId>
  15. <artifactId>commons-lang3</artifactId>
  16. </dependency>

4.2 启动类增加注解

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

4.3 鉴权过滤器

  1. @Slf4j
  2. @Component
  3. public class AuthFilter implements GatewayFilter, Ordered {
  4.  
  5. private static final String AUTH = "Authorization";
  6.  
  7. private static final String USER_NAME = "userName";
  8.  
  9. @Autowired
  10. private AuthService authService;
  11.  
  12. @Override
  13. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  14. log.info("开始验证");
  15.  
  16. // 从 header 中得到 token 和 用户名
  17. ServerHttpRequest request = exchange.getRequest();
  18. HttpHeaders headers = request.getHeaders();
  19. String token = headers.getFirst(AUTH);
  20. String userName= headers.getFirst(USER_NAME);
  21.  
  22. ServerHttpResponse response = exchange.getResponse();
  23.  
  24. if(StringUtils.isBlank(token)) {
  25. log.error("token没有找到");
  26. response.setStatusCode(HttpStatus.UNAUTHORIZED);
  27. return response.setComplete();
  28. }
  29.  
  30. // 验证用户名
  31. log.info("执行验证方法");
  32. AuthResponse resp = authService.verify(token, userName);
  33. log.info("执行验证方法完毕");

  34. if(resp == null || resp.getCode() != 200) {
  35. log.error("无效的token");
  36. response.setStatusCode(HttpStatus.FORBIDDEN);
  37. return response.setComplete();
  38. }
  39.  
  40. return chain.filter(exchange);
  41. }
  42.  
  43. @Override
  44. public int getOrder() {
  45. return 0;
  46. }
  47. }

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

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

  1. @Configuration
  2. public class GatewayConfig {
  3.  
  4. @Bean
  5. @Order
  6. public RouteLocator myRoutes(RouteLocatorBuilder builder, AuthFilter authFilter) {
  7.  
  8. return builder.routes()
  9. .route(r -> r.path("/business/**")
  10. .and()
  11. .method(HttpMethod.GET)
  12. .filters(f -> f.stripPrefix(1)
  13.  
  14. .filter(authFilter)
  15.  
  16. )
  17. .uri("lb://MY-EUREKA-CLIENT"))
  18. .build();
  19. }
  20. }

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 工程会报如下错误:

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

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

我们修改 AuthFilter 中的代码

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

  1. CompletableFuture<AuthResponse> completableFuture = CompletableFuture.supplyAsync
  2. (()-> {
  3.  
  4. return authService.verify(token, userName);
  5. });
  6.  
  7. AuthResponse resp = null;
  8. try {
  9. resp = completableFuture.get();
  10. } catch (Exception ex) {
  11. log.error("调用验证接口错误", ex);
  12. }

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 工程报了如下错误:

  1. 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)}
  2. at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1790) ~[spring-beans-5.3.12.jar:5.3.12]
  3. at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1346) ~[spring-beans-5.3.12.jar:5.3.12]
  4. at org.springframework.beans.factory.support.DefaultListableBeanFactory$DependencyObjectProvider.getObject(DefaultListableBeanFactory.java:1979) ~[spring-beans-5.3.12.jar:5.3.12]

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

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

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,欢迎大家关注

  1. 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. 手机访问pc网站自动跳转手机端网站PHP代码

    $agent = $_SERVER['HTTP_USER_AGENT']; if(strpos($agent,"comFront") strpos($agent,"iPh ...

  2. jQuery has been removed

    jQuery has been removed, 新的项目不要用jQuery了 这些问题都已经有了解决方案 * $()选择器, * $.ajax, * $dom.on("click" ...

  3. MySQL修改root密码的多种方法, mysql 导出数据库(包含视图)

    方法1: 用SET PASSWORD命令 mysql -u root mysql> SET PASSWORD FOR 'root'@'localhost' = PASSWORD('newpass ...

  4. Maven项目创建与配置(二)

    项目配置 1:添加Source Folder 右击项目>NEW>Source Folder maven规定必须创建一下几个Source Folder src/main/resources ...

  5. django安装xadmin

    环境:pycharm  django1.11.20  python2.7(根据网络上的资料,自己整理实现) 下载:https://github.com/sshwsfc/xadmin/tree/mast ...

  6. Navicat连接数据库成功,新建查询时提示错误“Cannot create file ……”

    Navicat连接数据库成功,新建查询时提示错误"Cannot create file --" 原因:编辑连接{高级}<设置位置>被修改,该oci.dll不正确 解决方 ...

  7. Viterbi 算法 Python实现 [NLP学习一]

    最近思考了一下未来,结合老师的意见,还是决定挑一个方向开始研究了,虽然个人更喜欢鼓捣.深思熟虑后,结合自己的兴趣点,选择了NLP方向,感觉比纯粹的人工智能.大数据之类的方向有趣多了,个人还是不适合纯粹 ...

  8. .NET 5 WPF 调用OCX 经验分享

    在.Net 5.0 WPF中调用OCX步骤如下: 1,用工具先把ocx转换成AxInterop.EloamViewLib.dll和Interop.EloamViewLib.dll.(这里是我用到的oc ...

  9. Serverless 对研发效能的变革和创新

    作者 | 杨皓然(不瞋) 对企业而言,Serverless 架构有着巨大的应用潜力.随着云产品的完善,产品的集成和被集成能力的加强,软件交付流程自动化能力的提高,我们相信在 Serverless 架构 ...

  10. 2021.3.3--vj补题

    题目 C - C CodeForces - 1166C The legend of the foundation of Vectorland talks of two integers xx and  ...