> 文章列表 > 用starter实现Oauth2中资源服务的统一配置

用starter实现Oauth2中资源服务的统一配置

用starter实现Oauth2中资源服务的统一配置

一、前言

Oauth2中的资源服务Resource需要验证令牌,就要配置令牌的解码器JwtDecoder,认证服务器的公钥等等。如果有多个资源服务Resource,就要重复配置,比较繁锁。把公共的配置信息抽取出来,制成starter,可以极大地简化操作。

  • 未使用starter的原来配置

在这里插入图片描述

在这里插入图片描述

二、制作starter

详细步骤参考:自定义启动器 Starter【保姆级教程】

1、完整结构图

在这里插入图片描述

2、外部引用模块

  • 名称:tuwer-oauth2-config-spring-boot-starter

  • 普通的 maven 项目

  • 资源服务中引入该模块的依赖即可

  • 模块中只有一个pom.xml文件,其余的都可删除

  • pom.xml
<?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.tuwer</groupId><artifactId>tuwer-oauth2-config-spring-boot-starter</artifactId><version>1.0-SNAPSHOT</version><description>oauth2-config启动器</description><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><!-- 编译编码 --><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!-- 自动配置模块 --><dependency><groupId>com.tuwer</groupId><artifactId>tuwer-oauth2-config-spring-boot-starter-autoconfigure</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies>
</project>

3、自动配置模块

  • 核心模块

  • 名称:tuwer-oauth2-config-spring-boot-starter-autoconfigure

  • spring boot 项目

  • pom.xml
<?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"><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.7</version></parent><modelVersion>4.0.0</modelVersion><groupId>com.tuwer</groupId><artifactId>tuwer-oauth2-config-spring-boot-starter-autoconfigure</artifactId><version>1.0-SNAPSHOT</version><description>oauth2-config启动器自动配置模块</description><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><!-- 编译编码 --><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!-- 基础启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><!-- 资源服务器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><!-- web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.22</version></dependency></dependencies>
</project>
  • handler(拒绝访问、认证失败)处理类
package com.tuwer.config.handler;import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;/*** <p>拒绝访问处理器</p>** @author 土味儿* Date 2022/5/11* @version 1.0*/
public class SimpleAccessDeniedHandler implements AccessDeniedHandler {@SneakyThrows@Overridepublic void handle(HttpServletRequest request,HttpServletResponse response,AccessDeniedException accessDeniedException) throws IOException, ServletException {//todo your businessHashMap<String, String> map = new HashMap<>(2);map.put("uri", request.getRequestURI());map.put("msg", "拒绝访问");response.setStatus(HttpServletResponse.SC_FORBIDDEN);response.setCharacterEncoding("utf-8");response.setContentType(MediaType.APPLICATION_JSON_VALUE);ObjectMapper objectMapper = new ObjectMapper();String resBody = objectMapper.writeValueAsString(map);PrintWriter printWriter = response.getWriter();printWriter.print(resBody);printWriter.flush();printWriter.close();}
}
package com.tuwer.config.handler;import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.web.AuthenticationEntryPoint;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;/*** <p>认证失败处理器</p>** @author 土味儿* Date 2022/5/11* @version 1.0*/
public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {@SneakyThrows@Overridepublic void commence(HttpServletRequest request,HttpServletResponse response,AuthenticationException authException) throws IOException, ServletException {HashMap<String, String> map = new HashMap<>(2);if (authException instanceof InvalidBearerTokenException) {// 令牌失效System.out.println("token失效");//todo token处理逻辑}map.put("uri", request.getRequestURI());map.put("msg", "认证失败");if (response.isCommitted()) {return;}response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.setStatus(HttpServletResponse.SC_ACCEPTED);response.setCharacterEncoding("utf-8");response.setContentType(MediaType.APPLICATION_JSON_VALUE);ObjectMapper objectMapper = new ObjectMapper();String resBody = objectMapper.writeValueAsString(map);PrintWriter printWriter = response.getWriter();printWriter.print(resBody);printWriter.flush();printWriter.close();}
}
  • property(权限、令牌)属性类

    权限属性类:AuthProperty,通过application.yml来配置权限,避免在自动配置类中以硬编码的形式写入权限

package com.tuwer.config.property;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.CollectionUtils;import java.util.Set;/*** <p>权限属性类</p>** @author 土味儿* Date 2022/9/2* @version 1.0*/
@Data
@ConfigurationProperties(prefix = "resource-auth")
public final class AuthProperty {private Authority authority;/*** 权限*/@Datapublic static class Authority {private Set<String> roles;private Set<String> scopes;private Set<String> auths;}/*** 组装权限字符串* 目的:给 hasAnyAuthority() 方法生成参数* @return*/public String getAllAuth() {StringBuilder res = new StringBuilder();// 角色Set<String> roles = this.authority.roles;// 角色非空时if (!CollectionUtils.isEmpty(roles)) {for (String role : roles) {res.append(role).append("','");}// 循环结果后,生成类似:x ',' y ',' z ','}// 范围Set<String> scopes = this.authority.scopes;// 非空时if (!CollectionUtils.isEmpty(scopes)) {for (String scope : scopes) {res.append("SCOPE_" + scope).append("','");}// 循环结果后,生成类似:x ',' y ',' z ',' SCOPE_a ',' SCOPE_b ',' SCOPE_c ','}// 细粒度权限Set<String> auths = this.authority.auths;// 非空时if (!CollectionUtils.isEmpty(auths)) {for (String auth : auths) {res.append(auth).append("','");}// 循环结果后,生成类似:x ',' y ',' z ',' SCOPE_a ',' SCOPE_b ',' SCOPE_c ',' l ',' m ',' n ','}// 如果res不为空,去掉最后多出的三个字符 ','int len = res.length();if (len > 3) {res.delete(len - 3, len);}return res.toString();}
}

在这里插入图片描述

在这里插入图片描述

package com.tuwer.config.property;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;/*** <p>属性配置类</p>** @author 土味儿* Date 2022/5/11* @version 1.0*/
@Data
@ConfigurationProperties(prefix = "jwt")
public class JwtProperty {/*======= 配置示例 ======# 自定义 jwt 配置jwt:cert-info:# 证书存放位置public-key-location: myKey.cerclaims:# 令牌的鉴发方:即授权服务器的地址issuer: http://os:9000*//*** 证书信息(内部静态类)* 证书存放位置...*/private CertInfo certInfo;/*** 证书声明(内部静态类)* 发证方...*/private Claims claims;@Datapublic static class Claims {/*** 发证方*/private String issuer;/*** 有效期*///private Integer expiresAt;}@Datapublic static class CertInfo {/*** 证书存放位置*/private String publicKeyLocation;}
}
  • 解码器自动配置类
package com.tuwer.config;import com.nimbusds.jose.jwk.RSAKey;
import com.tuwer.config.property.JwtProperty;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;import javax.annotation.Resource;
import java.io.InputStream;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.Collection;/*** <p>自定义jwt解码器</p>** @author 土味儿* Date 2022/5/11* @version 1.0*/
@EnableConfigurationProperties(JwtProperty.class)
@Configuration
public class JwtDecoderConfiguration {/*** 注入 JwtProperties 属性配置类*/@Resourceprivate JwtProperty jwtProperty;/***  校验jwt发行者 issuer 是否合法** @return the jwt issuer validator*/@BeanJwtIssuerValidator jwtIssuerValidator() {return new JwtIssuerValidator(this.jwtProperty.getClaims().getIssuer());}/***  校验jwt是否过期** @return the jwt timestamp validator
*/
/*    @BeanJwtTimestampValidator jwtTimestampValidator() {System.out.println("检测令牌是否过期!"+ LocalDateTime.now());return new JwtTimestampValidator(Duration.ofSeconds((long) this.jwtProperties.getClaims().getExpiresAt()));}*//*** jwt token 委托校验器,集中校验的策略{@link OAuth2TokenValidator}** // @Primary:自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否则将抛出异常* @param tokenValidators the token validators* @return the delegating o auth 2 token validator*/@Primary@Bean({"delegatingTokenValidator"})public DelegatingOAuth2TokenValidator<Jwt> delegatingTokenValidator(Collection<OAuth2TokenValidator<Jwt>> tokenValidators) {return new DelegatingOAuth2TokenValidator<>(tokenValidators);}/*** 基于Nimbus的jwt解码器,并增加了一些自定义校验策略** // @Qualifier 当有多个相同类型的bean存在时,指定注入* @param validator DelegatingOAuth2TokenValidator<Jwt> 委托token校验器* @return the jwt decoder*/@SneakyThrows@Beanpublic JwtDecoder jwtDecoder(@Qualifier("delegatingTokenValidator")DelegatingOAuth2TokenValidator<Jwt> validator) {// 指定 X.509 类型的证书工厂CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");// 读取cer公钥证书来配置解码器String publicKeyLocation = this.jwtProperty.getCertInfo().getPublicKeyLocation();// 获取证书文件输入流ClassPathResource resource = new ClassPathResource(publicKeyLocation);InputStream inputStream = resource.getInputStream();// 得到证书X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream);// 解析RSAKey rsaKey = RSAKey.parse(certificate);// 得到公钥RSAPublicKey key = rsaKey.toRSAPublicKey();// 构造解码器NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withPublicKey(key).build();// 注入自定义JWT校验逻辑nimbusJwtDecoder.setJwtValidator(validator);return nimbusJwtDecoder;}
}
  • 主自动配置类:安全权限等
package com.tuwer.config;import com.tuwer.config.handler.SimpleAccessDeniedHandler;
import com.tuwer.config.handler.SimpleAuthenticationEntryPoint;
import com.tuwer.config.property.AuthProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;import javax.annotation.Resource;/*** <p>资源服务器配置</p>* 当解码器JwtDecoder存在时生效** @author 土味儿* Date 2022/5/11* @version 1.0*/
@ConditionalOnBean(JwtDecoder.class)
@EnableConfigurationProperties(AuthProperty.class)
@Configuration
public class AutoConfiguration {@Resourceprivate AuthProperty authProperty;/*** 资源管理器配置** @param http the http* @return the security filter chain* @throws Exception the exception*/@BeanSecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception {// 拒绝访问处理器 401SimpleAccessDeniedHandler accessDeniedHandler = new SimpleAccessDeniedHandler();// 认证失败处理器 403SimpleAuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();return http// security的session生成策略改为security不主动创建session即STALELESS.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 允许【pc客户端】或【其它微服务】访问.authorizeRequests()//.antMatchers("/**").hasAnyAuthority("SCOPE_client_pc","SCOPE_micro_service")// 从配置文件中读取权限信息.antMatchers("/**").hasAnyAuthority(authProperty.getAllAuth())// 其余请求都需要认证.anyRequest().authenticated().and()// 异常处理.exceptionHandling(exceptionConfigurer -> exceptionConfigurer// 拒绝访问.accessDeniedHandler(accessDeniedHandler)// 认证失败.authenticationEntryPoint(authenticationEntryPoint))// 资源服务.oauth2ResourceServer(resourceServer -> resourceServer.accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(authenticationEntryPoint).jwt()).build();}/*** JWT个性化解析** @return*/@BeanJwtAuthenticationConverter jwtAuthenticationConverter() {JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
//        如果不按照规范  解析权限集合Authorities 就需要自定义key
//        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("scopes");
//        OAuth2 默认前缀是 SCOPE_     Spring Security 是 ROLE_
//        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);// 用户名 可以放subjwtAuthenticationConverter.setPrincipalClaimName(JwtClaimNames.SUB);return jwtAuthenticationConverter;}/*** 开放一些端点的访问控制* 不需要认证就可以访问的端口* @return*/@BeanWebSecurityCustomizer webSecurityCustomizer() {return web -> web.ignoring().antMatchers("/actuator/**");}
}
  • spring.factories

指明自动配置类的地址,在 resources 目录下编写一个自己的 META-INF\\spring.factories;有两个自动配置类,中间用逗号分开

注意点

如果同一个组中有多个starter,自动配置类名称不要相同;如果相同,将只有一个配置类生效,其余的将失效。

如:

  • starterA中:com.tuwer.config.AutoConfiguration

  • starterB中:就不要再用 com.tuwer.config.AutoConfiguration 名称,可以改为 com.tuwer.config.AutoConfigurationB

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\com.tuwer.config.JwtDecoderConfiguration,\\com.tuwer.config.AutoConfiguration
  • application.yml

令牌、权限的配置可以放在引用starter的资源服务中;如果每个资源服务的配置都一样,可以放在starter中

# 自定义 jwt 配置(校验jwt)
jwt:cert-info:# 公钥证书存放位置public-key-location: myjks.cerclaims:# 令牌的鉴发方:即授权服务器的地址issuer: http://os.com:9000# 自定义权限配置
resource-auth:# 权限authority:# 角色名称;不用加ROlE_,提取用户角色权限时,自动加roles:# 授权范围;不用加SCOPE_,保持与认证中心中定义的一致即可;# 后台自动加 SCOPE_scopes:- client_pc- micro_service# 细粒度权限auths:
  • 公钥

把认证中心的公钥文件myjks.cer放到resources目录下

4、install

把starter安装install到本地maven仓库中

在这里插入图片描述

在这里插入图片描述

三、使用starter

1、引入starter依赖

在资源服务中引入 tuwer-oauth2-config-spring-boot-starter

在这里插入图片描述

2、application.yml

在这里插入图片描述

3、删除资源服务中原文件

在这里插入图片描述