美文网首页
在SpringBoot Gateway 中实现RedisToke

在SpringBoot Gateway 中实现RedisToke

作者: 东南枝下 | 来源:发表于2023-10-16 10:44 被阅读0次

    背景

    1. jwt token的载荷是明文(base64),虽然只是用来传递一些非敏感信息,但依旧会让人感觉有些不适
    2. jwt token无法主动失效
    3. 微服务之间尽量减少耦合度

    解决思路

    1. 由认证服务(iam)产生RedisToken,该token保存在iam服务的redis中,可以主动失效,也可以设置失效时间
    2. 采用jwt作为微服务间验证的依据,如果使用RedisToken,则所有微服务均需要依赖同一个redis数据库(或集群)

    实现方式

    1. 用户登陆,由 认证服务 产生RedisToken交给用户
    2. 用户使用RedisToken通过 网关服务 访问 业务服务,在 网关服务 中使用RedisToken交换JwtToken(可以包含一些非敏感的当前用户的信息),再使用JwtToken访问业务服务
    3. 业务服务只对JwtToken进行验证,并且可以从jwt payload中解析出当前用户的信息

    代码实现

    注册中心

    eureka、nacos等,略

    认证服务

    使用SpringSecurity实现用户登陆

    参考文档:https://www.cnblogs.com/cbvlog/p/15624215.html

    1. 依赖注入

    其他如数据库,redis,服务发现,服务调用等依赖就不写了

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
    
            <!-- security -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
    
    1. 设置用户密码加密方式
    /**
     * 密码加密方式
     *
     * @author Jenson
     */
    @Component
    @Slf4j
    public class CustomBCryptPasswordEncoder implements PasswordEncoder {
    
        @Override
        public String encode(CharSequence rawPassword) {
            // 简单加密,生成一个salt
            String salt = BCrypt.gensalt();
            return BCrypt.hashpw(rawPassword.toString(), salt);
        }
    
        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            if (rawPassword != null && encodedPassword != null && !encodedPassword.isEmpty()) {
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            } else {
                log.warn("Empty encoded password");
                return false;
            }
        }
    }
    
    
    1. 登录认证过滤器,设置登录地址、调用方式,继承 org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter
    /**
     * 登录认证过滤器
     *
     * @author Jenson
     */
    public class AuthenticationLoginFilter extends AbstractAuthenticationProcessingFilter {
    
        /**
         * 设置登录地址、调用方式
         */
        public AuthenticationLoginFilter() {
            super(new AntPathRequestMatcher("/login", "POST"));
        }
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            // 读取表单提取数据
            String username = request.getParameter("username");
            String password = request.getParameter("password");
            // 封装到token中提交
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            return getAuthenticationManager().authenticate(authRequest);
        }
    }
    
    
    1. 实现 org.springframework.security.core.userdetails.UserDetailsService

    验证登陆接口中用户传入的账号密码,生成org.springframework.security.core.userdetails.UserDetails

    /**
     * @author Jenson
     */
    @Slf4j
    @Service
    public class CustomUserDetailsServiceImpl implements UserDetailsService {
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 使用用户名查询数据库(或是缓存中的)中的用户持久层对象 UserPO(名字随便了)
            UserPO userPo = this.searchUserPoFromDb(username);
            if(userPo == null){
                // 用户不存在,登录失败
                return null;
            }
            // 构建 org.springframework.security.core.userdetails.User 对象,当然最好是继承它,可以添加一些自己的属性上去,比如生日年龄性别啥的
            User  user =new User(userPo.getUsername,
                                 userPo.getPassword, 
                                 AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
            return user ;
        }
    }
    
    
    1. 实现 认证成功处理器,org.springframework.security.web.authentication.AuthenticationSuccessHandler

    认证成功后,生成token,输出token

    /**
     * 认证成功处理器
     *
     * @author Jenson
     */
    @Slf4j
    @Component
    public class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
        
        // 这是自己写的redis-token工具对象
        @Autowired
        private RedisTokenUtils redisTokenUtils;
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request,
                                            HttpServletResponse response,
                                            Authentication authentication) throws IOException {
            User user = (User) authentication.getPrincipal();
            SecurityContextHolder.getContext().setAuthentication(authentication);
            // 根据获取的用户信息生成token,并将token保存在redis中,设置失效时间,
            // 考虑到登陆时,系统中可能有未失效的token,为了避免多端登陆互相踢出,可以先尝试获取用户的token,
            // 如果存在则刷新缓存时间(token续期),更新token绑定用户缓存信息,返回老token;如果不存在则沈城新token
            UserTokenRel userTokenRel = redisTokenUtils.refreshToken(user);
            // 书写响应体
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setStatus(HttpStatus.OK.value());
            // 在响应体中写出token
            byte[] body = JSON.toJSONString(userTokenRel.getLoginToken()).getBytes(StandardCharsets.UTF_8);
            OutputStream outputStream = response.getOutputStream();
            try {
                outputStream.write(body);
            } finally {
                outputStream.flush();
                outputStream.close();
            }
    
        }
    }
    
    1. 实现 认证失败处理器,org.springframework.security.web.authentication.AuthenticationFailureHandler
    /**
     * 认证失败处理
     *
     * @author Jenson
     */
    @Slf4j
    @Component
    public class LoginAuthenticationFailureHandler implements AuthenticationFailureHandler {
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
            String exceptionMsg = "认证失败原因";
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            OutputStream outputStream = response.getOutputStream();
            // 自定义的响应体
            CuxResponseEntity<ExceptionResponse> responseEntity =
                    new CuxResponseEntity<>(new ExceptionResponse("AUTHENTICATION_FAILURE", exceptionMsg), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
            byte[] body = JSON.toJSONString(responseEntity.getBody()).getBytes(StandardCharsets.UTF_8);
            try {
                outputStream.write(body);
            } finally {
                outputStream.flush();
                outputStream.close();
            }
        }
    }
    
    
    1. 登录过滤器的配置

    需要使用到上述创建的三个实例

    /**
     * 登录过滤器的配置
     *
     * @author Jenson
     */
    @Configuration
    public class AuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
        /**
         * userDetailService
         */
        @Qualifier("customUserDetailsServiceImpl")
        @Autowired
        private UserDetailsService userDetailsService;
    
        /**
         * 登录成功处理器
         */
        @Autowired
        private AuthenticationSuccessHandler loginAuthenticationSuccessHandler;
    
        /**
         * 登录失败处理器
         */
        @Autowired
        private AuthenticationFailureHandler loginAuthenticationFailureHandler;
    
        /**
         * 加密
         */
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        /**
         * 将登录接口的过滤器配置到过滤器链中
         * 1. 配置登录成功、失败处理器
         * 2. 配置自定义的userDetailService(从数据库中获取用户数据)
         * 3. 将自定义的过滤器配置到spring security的过滤器链中,配置在UsernamePasswordAuthenticationFilter之前
         *
         * @param http HttpSecurity
         */
        @Override
        public void configure(HttpSecurity http) {
            AuthenticationLoginFilter filter = new AuthenticationLoginFilter();
            filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
            //认证成功处理器
            filter.setAuthenticationSuccessHandler(loginAuthenticationSuccessHandler);
            //认证失败处理器
            filter.setAuthenticationFailureHandler(loginAuthenticationFailureHandler);
            //直接使用DaoAuthenticationProvider
            DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
            //设置userDetailService
            provider.setUserDetailsService(userDetailsService);
            //设置加密算法
            provider.setPasswordEncoder(passwordEncoder);
            http.authenticationProvider(provider);
            //将这个过滤器添加到UsernamePasswordAuthenticationFilter之前执行
            http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
        }
    }
    
    
    1. Token校验过滤器

    此处的token,是jwt-token,在上述“认证成功过滤器”中,发放的是redis-token,但是使用redis-token调用网关接口时,网关会使用redis-token交换jwt-token,所以该认证服务的filter需要认证的是jwt-token

    /**
     * Token校验过滤器
     *
     * @author Jenson
     */
    @Slf4j
    public class TokenAuthenticationFilter extends GenericFilterBean {
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            HttpServletRequest httpServletRequest;
            httpServletRequest = (HttpServletRequest) request;
            String authorization = httpServletRequest.getHeader("Authorization");
            log.info("---------> Authorization : {}", authorization);
            if (StringUtils.hasText(authorization)) {
    
                String[] tokenDetail = authorization.trim().split(BaseConstants.Symbol.SPACE);
                String token = tokenDetail[1];
                if (StringUtils.hasText(token)) {
                    User user = null;
                    try {
                        // JwtUtils 是自定义的jwt-token解析工具
                        // 解析jwt-token,创建用户对象
                        // HttpServletResponseUtils 是自定义的异常打印工具类
                        User = new User(JwtUtils.verify(token));
                    } catch (UnauthorizedException e) {
                        HttpServletResponseUtils.outPrintUnauthorizedException(httpServletResponse);
                        return;
                    } catch (ForbiddenException e) {
                        HttpServletResponseUtils.outPrintForbiddenException(httpServletResponse);
                        return;
                    } catch (Exception e) {
                        HttpServletResponseUtils.outPrintUnknownException(httpServletResponse);
                        return;
                    }
                    // 账号不为空且还没有认证过
                    if (user !=null && SecurityContextHolder.getContext().getAuthentication() == null) {
                        // 认证成功,设置当前用户对象
                        UsernamePasswordAuthenticationToken authentication =
                                new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }
            chain.doFilter(request, response);
        }
    }
    
    

    该过滤器没有像参考文档一样继承OncePerRequestFilter,以为我实际使用中遇到了非自定义异常时,又会过一遍filter才抛出(不知道是不是哪配置错了),而这次过filter时filter调用链中没有了认证的filter,就会抛出认证失败的异常,将原异常覆盖。

    1. 接口未认证异常
    /**
     * 用户未通过认证访问受保护的资源 401
     *
     * @author Jenson
     */
    @Slf4j
    @Component
    public class EntryPointUauthenticationHandler implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
            authException.printStackTrace();
            // 自定义异常打印工具
            HttpServletResponseUtils.outPrintUnauthorizedException(response);
        }
    }
    
    
    1. 接口认证无权限异常
    
    /**
     * 认证成功的用户访问受保护的资源,但是权限不够 403
     *
     * @author Jenson
     */
    @Slf4j
    @Component
    public class RequestAccessDeniedHandler implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
            // 自定义异常打印工具
            HttpServletResponseUtils.outPrintForbiddenException(response);
        }
    }
    
    1. 接口授权配置
    
    /**
     * @author Jenson
     */
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {
    
        @Autowired
        private AuthenticationSecurityConfig authenticationSecurityConfig;
        @Qualifier("entryPointUauthenticationHandler")
        @Autowired
        private AuthenticationEntryPoint entryPointUauthenticationHandler;
        @Qualifier("requestAccessDeniedHandler")
        @Autowired
        private AccessDeniedHandler requestAccessDeniedHandler;
    
        /**
         * 授权配置,最高优先级
         *
         * @param http HttpSecurity
         * @return SecurityFilterChain
         * @throws Exception
         */
        @Bean
        @Order(1)
        public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
    
            http
                    // 禁用表单登录
                    .formLogin().disable()
                    // 应用登录过滤器的配置,配置分离
                    .apply(authenticationSecurityConfig)
                    .and()
                    // 设置URL的授权
                    .authorizeHttpRequests()
                    // 这里需要将登录页面放行,permitAll()表示不再拦截,/login 登录的url,/refreshToken刷新token的url
                    .requestMatchers(
                            // 登陆接口
                            "/login",
                            // token交换接口(redis-token -> jwt-token)
                            "/token/generate/jwt/{redisToken}"
                    )
                    .permitAll()
                    .anyRequest()
                    .authenticated()
                    //处理异常情况:认证失败和权限不足
                    .and()
                    .exceptionHandling()
                    //认证未通过,不允许访问异常处理器
                    .authenticationEntryPoint(entryPointUauthenticationHandler)
                    //认证通过,但是没权限处理器
                    .accessDeniedHandler(requestAccessDeniedHandler)
                    .and()
                    //禁用session,JWT校验不需要session
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    //将TOKEN校验过滤器配置到过滤器链中,否则不生效,放到UsernamePasswordAuthenticationFilter之前
                    .addFilterAt(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                    // 解决跨域问题(其实没有解决)
                    .cors()
                    .and()
                    // 关闭csrf
                    .csrf().disable();
    
            return http.build();
        }
    
    }
    

    将登陆接口(/login)和 token交换接口(/token/generate/jwt/{redisToken}) 设置为免登录。
    /token/generate/jwt/{redisToken}其实是需要认证的,但是filter配置为了认证jwt-token,当然也可以在filter中增加一种认证方式,但我这里选择在容器内认证,所以该接口设置为免登录。

    1. token 交换接口 /token/generate/jwt/{redisToken}
     /**
         * 生成JWT
         *
         * @param redisToken redisToken
         * @return jwt
         */
        @GetMapping("/generate/jwt/{redisToken}")
        public WmResponseEntity<String> generateJwt(@PathVariable String redisToken) {
    
            log.info("-----> generateJwt redisToken : {}", redisToken);
            // 根据 redisToken 获取用户信息 ,redisTokenUtils 为自定义用户redis-token管理工具
            UserTokenRel userTokenRel = redisTokenUtils.getUserTokenRel(redisToken);
            if (userTokenRel == null) {
                // CommonException 为自定义 RuntimeException
                throw new CommonException("NOT_LOGGED_IN", "用户未登录");
            }
            User user = userService.searchUserByUserId(userTokenRel.getUserId());
            // 生成 jwt-token,jwt的payload中可以尽可能装入除用户密码外的用户信息,
            // 在业务服务中进行解析,就不需要跨服务获取调用者信息了,实现了服务解耦
            String jwt = tokenService.generateJwt(user);
            // 异步刷新一下token,避免使用中失效
            Long userId = user.getUserId();
            // 异步刷新token,token自动续期,防止用户用着用着突然掉线
            CompletableFuture.runAsync(() -> redisTokenUtils.refreshToken(userId), commonExecutor);
            return Results.success(jwt);
        }
    

    网关

    1. 依赖注入

    其他工具依赖就省略了

             <!-- gateway-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
                <version>4.0.3</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
    
    
    1. gateway 路由配置
      无论采用静态路由配置还是动态路由配置,不在此处细说
      配置认证服务的路由前缀为 /iam,则登陆接口地址就是 /iam/login

    spring-gateway+nacos 实现动态路由配置可以参考以下文章:
    https://blog.csdn.net/qq_38374397/article/details/125874882

    1. 实现 GlobalFilter ,拦截请求进行客户化处理
      自定义filter,在网关路由前调用认证服务接口交换token(只能异步调用),使用jwt调用服务
    /**
     * 网关请求拦截客户化处理
     *
     * @author Jenson
     * @version 1.0
     */
    @Slf4j
    @Component
    public class CuxGlobalFilter implements GlobalFilter, Ordered {
    
        @SneakyThrows
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();
            String path = request.getURI().getPath();
            log.info("-----------> path : {}", path);
            HttpHeaders headers = request.getHeaders();
            String authorization = headers.getFirst("Authorization");
            String tenant = headers.getFirst("Tenant");
            if (!"/iam/login".equals(path) && StringUtils.hasText(authorization)) {
                // 非 认证 服务,换用 jwt token,此举是为了在认证层面解耦服务
                headers = HttpHeaders.writableHttpHeaders(headers);
                String[] tokenDetail = authorization.trim().split(BaseConstants.Symbol.SPACE);
                String token = tokenDetail[1];
                if (!StringUtils.hasText(token)) {
                    return outUnauthorizedResponse(response);
                }
    
                // ------ 获取Token start -----
                // 在此处,异步 feign调用 交换 token 接口,获得新jwt-token
                CompletableFuture<ResponseEntity<String>> newJwtFuture = CompletableFuture.supplyAsync(() -> tokenService.generateJwt(token));
                String jwt;
                try {
                    ResponseEntity<String> responseEntity = newJwtFuture.get(1, TimeUnit.SECONDS);
                    // FeignRspEntityParseUtils 自定义接口响应结果解析工具
                    jwt = FeignRspEntityParseUtils.parse(responseEntity, String.class);
                } catch (CommonException e) {
                    if ("NOT_LOGGED_IN".equals(e.getCode())) {
                        return outUnauthorizedResponse(response);
                    }
                    return outCommonExceptionResponse(response, e.getCode(), e.getMsg());
                } catch (Exception e) {
                    log.info("----> 换取 jwt 失败 , {}", e);
                    return outCommonExceptionResponse(response, "SWITCH_JWT_ERROR", "换取 jwt 失败 ,请联系管理员检查认证服务");
                }
                log.info("-----> jwt : {}", jwt);
                // ---- 在这里获取Token end -----
    
                headers.setBearerAuth(jwt);
            }
            return chain.filter(exchange);
        }
    
        @Override
        public int getOrder() {
            return 1;
        }
    
        /**
         * 输出未认证响应
         *
         * @param response 响应体
         * @return 未认证
         * @throws JsonProcessingException json解析异常
         */
        private Mono<Void> outUnauthorizedResponse(ServerHttpResponse response) throws JsonProcessingException {
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            DataBufferFactory bufferFactory = response.bufferFactory();
            ObjectMapper objectMapper = new ObjectMapper();
            CuxResponseEntity<ExceptionResponse> responseEntity =
                    new CuxResponseEntity<>(new ExceptionResponse("UNAUTHORIZED", "Unauthorized !"), new HttpHeaders(), HttpStatus.UNAUTHORIZED);
            DataBuffer wrap = bufferFactory.wrap(objectMapper.writeValueAsBytes(responseEntity.getBody()));
            return response.writeWith(Mono.fromSupplier(() -> wrap));
        }
    
        /**
         * 输出通用异常响应信息
         *
         * @param response 响应体
         * @return 未认证
         * @throws JsonProcessingException json解析异常
         */
        private Mono<Void> outCommonExceptionResponse(ServerHttpResponse response, String code, String msg) throws JsonProcessingException {
            response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
            DataBufferFactory bufferFactory = response.bufferFactory();
            ObjectMapper objectMapper = new ObjectMapper();
            CuxResponseEntity<ExceptionResponse> responseEntity =
                    new CuxResponseEntity<>(new ExceptionResponse(code, msg), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
            DataBuffer wrap = bufferFactory.wrap(objectMapper.writeValueAsBytes(responseEntity.getBody()));
            return response.writeWith(Mono.fromSupplier(() -> wrap));
        }
    }
    
    
    1. 自定义异常处理(非必须)
      将网关抛的错改造为自定义的异常格式,方便前端处理
    /**
     * 自定义异常处理
     *
     * @author Jenson
     * @version 1.0
     */
    @Slf4j
    @Configuration
    @Order(-1)
    public class CuxWebExceptionHandler implements WebExceptionHandler {
        @SneakyThrows
        @Override
        public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
            ServerHttpResponse response = exchange.getResponse();
            if (response.isCommitted()) {
                return Mono.error(ex);
            } else if (ex instanceof ResponseStatusException rspEx) {
                HttpStatusCode httpStatusCode = rspEx.getStatusCode();
                response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                response.setStatusCode(httpStatusCode);
                DataBufferFactory bufferFactory = response.bufferFactory();
                ObjectMapper objectMapper = new ObjectMapper();
                CuxResponseEntity<ExceptionResponse> responseEntity =
                        new CuxResponseEntity<>(new ExceptionResponse(((HttpStatus) httpStatusCode).name(), rspEx.getReason()),
                                rspEx.getHeaders(), httpStatusCode.value());
                DataBuffer wrap = bufferFactory.wrap(objectMapper.writeValueAsBytes(responseEntity.getBody()));
                return response.writeWith(Mono.fromSupplier(() -> wrap));
            } else {
                return Mono.error(ex);
            }
        }
    }
    
    
    1. 解决跨域
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.reactive.CorsWebFilter;
    import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
    import org.springframework.web.util.pattern.PathPatternParser;
    
    /**
     * 跨域
     *
     * @author Jenson
     * @version 1.0
     */
    @Configuration
    public class CorsConfig {
    
        @Bean
        public CorsWebFilter corsFilter() {
            CorsConfiguration config = new CorsConfiguration();
            // 允许的请求头
            config.addAllowedMethod("*");
            // 允许的请求源 (如:http://localhost:8080)
            config.addAllowedOrigin("*");
            // 允许的请求方法 ==> GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
            config.addAllowedHeader("*");
            // URL 映射 (如: /admin/**)
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
            source.registerCorsConfiguration("/**", config);
            return new CorsWebFilter(source);
        }
    }
    
    
    

    业务服务

    业务服务的认证逻辑都是统一的,所以采用依赖starter组件的方式,就可以快速为每一个业务服务增加接口认证

    1. 构建cux-start-core工程
    • pom.xml 依赖注入
    ......
    <groupId>com.cux</groupId>
        <artifactId>cux-starter-core</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <name>cux-starter-core</name>
    
    <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.0.4</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
    
        <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <!-- security -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
    
            ......
    
    1. 接口认证filter(jwt)

    此处代码逻辑和认证服务是一致的,其实可以考虑将此核心依赖包运用于认证服务

    /**
     * Token校验过滤器
     *
     * @author Jenson
     */
    @Slf4j
    public class TokenAuthenticationFilter extends GenericFilterBean {
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            HttpServletRequest httpServletRequest;
            httpServletRequest = (HttpServletRequest) request;
            String authorization = httpServletRequest.getHeader("Authorization");
            log.info("---------> Authorization : {}", authorization);
            if (StringUtils.hasText(authorization)) {
    
                String[] tokenDetail = authorization.trim().split(BaseConstants.Symbol.SPACE);
                String token = tokenDetail[1];
                if (StringUtils.hasText(token)) {
                    User user = null;
                    try {
                        // JwtUtils 是自定义的jwt-token解析工具
                        // 解析jwt-token,创建用户对象
                        // HttpServletResponseUtils 是自定义的异常打印工具类
                        User = new User(JwtUtils.verify(token));
                    } catch (UnauthorizedException e) {
                        HttpServletResponseUtils.outPrintUnauthorizedException(httpServletResponse);
                        return;
                    } catch (ForbiddenException e) {
                        HttpServletResponseUtils.outPrintForbiddenException(httpServletResponse);
                        return;
                    } catch (Exception e) {
                        HttpServletResponseUtils.outPrintUnknownException(httpServletResponse);
                        return;
                    }
                    // 账号不为空且还没有认证过
                    if (user !=null && SecurityContextHolder.getContext().getAuthentication() == null) {
                        // 认证成功,设置当前用户对象
                        UsernamePasswordAuthenticationToken authentication =
                                new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }
            chain.doFilter(request, response);
        }
    }
    
    
    1. 接口未认证异常
      此处代码逻辑和认证服务是一致的,其实可以考虑将此核心依赖包运用于认证服务
    /**
     * 用户未通过认证访问受保护的资源 401
     *
     * @author Jenson
     */
    @Slf4j
    @Component
    public class EntryPointUauthenticationHandler implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
            authException.printStackTrace();
            // 自定义异常打印工具
            HttpServletResponseUtils.outPrintUnauthorizedException(response);
        }
    }
    
    
    1. 接口认证无权限异常
      此处代码逻辑和认证服务是一致的,其实可以考虑将此核心依赖包运用于认证服务
    
    /**
     * 认证成功的用户访问受保护的资源,但是权限不够 403
     *
     * @author Jenson
     */
    @Slf4j
    @Component
    public class RequestAccessDeniedHandler implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
            // 自定义异常打印工具
            HttpServletResponseUtils.outPrintForbiddenException(response);
        }
    }
    
    1. 认证配置
    /**
     * @author Jenson
     */
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {
    
        @Autowired
        private PermitRequestMatchers permitRequestMatchers;
    
        /**
         * 授权配置,最高优先级
         *
         * @param http HttpSecurity
         * @return SecurityFilterChain
         * @throws Exception
         */
        @Bean
        @Order(1)
        public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
    
            http
                    // 禁用表单登录
                    .formLogin().disable()
                    // 设置URL的授权
                    .authorizeHttpRequests()
                    // 需要放行的url,动态获取免登录接口
                    .requestMatchers(permitRequestMatchers.requestMatchersToArray())
                    .permitAll()
                    .anyRequest()
                    .authenticated()
                    //处理异常情况:认证失败和权限不足
                    .and()
                    .exceptionHandling()
                    //认证未通过,不允许访问异常处理器
                    .authenticationEntryPoint(new EntryPointUauthenticationHandler())
                    //认证通过,但是没权限处理器
                    .accessDeniedHandler(new RequestAccessDeniedHandler())
                    .and()
                    //禁用session,JWT校验不需要session
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    //将TOKEN校验过滤器配置到过滤器链中,否则不生效,放到UsernamePasswordAuthenticationFilter之前
                    .addFilterAt(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                    // 解决跨域问题
                    .cors()
                    .and()
                    // 关闭csrf
                    .csrf().disable();
    
            return http.build();
        }
    
    }
    
    
    
    1. 服务间调用,token传递(openfeign)

    微服务间feign调用是不走网关的,为了互相传递当前调用接口的jwt-token,需要配置feign,token的传递可以进行公共配置

    • feign 依赖
     <!--服务调用-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
                <version>4.0.1</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-loadbalancer</artifactId>
                <version>4.0.1</version>
            </dependency>
    
    
    • Feign 请求拦截器
    /**
     * Feign 请求拦截器
     * <p>
     * 请求带上token
     *
     * @author Jenson
     */
    public class FeignRequestInterceptor implements RequestInterceptor {
        @Override
        public void apply(RequestTemplate requestTemplate) {
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
            if (requestAttributes != null) {
                HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
                String authorization = request.getHeader("Authorization");
                requestTemplate.header("Authorization", authorization);
            }
        }
    }
    
    
    /**
     * @author Jenson
     */
    @Configuration
    public class FeignRequestInterceptorConfig {
    
        @Bean
        public RequestInterceptor createFeignRequestInterceptor() {
            return new FeignRequestInterceptor();
        }
    }
    
    1. spring.factories 配置文件

    定义需要装载的配置类

    image.png
    1. 打包,安装依赖

    本地安装:mvn install

    1. 将该核心依赖运用于业务服务,即可完成对业务服务的接口权限控制
      业务服务与认证服务、网关需要在同一个注册中心下;
      网关需要配置对应业务服务的路由;
      此时,直接访问业务服务的接口需要jwt-token,通过网关访问,需要redis-token;
      只需将网关暴露到外部而不需要暴露业务服务

    其他补充

    1. jwt 生成 和 解析
    • pom.xml 依赖
    <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>3.5.0</version>
            </dependency>
    
    • 生成jwt
     /**
         * 生成jwt,设置超时时间
         *
         * @param payload 载荷
         * @return jwt
         */
        public static String generate(Map<String, String> payload) {
            //过期时间
            Date expireDate = new Date(System.currentTimeMillis() + DEFAULT_JWT_TTL);
            Map<String, Object> map = new HashMap<>();
            map.put("alg", "HS256");
            map.put("typ", "JWT");
            JWTCreator.Builder jwtBuilder = JWT.create()
                    // 添加头部
                    .withHeader(map)
                    //超时设置,设置过期的日期
                    .withExpiresAt(expireDate)
                    //签发时间
                    .withIssuedAt(new Date());
    
            // 构建 jwt 载荷
            payload.forEach(jwtBuilder::withClaim);
            // 签名,返回
            return jwtBuilder.sign(Algorithm.HMAC256(DEFAULT_JWT_SECRET));
        }
    
    
    • 解析 jwt
    /**
         * 校验token并解析token
         *
         * @return 从token中解析出的载荷
         */
        public static Map<String, Claim> verify(String token) {
            if ("token_absent".equals(token)) {
                // redis token 不存在
                throw new UnauthorizedException();
            }
            try {
                JWTVerifier verifier = JWT.require(Algorithm.HMAC256(DEFAULT_JWT_SECRET)).build();
                DecodedJWT jwt = verifier.verify(token);
                return jwt.getClaims();
            } catch (TokenExpiredException e) {
                log.error("jwt token已过期");
                throw new UnauthorizedException();
            } catch (JWTVerificationException e) {
                log.error("jwt token不存在或不正确");
                throw new ForbiddenException();
            }
        }
    
    

    相关文章

      网友评论

          本文标题:在SpringBoot Gateway 中实现RedisToke

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