一、前言
1、pig资源服务器的配置
Spring Security oauth2
相关的依赖是在pigx-common-security
模块中引入的,其他模块需要进行token
鉴权的,需要在微服务中引入pigx-common-security
模块的依赖,从而间接引入相关的Spring security oauth2
依赖。
其最简单的一个目的,是对资源进行保护,对访问资源时携带的token
进行鉴权。
微服务,开启资源服务器配置步骤:
①引入相关的依赖
<!--安全模块--> <dependency> <groupId>com.pig4cloud</groupId> <artifactId>pig-common-security</artifactId> <version>laster.version</version> </dependency>
②main
方法开启@EnablePigResourceServer
pig4cloud
对Spring Security OAuth2
的资源服务器配置进行了封装,只需要一个注解即可完成相关的操作。
二、EnablePigxResourceServer
解析
1、EnablePigxResourceServer
的源码
/* 用于指示编译器将被注解的元素的注释信息包含在生成的文档中 使用该自定义注解的地方会在生成的文档中显示该注解的信息和说明 */ @Documented /* 用于指示一个自定义注解是否具有继承性 当使用@Inherited注解某个自定义注解时,如果一个类或接口使用了该被注解的自定义注解,那么其子类或实现类也会自动被应用该注解 */ @Inherited /* 用于限定自定义注解可以应用的目标元素类型 TYPE 类或接口; FIELD 字段(成员变量); METHOD 方法;PARAMETER 方法参数; CONSTRUCTOR 构造函数;LOCAL_VARIABLE 局部变量; ANNOTATION_TYPE 注解类型;PACKAGE 包; TYPE_PARAMETER 类型参数;TYPE_USE 类型使用; */ @Target({ ElementType.TYPE }) /* 指定自定义注解的保留策略 SOURCE: 自定义注解仅在源代码中保留,编译后不包含 CLASS: 自定义注解在编译后的字节码文件中保留,但不会被加载到虚拟机中 RUNTIME: 自定义注解在运行时保留 */ @Retention(RetentionPolicy.RUNTIME) /* @Import注解主要用于将其他配置类导入到当前的配置类中,以实现配置的组合和复用,而不是用于创建Bean对象 */ @Import({ PigxResourceServerAutoConfiguration.class, PigxResourceServerConfiguration.class, PigxFeignClientConfiguration.class }) public @interface EnablePigxResourceServer { }
2、PigxResourceServerAutoConfiguration.class
源码:
/* 用于自动生成一个包含所有非final和非null字段的构造函数 */ @RequiredArgsConstructor /* 只要在加载PigxResourceServerAutoConfiguration时 才会去加载对应的属性配置类:PermitAllUrlProperties 注意: 通过该注解引入的配置@Import({EnableConfigurationPropertiesRegistrar.class}), 会将被@ConfigurationProperties 注解标记的目标类PermitAllUrlProperties注册为一个bean对象 目的:减少spring管控在资源数量 详情见2.1 */ @EnableConfigurationProperties(PermitAllUrlProperties.class) public class PigxResourceServerAutoConfiguration { /** * 鉴权具体的实现逻辑 详情见2.2 * @return (#pms.xxx) */ @Bean("pms") public PermissionService permissionService() { return new PermissionService(); } /** * 请求令牌的抽取逻辑 详情见2.3 * @param urlProperties 对外暴露的接口列表 * @return BearerTokenExtractor */ @Bean public PigxBearerTokenExtractor pigBearerTokenExtractor(PermitAllUrlProperties urlProperties) { return new PigxBearerTokenExtractor(urlProperties); } /** * 资源服务器异常处理 详情见2.4 * @param objectMapper jackson 输出对象 * @param securityMessageSource 自定义国际化处理器 * @return ResourceAuthExceptionEntryPoint */ @Bean public ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint(ObjectMapper objectMapper, MessageSource securityMessageSource) { return new ResourceAuthExceptionEntryPoint(objectMapper, securityMessageSource); } /** * 资源服务器toke内省处理器 详情见2.5 * @param authorizationService token 存储实现 * @return TokenIntrospector */ @Bean public OpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2AuthorizationService authorizationService) { return new PigxCustomOpaqueTokenIntrospector(authorizationService); } }
2.1、属性配置类:PermitAllUrlProperties
①将默认的忽略地址加入ignoreUrls列表
②将配置文件中配置的地址加入到ignoreUrls列表
③通过请求映射器获得所有的请求控制器,将添加@inner注解的请求地址加入到ignoreUrls列表
//自动添加日志记录器(Logger)的字段,实现了简化日志记录的功能 @Slf4j /* @ConfigurationProperties将配置文件中以指定前缀开头的属性值映射到一个Java类中, 以方便统一管理和使用 */ @ConfigurationProperties(prefix = "security.oauth2.client") /* InitializingBean: 在Bean声明周期中的初始化操作,InitializingBean接口中有一个afterPropertiesSet()方法, 其执行时机早于init-method配置的方法,其是在所有的bean实例化完成并完成依赖注入后执行的, 自动调用实现了InitializingBean接口的bean的afterPropertiesSet()方法,即在bean实例化后和依赖注入后执行的回调方法 注意:implements InitializingBean接口并不是在所有类中都能生效的,它只适用于Spring容器中的bean对象 */ public class PermitAllUrlProperties implements InitializingBean { private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}"); private static final String[] DEFAULT_IGNORE_URLS = new String[] { "/actuator/**", "/error", "/v3/api-docs" }; //在配置文件中指定的需要忽略的url @Getter @Setter private List<String> ignoreUrls = new ArrayList<>(); //在Bean属性设置后执行该方法 @Override public void afterPropertiesSet() { //忽略url的列表中先加入默认忽略的url ignoreUrls.addAll(Arrays.asList(DEFAULT_IGNORE_URLS)); /* RequestMappingHandlerMapping 是 Spring MVC 中的一个重要组件,它负责将请求映射到具体的处理方法(handler method) 在 Spring MVC 的处理流程中,RequestMappingHandlerMapping 会根据请求的 URL 和请求方式(GET、POST 等)来确定需要调用哪个处理方法, 从而完成请求的处理过程 */ RequestMappingHandlerMapping mapping = SpringContextHolder.getBean("requestMappingHandlerMapping"); //RequestMappingInfo:请求映射信息,包括请求路径、请求方式等 //HandlerMethod:获得所有处理方法的具体信息,包括所属的类、方法名、参数列表等存放到 Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods(); //处理@Inner注解的方法和类,将其添加到ignoreUrls列表中 map.keySet().forEach(info -> { //获取对应的映射处理方法 HandlerMethod handlerMethod = map.get(info); // 获取方法上边的注解 替代path variable 为 * //通过AnnotationUtils获取当前映射处理方法上的Inner注解,赋值给method,如果没有inner注解,method的值为null Inner method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Inner.class); //如果method不为空(当前方法添加Inner注解)将映射的url通过正则表达式解析后加入到ignoreurls列表中 //正则表达式主要是对路径上的参数进行处理,匹配{}中的内容,然后替换为* Optional.ofNullable(method).ifPresent(inner -> Objects.requireNonNull(info.getPathPatternsCondition()) .getPatternValues().forEach(url -> ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, "*")))); // 获取类上边的注解, 替代path variable 为 * //同理方法 Inner controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Inner.class); Optional.ofNullable(controller).ifPresent(inner -> Objects.requireNonNull(info.getPathPatternsCondition()) .getPatternValues().forEach(url -> ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, "*")))); }); } }
Map<RequestMappingInfo, HandlerMethod>内容如下所示:
2.2、接口权限判断工具:PermissionService
/** * 鉴权具体的实现逻辑 * @return (#pms.xxx) */ @Bean("pms") public PermissionService permissionService() { return new PermissionService(); }
具体解析
public class PermissionService { /** * 判断接口是否有任意xxx,xxx权限 * @param permissions 权限 * @return {boolean} */ //String... 可变参数,允许将任意数量的String参数打包成一个数组 //可以将一个 ArrayList 作为参数传递给可变参数 String... permissions //eg:hasPermission("param1", "param2")、hasPermission(Arrays.asList("param1", "param2")) public boolean hasPermission(String... permissions) { //入参为空,返回false if (ArrayUtil.isEmpty(permissions)) { return false; } //从用户的安全上下文信息获取权限信息 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); //用户权限信息为null,返回false if (authentication == null) { return false; } //获得权限信息赋值给authorities Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); //权限是否匹配 return authorities.stream().map(GrantedAuthority::getAuthority).filter(StringUtils::hasText) .anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x)); } }
具体使用方式:
/** * 更新角色菜单 * * @param roleVo 角色对象 * @return success、false */ @SysLog("更新角色菜单") @PutMapping("/menu") @PreAuthorize("@pms.hasPermission('sys_role_perm')") public R saveRoleMenus(@RequestBody RoleVO roleVo) { return R.ok(sysRoleService.updateRoleMenus(roleVo)); }
使用的Spring Security
的@PreAuthorize
注解,用于指定方法执行前需要满足的权限要求,它通常用于控制访问某些受保护资源时的权限控制。
在 @PreAuthorize
中,可以指定一个 SpEL 表达式作为权限要求,如@PreAuthorize("@pms.hasPermission('sys_role_perm')")
“@pms
”是 SpEL 中使用的 Spring EL Bean 引用语法,表示引用名为 pms
的 Bean。hasPermission
是 pms
Bean 中定义的一个方法,用于检查当前用户是否拥有指定的权限。
因此,上述注解的作用是,当执行该方法时,应该检查当前用户是否具有 sys_role_perm
权限。如果当前用户不具备该权限,方法将被拒绝执行,抛出 AccessDeniedException 异常。
注意:使用的Spring Security
的@PreAuthorize
注解,需要配置全局的方法级安全性设置,启用 Spring Security 的方法级安全性(Method Security)意味着你可以在方法级别上对访问权限进行控制。通过使用 @PreAuthorize
、@PostAuthorize
、@Secured
等注解,你可以在方法执行前或执行后对用户的权限进行验证。
在yml中配置:
spring: security: enabled: true method: security: enabled: true
在xml配置文件中配置:
<beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> <global-method-security pre-post-annotations="enabled" secured-annotations="enabled" /> <!-- 其他配置 --> </beans:beans>
在pig中是直接通过注解@EnableMethodSecurity
开启的
@Slf4j @EnableWebSecurity @EnableMethodSecurity @RequiredArgsConstructor public class PigxResourceServerConfiguration { …… }
2.3、请求令牌的抽取逻辑:PigxBearerTokenExtractor
/** * 请求令牌的抽取逻辑 * @param urlProperties 对外暴露的接口列表 * @return BearerTokenExtractor */ @Bean public PigxBearerTokenExtractor pigBearerTokenExtractor(PermitAllUrlProperties urlProperties) { return new PigxBearerTokenExtractor(urlProperties); }
即获取请求中的token的相关逻辑
//BearerTokenResolver:是Spring Security中的一个接口,用于解析Bearer Token,并将其返回 //该接口定义了一个方法 resolve(HttpServletRequest request),用于从请求中提取出 Bearer Token,需要在实现类中重写 //这里pigx自定义了一个类PigxBearerTokenExtractor作为BearerTokenResolver的实现类,用于解析Bearer Token public class PigxBearerTokenExtractor implements BearerTokenResolver { //定义处理Bearer Token 的正则表达式模式 private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-:._~+/]+=*)$", Pattern.CASE_INSENSITIVE); //是否允许从表单编码的请求体参数中获取 Token。 private boolean allowFormEncodedBodyParameter = false; //是否允许从 URI 查询参数中获取 Token private boolean allowUriQueryParameter = true; //存储 Bearer Token 的请求头名称,默认为 Authorization //常量值public static final String AUTHORIZATION = "Authorization"; private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION; //用于检查当前请求路径是否应被忽略的路径匹配器 private final PathMatcher pathMatcher = new AntPathMatcher(); //存储可忽略 URL 列表 private final PermitAllUrlProperties urlProperties; //构造器传入属性配置类:PermitAllUrlProperties(存储对外暴露的接口列表) public PigxBearerTokenExtractor(PermitAllUrlProperties urlProperties) { this.urlProperties = urlProperties; } //对token的抽取方法 @Override public String resolve(HttpServletRequest request) { //获取当前请求的url String requestUri = request.getRequestURI(); //去除上下文,获得相对路径 String relativePath = requestUri.substring(request.getContextPath().length()); //当前请求路径是否忽略 boolean match = urlProperties.getIgnoreUrls().stream().anyMatch(url -> pathMatcher.match(url, relativePath)); //当前请求路径忽略,返回null if (match) { return null; } //通过resolveFromAuthorizationHeader方法获取token 详情见2.3.1 final String authorizationHeaderToken = resolveFromAuthorizationHeader(request); //通过isParameterTokenSupportedForRequest方法从请求参数中解析出 Bearer Token,并返回 Token 字符串详情见2.3.2 //通过isParameterTokenSupportedForRequest 判断当前请求是否支持从请求参数中获取 Token 详情见2.3.3 final String parameterToken = isParameterTokenSupportedForRequest(request) ? resolveFromRequestParameters(request) : null; //请求头中获取到token if (authorizationHeaderToken != null) { //请求参数中也有token,则抛出重复token if (parameterToken != null) { final BearerTokenError error = BearerTokenErrors .invalidRequest("Found multiple bearer tokens in the request"); throw new OAuth2AuthenticationException(error); } //返回请求头中的token return authorizationHeaderToken; } //检测是否支持参数中获取token(详情见2.3.4),并且判断参数中是否有token //如果支持参数中获取token,并且参数中有token则返回参数中的token if (parameterToken != null && isParameterTokenEnabledForRequest(request)) { return parameterToken; } return null; } //详情2.3.1 从请求头中解析出 Bearer Token,并返回 Token 字符串 private String resolveFromAuthorizationHeader(HttpServletRequest request) { //从请求头中获取请求头名称,默认为 Authorization的值 String authorization = request.getHeader(this.bearerTokenHeaderName); //不以不区分大小写的方式以 "bearer" 开头,则返回 null,表示未找到有效的 Bearer Token if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) { return null; } //通过正则表达式对 authorization 进行匹配 Matcher matcher = authorizationPattern.matcher(authorization); //果匹配失败,即 Bearer Token 格式不正确,则抛出 OAuth2AuthenticationException 异常,异常信息为 "Bearer token is malformed" if (!matcher.matches()) { BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed"); throw new OAuth2AuthenticationException(error); } //匹配成功,通过 matcher.group("token") 方法提取出 Token 字符串,并返回 return matcher.group("token"); } //详情2.3.2 从请求参数中解析出 Bearer Token,并返回 Token 字符串 private static String resolveFromRequestParameters(HttpServletRequest request) { //通过 request.getParameterValues("access_token") 方法获取名为 "access_token" 的请求参数的值,存储在 values 数组中 String[] values = request.getParameterValues("access_token"); //如果 values 为 null 或长度为 0,则返回 null,表示未找到有效的 Bearer Token if (values == null || values.length == 0) { return null; } //如果 values 的长度为 1,则直接返回第一个值(默认取第一个),即 Token 字符串 if (values.length == 1) { return values[0]; } //如果 values 的长度大于 1,表示请求中包含多个 Bearer Token,此时抛出 OAuth2AuthenticationException 异常,异常信息为 "Found multiple bearer tokens in the request" BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request"); throw new OAuth2AuthenticationException(error); } //详情2.3.3 判断当前请求是否支持从请求参数中获取 Token private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) { return (("POST".equals(request.getMethod()) && MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType())) || "GET".equals(request.getMethod())); } //详情2.3.4该方法的作用是判断是否允许在当前请求中通过请求参数获取 Token private boolean isParameterTokenEnabledForRequest(final HttpServletRequest request) { /* 满足情况: 1、allowFormEncodedBodyParameter 的值是否为 true,并且当前请求方法为 "POST",且请求的 Content-Type 为 "application/x-www-form-urlencoded 2、allowUriQueryParameter 的值是否为 true,并且当前请求方法为 "GET" */ return ((this.allowFormEncodedBodyParameter && "POST".equals(request.getMethod()) && MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType())) || (this.allowUriQueryParameter && "GET".equals(request.getMethod()))); } }
2.4、资源服务器异常处理resourceAuthExceptionEntryPoint
/** * 资源服务器异常处理 * @param objectMapper jackson 输出对象 * @param securityMessageSource 自定义国际化处理器 * @return ResourceAuthExceptionEntryPoint */ @Bean public ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint(ObjectMapper objectMapper, MessageSource securityMessageSource) { return new ResourceAuthExceptionEntryPoint(objectMapper, securityMessageSource); }
具体内容解析
/** * @author lengleng * @date 2019/2/1 * * 客户端异常处理 AuthenticationException 不同细化异常处理 */ //全参构造器,会生成一个带有所有 final 字段的构造函数 @RequiredArgsConstructor public class ResourceAuthExceptionEntryPoint implements AuthenticationEntryPoint { //进行 JSON 序列化 private final ObjectMapper objectMapper; //国际化消息处理 private final MessageSource messageSource; @Override @SneakyThrows //@SneakyThrows 是 Lombok 提供的注解,用于在方法上抛出异常时,自动将该异常包装为 RuntimeException 抛出 public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { /// 设置响应的字符编码为UTF8,内容类型为 JSON response.setCharacterEncoding(CommonConstants.UTF8); response.setContentType(ContentType.JSON.getValue()); //创建一个封装错误信息的对象 R<String> result = new R<>(); //设置code为失败 //常量:Integer FAIL = 1; result.setCode(CommonConstants.FAIL); //设置响应状态码为未授权401 //UNAUTHORIZED(401, HttpStatus.Series.CLIENT_ERROR, "Unauthorized"), response.setStatus(HttpStatus.UNAUTHORIZED.value()); 如果存在认证异常,设置错误消息为 "error",数据为认证异常的消 if (authException != null) { result.setMsg("error"); result.setData(authException.getMessage()); } // 针对令牌过期返回特殊的 424 if (authException instanceof InvalidBearerTokenException || authException instanceof InsufficientAuthenticationException) { //设置响应状态码为 424(FAILED_DEPENDENCY) //FAILED_DEPENDENCY(424, HttpStatus.Series.CLIENT_ERROR, "Failed Dependency") response.setStatus(HttpStatus.FAILED_DEPENDENCY.value()); 设置特定的错误消息 result.setMsg(this.messageSource.getMessage("OAuth2ResourceOwnerBaseAuthenticationProvider.tokenExpired", null, LocaleContextHolder.getLocale())); //如果用户令牌过期 修改code result.setCode(TOKEN_EXPIRED_FAIL); } //获取响应的输出流,通过该输出流可以向客户端发送数据 PrintWriter printWriter = response.getWriter(); //使用 Jackson 的 ObjectMapper 将 result 对象序列化为 JSON 格式的字符串 //将序列化后的 JSON 字符串添加到输出流中,以便将其发送给客户端 printWriter.append(objectMapper.writeValueAsString(result)); } }
2.5、资源服务器toke内省处理器opaqueTokenIntrospector
自定义认证器,用于通过传递的令牌进行身份验证
/** * 资源服务器toke内省处理器 * @param authorizationService token 存储实现 * @return TokenIntrospector */ @Bean public OpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2AuthorizationService authorizationService) { return new PigxCustomOpaqueTokenIntrospector(authorizationService); }
具体解析
/** * @author lengleng * @date 2022/5/28 */ @Slf4j @RequiredArgsConstructor public class PigxCustomOpaqueTokenIntrospector implements OpaqueTokenIntrospector { private final OAuth2AuthorizationService authorizationService; //用于根据传递的令牌进行身份验证 @Override public OAuth2AuthenticatedPrincipal introspect(String token) { //通过OAuth2AuthorizationService的实现类去获取对应的token 详情见2.5.1 OAuth2Authorization oldAuthorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN); //如果找不到与令牌关联的授权信息,则抛出 InvalidBearerTokenException 异常,表示令牌无效 if (Objects.isNull(oldAuthorization)) { throw new InvalidBearerTokenException(token); } // 客户端模式默认返回 //判断授权类型是否为客户端模式 //public static final AuthorizationGrantType CLIENT_CREDENTIALS = new AuthorizationGrantType("client_credentials"); if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(oldAuthorization.getAuthorizationGrantType())) { //默认返回一个 PigxClientCredentialsOAuth2AuthenticatedPrincipal 对象。该对象包含了传递的授权信息的属性、空权限列表以及授权主体名称。 return new PigxClientCredentialsOAuth2AuthenticatedPrincipal(oldAuthorization.getAttributes(), AuthorityUtils.NO_AUTHORITIES, oldAuthorization.getPrincipalName()); } //如果授权类型不是客户端模式,则获取所有实现了 PigxUserDetailsService 接口的 Bean 对象,并过滤出支持当前授权信息的 PigxUserDetailsService 对象 //这里会获取到对应的PigxUserDetailsService的实现类 Map<String, PigxUserDetailsService> userDetailsServiceMap = SpringContextHolder .getBeansOfType(PigxUserDetailsService.class); //选择支持度最高的 PigxUserDetailsService 对象(根据 Ordered 接口的顺序进行比较) Optional<PigxUserDetailsService> optional = userDetailsServiceMap.values().stream() .filter(service -> service.support(Objects.requireNonNull(oldAuthorization).getRegisteredClientId(), oldAuthorization.getAuthorizationGrantType().getValue())) .max(Comparator.comparingInt(Ordered::getOrder)); //获取用户信息 UserDetails userDetails = null; try { Object principal = Objects.requireNonNull(oldAuthorization).getAttributes().get(Principal.class.getName()); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) principal; Object tokenPrincipal = usernamePasswordAuthenticationToken.getPrincipal(); userDetails = optional.get().loadUserByUser((PigxUser) tokenPrincipal); } catch (UsernameNotFoundException notFoundException) { log.warn("用户不不存在 {}", notFoundException.getLocalizedMessage()); throw notFoundException; } catch (Exception ex) { log.error("资源服务器 introspect Token error {}", ex.getLocalizedMessage()); } // 注入客户端信息,方便上下文中获取 PigxUser pigxUser = (PigxUser) userDetails; Objects.requireNonNull(pigxUser).getAttributes().put(SecurityConstants.CLIENT_ID, oldAuthorization.getRegisteredClientId()); return pigxUser; } }
2.5.1 authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
其实现类有三个,我们用的是Pix提供的实现类PigxRedisOAuth2AuthorizationService
其中的方法如下,即从redis中去获取对应的token信息
@Override @Nullable public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) { Assert.hasText(token, "token cannot be empty"); Assert.notNull(tokenType, "tokenType cannot be empty"); redisTemplate.setValueSerializer(RedisSerializer.java()); return (OAuth2Authorization) redisTemplate.opsForValue().get(buildKey(tokenType.getValue(), token)); }
3、PigxResourceServerConfiguration
资源服务器认证授权配置
@Slf4j @EnableWebSecurity @EnableMethodSecurity @RequiredArgsConstructor public class PigxResourceServerConfiguration { protected final ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint; private final PermitAllUrlProperties permitAllUrl; private final PigxBearerTokenExtractor pigxBearerTokenExtractor; private final OpaqueTokenIntrospector customOpaqueTokenIntrospector; @Bean @Order(Ordered.HIGHEST_PRECEDENCE) SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { AntPathRequestMatcher[] requestMatchers = permitAllUrl.getIgnoreUrls().stream().map(AntPathRequestMatcher::new) .collect(Collectors.toList()).toArray(new AntPathRequestMatcher[] {}); http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.requestMatchers(requestMatchers).permitAll() .anyRequest().authenticated()) .oauth2ResourceServer( oauth2 -> oauth2.opaqueToken(token -> token.introspector(customOpaqueTokenIntrospector)) .authenticationEntryPoint(resourceAuthExceptionEntryPoint) .bearerTokenResolver(pigxBearerTokenExtractor)) .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) .csrf(AbstractHttpConfigurer::disable); return http.build(); } }
这段代码是一个 Java 类 PigxResourceServerConfiguration
,它配置了 Spring Security 的资源服务器。
首先,类中定义了一些依赖注入的属性:
resourceAuthExceptionEntryPoint
:用于处理资源服务器的异常入口点。permitAllUrl
:用于配置允许所有请求的 URL 列表。pigxBearerTokenExtractor
:用于从请求中提取 Bearer Token。customOpaqueTokenIntrospector
:自定义的不透明令牌内省器。
接下来,使用 @Bean
注解标记了一个方法 securityFilterChain
,该方法返回一个 SecurityFilterChain
对象。该方法的作用是配置 Spring Security 的安全过滤器链。
在 securityFilterChain
方法中,首先根据 permitAllUrl
中的忽略 URL 列表创建了一个 AntPathRequestMatcher
数组 requestMatchers
。这里使用了 Stream API 将忽略 URL 列表转换为 AntPathRequestMatcher
数组。
然后,通过调用 authorizeHttpRequests()
方法配置了请求的授权规则。其中,使用 requestMatchers(requestMatchers).permitAll().anyRequest().authenticated()
来配置了忽略 URL 列表的请求允许访问,而其他请求需要进行身份验证。
接着,使用 oauth2ResourceServer()
方法配置了 OAuth2 资源服务器。通过调用 opaqueToken()
方法设置了自定义的不透明令牌内省器,并使用 bearerTokenResolver()
方法设置了用于解析 Bearer Token 的 pigxBearerTokenExtractor
。
继续,使用 headers()
方法配置了 HTTP 头部,通过调用 frameOptions()
方法禁用了 X-Frame-Options。
最后,使用 csrf()
方法禁用了 CSRF(跨站请求伪造)保护,并调用 http.build()
方法构建并返回了安全过滤器链。
这段代码的作用是配置 Spring Security 的资源服务器,定义了请求的授权规则、OAuth2 资源服务器和一些其他配置。
4、PigxFeignClientConfiguration.class
public class PigxFeignClientConfiguration { /** * 注入 oauth2 feign token 增强 * @param tokenResolver token获取处理器 * @return 拦截器 */ @Bean public RequestInterceptor oauthRequestInterceptor(BearerTokenResolver tokenResolver) { return new PigxOAuthRequestInterceptor(tokenResolver); } @Bean public RequestInterceptor clientToCRequestInterceptor() { return new PigxClientToCRequestInterceptor(); } }
4.1 、oauthRequestInterceptor
方法
该类的作用是在发送请求之前拦截并修改请求模板(RequestTemplate
)
/** * 注入 oauth2 feign token 增强 * @param tokenResolver token获取处理器 * @return 拦截器 */ @Bean public RequestInterceptor oauthRequestInterceptor(BearerTokenResolver tokenResolver) { return new PigxOAuthRequestInterceptor(tokenResolver); }
具体详解:
/** * oauth2 feign token传递 * * 重新 OAuth2FeignRequestInterceptor ,官方实现部分常见不适用 * * @author lengleng * @date 2022/5/29 */ @Slf4j @RequiredArgsConstructor public class PigxOAuthRequestInterceptor implements RequestInterceptor { private final BearerTokenResolver tokenResolver; /** * Create a template with the header of provided name and extracted extract </br> * * 1. 如果使用 非web 请求,header 区别 </br> * * 2. 根据authentication 还原请求token * @param template */ @Override public void apply(RequestTemplate template) { Collection<String> fromHeader = template.headers().get(SecurityConstants.FROM); // 带from 请求直接跳过 if (CollUtil.isNotEmpty(fromHeader) && fromHeader.contains(SecurityConstants.FROM_IN)) { return; } // 非web 请求直接跳过 if (WebUtils.getRequest() == null) { return; } HttpServletRequest request = WebUtils.getRequest(); // 避免请求参数的 query token 无法传递 String token = tokenResolver.resolve(request); if (StrUtil.isBlank(token)) { return; } //添加token信息 template.header(HttpHeaders.AUTHORIZATION, String.format("%s %s", OAuth2AccessToken.TokenType.BEARER.getValue(), token)); } }
4.2、 clientToCRequestInterceptor
方法
@Bean public RequestInterceptor clientToCRequestInterceptor() { return new PigxClientToCRequestInterceptor(); }
具体详解:
/** * TOC 客户标识传递 * * @author lengleng * @date 2023/3/17 */ @Slf4j public class PigxClientToCRequestInterceptor implements RequestInterceptor { /** * Called for every request. Add data using methods on the supplied * {@link RequestTemplate}. * @param template */ public void apply(RequestTemplate template) { String reqVersion = WebUtils.getRequest() != null ? WebUtils.getRequest().getHeader(SecurityConstants.HEADER_TOC) : null; if (StrUtil.isNotBlank(reqVersion)) { log.debug("feign add header toc :{}", reqVersion); template.header(SecurityConstants.HEADER_TOC, reqVersion); } } }