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,歡迎大家關注