前情提要
最近想自己搭建一套微服务项目练练手,初始化项目后发现以前老项目的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 // 依赖管理
步骤
-
首先要初始化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> -
初始化各个模块,可参考项目结构,这里主要介绍下security的各项配置(主要是过滤器)
-
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(); } } -
主配置类配置好,架子就搭好了剩下就是把架子里面需要的完善下(主要是过滤器),熟悉安全框架的都清楚,安全主要靠认证和鉴权。本文主要介绍认证中的密码认证、jwt认证,鉴权则是通过数据库自定义查询实现。
-
密码认证过滤器,密码认证主要是把用户名、密码等信息解析出来,然后去数据库查询出来对应的用户,进行密码和身份的验证。这里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()); } } -
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()); } } -
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源码。