美文网首页
SpringCloud 2020.0.4 系列之 JWT用户鉴权

SpringCloud 2020.0.4 系列之 JWT用户鉴权

作者: 追风人聊Java | 来源:发表于2022-01-12 18:57 被阅读0次

    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 中,可以正常访问接口

    image

    5. 综述

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

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

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

    相关文章

      网友评论

          本文标题:SpringCloud 2020.0.4 系列之 JWT用户鉴权

          本文链接:https://www.haomeiwen.com/subject/pnawcrtx.html