SpringCloud 2020.0.4 系列之 JWT用戶鑒權


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM