Gateway整合SpringSecurity实现自定义鉴权

882 阅读7分钟

前情提要

最近想自己搭建一套微服务项目练练手,初始化项目后发现以前老项目的security无法直接迁移进来。网上找了很多笔记感觉都不太完整,遂记录下本次初始化项目的过程,以后如果有类似需要网关整合可以直接摘抄。

项目源码

demo-security: 使用spring-security自定义鉴权

项目环境(低版本的其实改一些api也可以使用)

java21
springboot3.3.0
spring-security6.3.0

项目结构

com.wysiwyg     
├── wysiwyg-server             // 微服务模块
│       └── wysiwyg-admin      // 项目后端管理系统(此项目中主要的资源模块)
├── wysiwyg-base               // 基础模块
│       └── wysiwyg-common     // 通用模块
├── wysiwyg-gateway            // 网关模块(嵌入鉴权逻辑)
├──pom.xml                     // 依赖管理

步骤

  1. 首先要初始化maven项目,微服务项目导入必要的包

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    ​
        <groupId>com.wysiwyg</groupId>
        <artifactId>demo-security</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>pom</packaging>
        <modules>
            <module>wysiwyg-server</module>
            <module>wysiwyg-base</module>
            <module>wysiwyg-api</module>
            <module>wysiwyg-gateway</module>
        </modules>
    ​
        <properties>
            <project.encoding>UTF-8</project.encoding>
            <java.version>21</java.version>
    ​
            <!-- 核心依赖 -->
            <spring-boot.version>3.3.0</spring-boot.version>
            <spring-cloud.version>2023.0.2</spring-cloud.version>
            <spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
    ​
            <!--分离出这两个东西,是为了能够在 spring cloud alibaba不更新的时候,及时更新-->
            <seata.version>2.0.0</seata.version>
            <nacos.version>2.3.2</nacos.version>
    ​
            <!-- 次要依赖 -->
            <hutool.version>5.8.32</hutool.version>
            <mybatis-spring-boot-starter.version>3.0.3</mybatis-spring-boot-starter.version>
            <pagehelper-spring-boot-starter.version>2.1.0</pagehelper-spring-boot-starter.version>
            <knife4j.version>4.4.0</knife4j.version>
            <elasticsearch.version>7.17.21</elasticsearch.version>
            <fastjson2.version>2.0.51</fastjson2.version>
            <spring-security.version>6.3.0</spring-security.version>
            <jjwt.version>0.12.6</jjwt.version>
            <mapstruct.version>1.6.3</mapstruct.version>
            <mp.version>3.5.7</mp.version>
            <druid-spring-boot-starter.version>1.2.23</druid-spring-boot-starter.version>
        </properties>
    ​
        <dependencyManagement>
            <dependencies>
                <!-- 核心依赖 -->
                <!--spring boot-->
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-dependencies</artifactId>
                    <version>${spring-boot.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
                <!--spring cloud-->
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>${spring-cloud.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
    ​
                <!--spring cloud alibaba-->
                <dependency>
                    <groupId>com.alibaba.cloud</groupId>
                    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                    <version>${spring-cloud-alibaba.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
    ​
                <dependency>
                    <groupId>com.alibaba.nacos</groupId>
                    <artifactId>nacos-client</artifactId>
                    <version>${nacos.version}</version>
                    <exclusions>
                        <exclusion>
                            <groupId>com.google.guava</groupId>
                            <artifactId>guava</artifactId>
                        </exclusion>
                    </exclusions>
                </dependency>
                <dependency>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                    <version>${seata.version}</version>
                    <exclusions>
                        <exclusion>
                            <groupId>com.alibaba</groupId>
                            <artifactId>druid</artifactId>
                        </exclusion>
                    </exclusions>
                </dependency>
    ​
    ​
                <dependency>
                    <groupId>cn.hutool</groupId>
                    <artifactId>hutool-bom</artifactId>
                    <version>${hutool.version}</version>
                    <type>pom</type>
                    <!-- 注意这里是import -->
                    <scope>import</scope>
                </dependency>
    ​
    ​
                <dependency>
                    <groupId>org.mybatis.spring.boot</groupId>
                    <artifactId>mybatis-spring-boot-starter</artifactId>
                    <version>${mybatis-spring-boot-starter.version}</version>
                </dependency>
    ​
                <dependency>
                    <groupId>com.github.pagehelper</groupId>
                    <artifactId>pagehelper-spring-boot-starter</artifactId>
                    <version>${pagehelper-spring-boot-starter.version}</version>
                </dependency>
    ​
    ​
    ​
                <dependency>
                    <groupId>org.elasticsearch.client</groupId>
                    <artifactId>elasticsearch-rest-high-level-client</artifactId>
                    <version>${elasticsearch.version}</version>
                </dependency>
                <dependency>
                    <groupId>org.elasticsearch</groupId>
                    <artifactId>elasticsearch</artifactId>
                    <version>${elasticsearch.version}</version>
                </dependency>
                <dependency>
                    <groupId>org.elasticsearch.client</groupId>
                    <artifactId>elasticsearch-rest-client</artifactId>
                    <version>${elasticsearch.version}</version>
                    <exclusions>
                        <exclusion>
                            <artifactId>commons-logging</artifactId>
                            <groupId>commons-logging</groupId>
                        </exclusion>
                    </exclusions>
                </dependency>
    ​
                <dependency>
                    <groupId>com.alibaba.fastjson2</groupId>
                    <artifactId>fastjson2</artifactId>
                    <version>${fastjson2.version}</version>
                </dependency>
            </dependencies>
        </dependencyManagement>
    ​
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
            <resources>
                <resource>
                    <!--               执行资源文件目录-->
                    <directory>src/main/resources</directory>
                    <!--               指定资源文件目录中哪些文件不被打包-->
                    <excludes>
                        <exclude>application*.yml</exclude>
                        <exclude>log4j2*.xml</exclude>
                    </excludes>
                </resource>
                <resource>
                    <directory>src/main/resources</directory>
                    <filtering>true</filtering>
                    <!--               指定资源文件目录中哪些文件被打包-->
                    <includes>
                        <include>application.yml</include>
                        <include>application-${package.env}.yml</include>
                        <include>log4j2-${package.env}.xml</include>
                    </includes>
                </resource>
            </resources>
        </build>
    ​
        <profiles>
            <profile>
                <id>dev</id>
                <properties>
                    <package.env>dev</package.env>
                </properties>
                <activation>
                    <activeByDefault>true</activeByDefault>
                </activation>
            </profile>
            <profile>
                <id>prod</id>
                <properties>
                    <package.env>prod</package.env>
                </properties>
            </profile>
        </profiles>
    </project>
    
  2. 初始化各个模块,可参考项目结构,这里主要介绍下security的各项配置(主要是过滤器)

  3. security主配置类

    package com.wysiwyg.gateway.security.config;
    ​
    ​
    import com.wysiwyg.gateway.security.converter.JwtAuthenticationConverter;
    import com.wysiwyg.gateway.security.converter.UsernamePasswordAuthenticationConverter;
    import com.wysiwyg.gateway.security.filter.CustomAuthorizationWebFilter;
    import com.wysiwyg.gateway.security.filter.JwtAuthenticationFilter;
    import com.wysiwyg.gateway.security.filter.UsernamePasswordAuthenticationFilter;
    import com.wysiwyg.gateway.security.handle.CustomServerAuthenticationFailureHandler;
    import com.wysiwyg.gateway.security.handle.CustomServerAuthenticationSuccessHandler;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager;
    import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
    import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
    import org.springframework.security.config.web.server.ServerHttpSecurity;
    import org.springframework.security.web.server.SecurityWebFilterChain;
    import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
    ​
    ​
    /**
     * @author wwcc
     */
    @Configuration
    @EnableWebFluxSecurity
    public class ReactiveWebSecurityConfiguration {
    ​
    ​
        /**
         * @param jwtAuthenticationManager jwt认证管理器
         * @param jwtAuthenticationConverter jwt认证转换器
         *
         * @param usernamePasswordAuthenticationManager 用户名密码认证管理器
         * @param customServerAuthenticationSuccessHandler 自定义认证成功处理
         * @param customServerAuthenticationFailureHandler 自定义认证失败处理
         * @param customAuthenticationConverter 用户名密码认证转换器
         */
        @Bean
        public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
    ​
                                                                JwtAuthenticationFilter.JwtAuthenticationManager jwtAuthenticationManager,
                                                                JwtAuthenticationConverter jwtAuthenticationConverter,
    ​
                                                                AbstractUserDetailsReactiveAuthenticationManager usernamePasswordAuthenticationManager,
                                                                UsernamePasswordAuthenticationConverter customAuthenticationConverter,
    ​
                                                                CustomServerAuthenticationSuccessHandler customServerAuthenticationSuccessHandler,
                                                                CustomServerAuthenticationFailureHandler customServerAuthenticationFailureHandler,
    ​
                                                                CustomAuthorizationWebFilter.PathBasedReactiveAuthorizationManager pathBasedReactiveAuthorizationManager
    ​
        ) {
    ​
            // 不可以将过滤器注入到容器中,否则会默认注册到DefaultWebFilterChain,和security过滤链重复
            JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(jwtAuthenticationManager, jwtAuthenticationConverter, customServerAuthenticationFailureHandler);
            UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter(usernamePasswordAuthenticationManager, customServerAuthenticationSuccessHandler, customServerAuthenticationFailureHandler, customAuthenticationConverter);
            CustomAuthorizationWebFilter customAuthorizationWebFilter = new CustomAuthorizationWebFilter(pathBasedReactiveAuthorizationManager);
    ​
    ​
            http
                    .csrf(ServerHttpSecurity.CsrfSpec::disable)
                    .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
                    .formLogin(ServerHttpSecurity.FormLoginSpec::disable)
                    .logout(ServerHttpSecurity.LogoutSpec::disable)
                    .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
                    .addFilterAt(usernamePasswordAuthenticationFilter, SecurityWebFiltersOrder.FORM_LOGIN)
                    .addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
                    .addFilterAt(customAuthorizationWebFilter, SecurityWebFiltersOrder.AUTHORIZATION);
    ​
            return http.build();
        }
    ​
    }
    ​
    
  4. 主配置类配置好,架子就搭好了剩下就是把架子里面需要的完善下(主要是过滤器),熟悉安全框架的都清楚,安全主要靠认证鉴权。本文主要介绍认证中的密码认证、jwt认证,鉴权则是通过数据库自定义查询实现。

  5. 密码认证过滤器,密码认证主要是把用户名、密码等信息解析出来,然后去数据库查询出来对应的用户,进行密码和身份的验证。这里security提供了一套组件,帮助我们完成,我们需要做的是继承这些组件完成自定义的操作。

    5.1、UsernamePasswordAuthenticationManager主要是将认证的组件进行组装

    package com.wysiwyg.gateway.security.filter;
    ​
    import com.wysiwyg.gateway.model.po.AdminUserPO;
    import com.wysiwyg.common.web.response.ResponseEnum;
    import com.wysiwyg.common.constant.AuthConstant;
    import com.wysiwyg.gateway.security.converter.UsernamePasswordAuthenticationConverter;
    import com.wysiwyg.gateway.security.handle.CustomServerAuthenticationFailureHandler;
    import com.wysiwyg.gateway.security.handle.CustomServerAuthenticationSuccessHandler;
    import com.wysiwyg.gateway.service.SecurityUserDetailService;
    import org.springframework.context.annotation.Primary;
    import org.springframework.security.authentication.*;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
    import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
    import org.springframework.stereotype.Component;
    import reactor.core.publisher.Mono;
    import reactor.core.scheduler.Scheduler;
    import reactor.core.scheduler.Schedulers;
    ​
    /**
     * @author wwcc
     * @date 2024/11/16 20:29:06
     * @description 自定义用户名密码认证过滤器,不能注入容器中,否则会被注入到默认的DefaultWebFilterChain过滤器链,导致过滤器执行多次
     */
    public class UsernamePasswordAuthenticationFilter extends AuthenticationWebFilter {
    ​
    ​
        /**
         * @param usernamePasswordAuthenticationManager    用户信息认证管理器
         * @param customServerAuthenticationSuccessHandler 认证成功处理逻辑
         * @param customServerAuthenticationFailureHandler 认证失败处理逻辑
         * @param customAuthenticationConverter            认证信息转换器
         * @return
         */
        public UsernamePasswordAuthenticationFilter(AbstractUserDetailsReactiveAuthenticationManager usernamePasswordAuthenticationManager,
                                                    CustomServerAuthenticationSuccessHandler customServerAuthenticationSuccessHandler,
                                                    CustomServerAuthenticationFailureHandler customServerAuthenticationFailureHandler,
                                                    UsernamePasswordAuthenticationConverter customAuthenticationConverter) {
            super(usernamePasswordAuthenticationManager);
            this.setServerAuthenticationConverter(customAuthenticationConverter);
            this.setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers(AuthConstant.LOGIN_URL));
            this.setAuthenticationFailureHandler(customServerAuthenticationFailureHandler);
            this.setAuthenticationSuccessHandler(customServerAuthenticationSuccessHandler);
        }
    ​
        @Component
        @Primary
        public static class UsernamePasswordAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager {
            private final Scheduler scheduler = Schedulers.boundedElastic();
    ​
            private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    ​
            private final SecurityUserDetailService securityUserDetailService;
    ​
            public UsernamePasswordAuthenticationManager(SecurityUserDetailService securityUserDetailService) {
                this.securityUserDetailService = securityUserDetailService;
            }
    ​
    ​
    ​
            @Override
            public Mono<Authentication> authenticate(Authentication authentication) {
                AdminUserPO adminUserPo = (AdminUserPO) authentication.getPrincipal();
                String mobile = adminUserPo.getMobile();
                String presentedPassword = (String) authentication.getCredentials();
                return retrieveUser(mobile)
                        .doOnNext(this::preAuthenticationChecks)
                        .publishOn(this.scheduler)
                        .filter((userDetails) -> this.passwordEncoder.matches(presentedPassword, userDetails.getPassword()))
                        .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException(ResponseEnum.LOGIN_FAILED.getMsg()))))
                        .map(this::createUsernamePasswordAuthenticationToken);
            }
    ​
    ​
    ​
    ​
            private UsernamePasswordAuthenticationToken createUsernamePasswordAuthenticationToken(UserDetails userDetails) {
                AdminUserPO adminUserPo = (AdminUserPO) userDetails;
                adminUserPo.setPassword(null);
                return UsernamePasswordAuthenticationToken.authenticated(adminUserPo, null,userDetails.getAuthorities());
            }
    ​
    ​
            @Override
            protected Mono<UserDetails> retrieveUser(String mobile) {
                return securityUserDetailService.findByUsername(mobile);
            }
    ​
    ​
            private void preAuthenticationChecks(UserDetails user) {
                if (!((AdminUserPO)user).getStatus().equals(1)) {
                    throw new LockedException(ResponseEnum.LOCALED_USER.getMsg());
                }
    ​
            }
        }
    ​
    ​
    }
    ​
    

    5.2、UsernamePasswordAuthenticationConverter将http请求体中自定义的参数拿出来,换成security支持的实体类Authentication

    package com.wysiwyg.gateway.security.converter;
    ​
    import com.wysiwyg.gateway.model.po.AdminUserPO;
    import com.wysiwyg.common.web.response.ResponseEnum;
    import com.wysiwyg.common.constant.AuthConstant;
    import com.wysiwyg.gateway.util.WebExchangeUtils;
    import org.springframework.http.HttpHeaders;
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.server.authentication.ServerFormLoginAuthenticationConverter;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    ​
    import java.util.Objects;
    ​
    /**
     * @author wwcc
     * @date 2024/11/14 20:39:09
     */
    @Component
    public class UsernamePasswordAuthenticationConverter extends ServerFormLoginAuthenticationConverter {
    ​
    ​
        @Override
        public Mono<Authentication> convert(ServerWebExchange exchange) {
            HttpHeaders headers = exchange.getRequest().getHeaders();
            String host = Objects.requireNonNull(headers.getHost()).getHostName();
    ​
            return WebExchangeUtils.parseRequestBodyToJson(exchange)
                    .flatMap(jsonNode -> {
    ​
                        String mobile = jsonNode.has(AuthConstant.MOBILE_PARAMETER) ? jsonNode.get(AuthConstant.MOBILE_PARAMETER).asText() : null;
                        String password = jsonNode.has(AuthConstant.PASSWORD_PARAMETER) ? jsonNode.get(AuthConstant.PASSWORD_PARAMETER).asText() : null;
                        String verificationCode = jsonNode.has(AuthConstant.VERIFICATION_PARAMETER) ? jsonNode.get(AuthConstant.VERIFICATION_PARAMETER).asText() : null;
    ​
                        // 创建Authentication对象
                        if (mobile != null && password != null) {
                            AdminUserPO adminUserPo = new AdminUserPO(mobile, verificationCode, host);
                            return Mono.just(new UsernamePasswordAuthenticationToken(adminUserPo, password));
                        } else {
                            return Mono.error(new BadCredentialsException(ResponseEnum.LOGIN_FAILED.getMsg()));
                        }
    ​
                    });
    ​
        }
    ​
    }
    

    5.3、CustomServerAuthenticationSuccessHandler认证完成返回给前端的响应体

    package com.wysiwyg.gateway.security.handle;
    ​
    import com.wysiwyg.gateway.model.po.AdminUserPO;
    import com.wysiwyg.common.web.response.ServerResponseEntity;
    import com.wysiwyg.gateway.security.jwt.JwtTokenGenerator;
    import com.wysiwyg.gateway.security.jwt.JwtTokenPair;
    import com.wysiwyg.gateway.util.WebExchangeUtils;
    import lombok.SneakyThrows;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.server.WebFilterExchange;
    import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
    import org.springframework.stereotype.Component;
    import reactor.core.publisher.Mono;
    ​
    import java.util.HashMap;
    import java.util.Map;
    ​
    /**
     * @author wwcc
     * @description  自定义认证成功处理器
     */
    @Component
    public class CustomServerAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
    ​
    ​
        @Autowired
        private JwtTokenGenerator jwtTokenGenerator;
    ​
        @Override
        @SneakyThrows
        public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
            AdminUserPO adminUserPo = (AdminUserPO) authentication.getPrincipal();
    ​
            Map<String, String> additional = new HashMap<>();
            additional.put("name", adminUserPo.getUsername());
            additional.put("mobile", adminUserPo.getMobile());
            additional.put("mail", adminUserPo.getMail());
    ​
            JwtTokenPair jwtTokenPair = jwtTokenGenerator.jwtTokenPair(adminUserPo.getUserId(), adminUserPo.getRoles(), additional);
    ​
            return WebExchangeUtils.writeJsonResponse(webFilterExchange.getExchange(), HttpStatus.OK, ServerResponseEntity.success(jwtTokenPair));
    ​
        }
    }
    ​
    

    5.4、CustomServerAuthenticationFailureHandler认证失败返回给前端的响应体

    package com.wysiwyg.gateway.security.handle;
    ​
    import com.wysiwyg.gateway.util.WebExchangeUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpStatus;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.server.WebFilterExchange;
    import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
    import org.springframework.stereotype.Component;
    import reactor.core.publisher.Mono;
    ​
    /**
     * @author wwcc
     * @description 自定义认证失败处理
     */
    @Component
    @Slf4j
    public class CustomServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
        @Override
        public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
            log.error(exception.getMessage(),exception);
            return WebExchangeUtils.writeErrorResponse(webFilterExchange.getExchange(), HttpStatus.UNAUTHORIZED, exception.getMessage());
        }
    }
    ​
    

    5.5、专门处理ServerWebExchange请求和响应

    package com.wysiwyg.gateway.util;
    
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.wysiwyg.common.web.response.ResponseEnum;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.core.io.buffer.DataBufferUtils;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    import java.nio.charset.StandardCharsets;
    
    /**
     * @author wwcc
     */
    @Slf4j
    public class WebExchangeUtils {
    
        private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    
        /**
         * 从ServerWebExchange的请求体解析为JsonNode
         *
         * @param exchange ServerWebExchange对象
         * @return 包含JsonNode的Mono
         */
        public static Mono<JsonNode> parseRequestBodyToJson(ServerWebExchange exchange) {
            return DataBufferUtils.join(exchange.getRequest().getBody())
                    .flatMap(dataBuffer -> {
                        byte[] bytes = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(bytes);
                        DataBufferUtils.release(dataBuffer);
                        String bodyStr = new String(bytes, StandardCharsets.UTF_8);
    
                        try {
                            return Mono.just(OBJECT_MAPPER.readTree(bodyStr));
                        } catch (JsonProcessingException e) {
                            log.error("Failed to parse JSON request body", e);
                            return Mono.empty();
                        }
                    });
        }
    
        /**
         * 从ServerWebExchange的请求体解析为指定类型
         *
         * @param exchange ServerWebExchange对象
         * @param clazz    指定解析的类型
         * @param <T>      类型参数
         * @return 包含指定类型对象的Mono
         */
        public static <T> Mono<T> parseRequestBody(ServerWebExchange exchange, Class<T> clazz) {
            return DataBufferUtils.join(exchange.getRequest().getBody())
                    .flatMap(dataBuffer -> {
                        byte[] bytes = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(bytes);
                        DataBufferUtils.release(dataBuffer);
                        String bodyStr = new String(bytes, StandardCharsets.UTF_8);
    
                        try {
                            return Mono.just(OBJECT_MAPPER.readValue(bodyStr, clazz));
                        } catch (JsonProcessingException e) {
                            log.error("Failed to parse request body to {}", clazz.getSimpleName(), e);
                            return Mono.empty();
                        }
                    });
        }
    
        /**
         * 向ServerWebExchange响应JSON数据
         *
         * @param exchange     ServerWebExchange对象
         * @param status       HTTP状态码
         * @param responseBody 响应体对象
         * @return 表示响应完成的Mono<Void>
         */
        public static Mono<Void> writeJsonResponse(ServerWebExchange exchange, HttpStatus status, Object responseBody) {
            return Mono.just(exchange.getResponse())
                    .flatMap(r -> {
                        r.setStatusCode(status);
                        r.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                        try {
                            DataBuffer buffer = r.bufferFactory().wrap(OBJECT_MAPPER.writeValueAsBytes(responseBody));
                            return r.writeWith(Mono.just(buffer));
                        } catch (JsonProcessingException e) {
                            return Mono.error(new RuntimeException(e));
                        }
                    });
        }
    
        /**
         * 向ServerWebExchange响应错误信息
         *
         * @param exchange     ServerWebExchange对象
         * @param status       HTTP状态码
         * @param errorMessage 错误信息
         * @return 表示响应完成的Mono<Void>
         */
        public static Mono<Void> writeErrorResponse(ServerWebExchange exchange, HttpStatus status, String errorMessage) {
            return Mono.just(exchange.getResponse())
                    .flatMap(r -> {
                        r.setStatusCode(status);
                        r.getHeaders().setContentType(MediaType.APPLICATION_JSON);
    
                        String errorJson = String.format("{"error": "%s"}", errorMessage);
                        DataBuffer buffer = r.bufferFactory().wrap(errorJson.getBytes(StandardCharsets.UTF_8));
                        return r.writeWith(Mono.just(buffer));
                    });
    
    
        }
    
        /**
         * @param exchange   ServerWebExchange对象
         * @param responseEnum 异常枚举
         * @return 表示响应完成的Mono<Void>
         */
        public static Mono<Void> writeErrorResponse(ServerWebExchange exchange, ResponseEnum responseEnum) {
            return writeJsonResponse(exchange, responseEnum.getHttpStatus(), responseEnum.getMsg());
    
    
        }
    }
    
  6. jwt认证过滤器

    package com.wysiwyg.gateway.security.filter;
    
    import com.wysiwyg.common.constant.AuthConstant;
    import com.wysiwyg.gateway.model.po.AdminUserPO;
    import com.wysiwyg.common.web.response.ResponseEnum;
    import com.wysiwyg.gateway.security.converter.JwtAuthenticationConverter;
    import com.wysiwyg.gateway.security.handle.CustomServerAuthenticationFailureHandler;
    import com.wysiwyg.gateway.security.jwt.JwtTokenGenerator;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpHeaders;
    import org.springframework.security.authentication.BadCredentialsException;
    import org.springframework.security.authentication.ReactiveAuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
    import org.springframework.stereotype.Component;
    import reactor.core.publisher.Mono;
    
    import java.util.Objects;
    
    import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
    
    /**
     * @author wwcc
     * @date 2020/8/17 下午12:53
     * @description 不能注入容器中,否则会被注入到默认的DefaultWebFilterChain过滤器链,导致过滤器执行多次
     */
    @Slf4j
    public class JwtAuthenticationFilter extends AuthenticationWebFilter {
    
        public JwtAuthenticationFilter(JwtAuthenticationManager jwtAuthenticationManager,
                                       JwtAuthenticationConverter jwtAuthenticationConverter,
                                       CustomServerAuthenticationFailureHandler customServerAuthenticationFailureHandler) {
            super(jwtAuthenticationManager);
            this.setRequiresAuthenticationMatcher(exchange ->
                                        Mono.justOrEmpty(exchange.getRequest().getHeaders())
                                            .mapNotNull((headers) -> headers.getFirst(HttpHeaders.AUTHORIZATION))
                                            .filter(Objects::nonNull)
                                            .flatMap((token) -> null != token  && token.startsWith(AuthConstant.BEARER_PREFIX) ? MatchResult.match() : MatchResult.notMatch())
                                            .switchIfEmpty(MatchResult.notMatch()));
            this.setServerAuthenticationConverter(jwtAuthenticationConverter);
            this.setAuthenticationFailureHandler(customServerAuthenticationFailureHandler);
    
        }
    
    
        @Component
        public static class JwtAuthenticationManager implements ReactiveAuthenticationManager {
    
            private final JwtTokenGenerator jwtTokenGenerator;
    
            public JwtAuthenticationManager(JwtTokenGenerator jwtTokenGenerator) {
                this.jwtTokenGenerator = jwtTokenGenerator;
            }
    
            @Override
            public Mono<Authentication> authenticate(Authentication authentication) {
                return Mono.justOrEmpty((String) authentication.getCredentials())
                        .flatMap(token -> Mono.just(jwtTokenGenerator.decodeAndVerify(token)))
                        .filter(Objects::nonNull)
                        .map(jsonObject -> jsonObject.to(AdminUserPO.class))
                        .onErrorMap(Exception.class, e -> new BadCredentialsException(ResponseEnum.AUTHENTICATION_FAILED.getMsg(),e))
                        .map(contextUserInfo -> UsernamePasswordAuthenticationToken.authenticated(contextUserInfo, null, contextUserInfo.getAuthorities()));
            }
        }
    
    }
    

    6.1、JwtAuthenticationConverter将token从请求头中解析出来

    package com.wysiwyg.gateway.security.converter;
    
    import com.wysiwyg.common.constant.AuthConstant;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpHeaders;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    /**
     * @author wwcc
     * @date 2024/11/14 20:39:09
     */
    @Component
    @Slf4j
    public class JwtAuthenticationConverter implements ServerAuthenticationConverter {
    
    
    
        @Override
        public Mono<Authentication> convert(ServerWebExchange exchange) {
            return Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION))
                    .filter(token -> token != null && token.startsWith(AuthConstant.BEARER_PREFIX))
                    .map(token -> token.substring(AuthConstant.BEARER_PREFIX.length()))
                    .map(jwt -> new UsernamePasswordAuthenticationToken(null, jwt));
        }
    
    }
    

    6.2、CustomServerAuthenticationFailureHandler认证失败处理器

    package com.wysiwyg.gateway.security.handle;
    
    import com.wysiwyg.gateway.util.WebExchangeUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpStatus;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.server.WebFilterExchange;
    import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
    import org.springframework.stereotype.Component;
    import reactor.core.publisher.Mono;
    
    /**
     * @author wwcc
     * @description 自定义认证失败处理
     */
    @Component
    @Slf4j
    public class CustomServerAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
        @Override
        public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
            log.error(exception.getMessage(),exception);
            return WebExchangeUtils.writeErrorResponse(webFilterExchange.getExchange(), HttpStatus.UNAUTHORIZED, exception.getMessage());
        }
    }
    
  7. CustomAuthorizationWebFilter鉴权过滤器

    package com.wysiwyg.gateway.security.filter;
    
    import com.wysiwyg.gateway.service.SecurityUserDetailService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authorization.AuthorizationDecision;
    import org.springframework.security.authorization.ReactiveAuthorizationManager;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.web.server.authorization.AuthorizationWebFilter;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    /**
     * @author wwcc
     * @date 2024/11/26 21:25:05
     */
    @Slf4j
    public class CustomAuthorizationWebFilter extends AuthorizationWebFilter {
    
        public CustomAuthorizationWebFilter(ReactiveAuthorizationManager<ServerWebExchange> authorizationManager) {
            super(authorizationManager);
        }
    
    
        @Component
        public static class PathBasedReactiveAuthorizationManager implements ReactiveAuthorizationManager<ServerWebExchange> {
    
    
            @Autowired
            private SecurityUserDetailService userDetailService;
    
            @Override
            public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, ServerWebExchange exchange) {
                return userDetailService
                        .findPathAuthorities(exchange.getRequest().getURI().getPath()) // 获取路径权限 Flux
                        .map(GrantedAuthority::getAuthority)
                        .collectList() // 收集为 List
                        .flatMap(pathAuthorities -> {
                            if (pathAuthorities.isEmpty()) {
                                // 若路径权限为空,直接授权通过
                                return Mono.just(new AuthorizationDecision(true));
                            }
    
                            // 检查用户是否认证,并逐一对比权限
                            return authentication.filter(Authentication::isAuthenticated)
                                    .flatMapMany(auth -> Flux.fromIterable(auth.getAuthorities()))
                                    .map(GrantedAuthority::getAuthority)
                                    .any(pathAuthorities::contains)
                                    .map(AuthorizationDecision::new);
                        })
                        .defaultIfEmpty(new AuthorizationDecision(false))
                        .doOnError(err-> log.error(err.getMessage(),err));
            }
    
        }
    
    }
    

结语

把主要配置贴出来供大家参考下,一些详细的配置还是可以参考gitee源码。