SpringCloud|SpringCloud 2020.0.4 系列之 JWT用户鉴权

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

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

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

2. 开发 授权鉴权服务接口层 my-auth-api
2.1 主要依赖

my-auth-api org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-openfeign


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 tokentoken * @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 主要依赖
org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-data-redis com.auth0 java-jwt 3.18.2 cn.zhuifengren my-auth-api ${project.version}


3.2 主要配置
server: port: 45000 spring: application: name: my-auth-service redis: database: 0 host: 192.168.1.22 port: 6379 password: zhuifengreneureka: 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 tokentoken * @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 增加依赖
cn.zhuifengren my-auth-api ${project.version} org.springframework.boot spring-boot-starter-web org.apache.commons commons-lang3


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 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 工程,调用实验接口:
GEThttp://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 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 服务,再次调用实验接口:
GEThttp://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> converters) { return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList())); }


4.7 实验授权鉴权
1)再次重启 Gateway 工程
【SpringCloud|SpringCloud 2020.0.4 系列之 JWT用户鉴权】2)调用登录接口获取 token
POST http://Gateway IP:端口/my-auth-service/login?userName=zhangsan&password=12345
3)调用业务接口,将 token 和用户名放到 header 中,可以正常访问接口
SpringCloud|SpringCloud 2020.0.4 系列之 JWT用户鉴权
文章图片


5. 综述
今天聊了一下 JWT用户鉴权,希望可以对大家的工作有所帮助。
欢迎帮忙点赞、评论、转发、加关注 :)
关注追风人聊Java,每天更新Java干货。

6. 个人公众号
追风人聊Java,欢迎大家关注
SpringCloud|SpringCloud 2020.0.4 系列之 JWT用户鉴权
文章图片

AuthFilter

    推荐阅读