Oauth2中的资源服务Resource需要验证令牌,就要配置令牌的解码器JwtDecoder,认证服务器的公钥等等。如果有多个资源服务Resource,就要重复配置,比较繁锁。把公共的配置信息抽取出来,制成starter,可以极大地简化操作。
详细步骤参考:自定义启动器 Starter【保姆级教程】
名称:
tuwer-oauth2-config-spring-boot-starter
普通的 maven 项目
资源服务中引入该模块的依赖即可
模块中只有一个pom.xml文件,其余的都可删除
<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.0modelVersion>
<groupId>com.tuwergroupId>
<artifactId>tuwer-oauth2-config-spring-boot-starterartifactId>
<version>1.0-SNAPSHOTversion>
<description>oauth2-config启动器description>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
properties>
<dependencies>
<dependency>
<groupId>com.tuwergroupId>
<artifactId>tuwer-oauth2-config-spring-boot-starter-autoconfigureartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
dependencies>
project>
核心模块
名称:
tuwer-oauth2-config-spring-boot-starter-autoconfigure
spring boot 项目
<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.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.7version>
parent>
<modelVersion>4.0.0modelVersion>
<groupId>com.tuwergroupId>
<artifactId>tuwer-oauth2-config-spring-boot-starter-autoconfigureartifactId>
<version>1.0-SNAPSHOTversion>
<description>oauth2-config启动器自动配置模块description>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-oauth2-resource-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.22version>
dependency>
dependencies>
project>
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;
/**
* 拒绝访问处理器
*
* @author 土味儿
* Date 2022/5/11
* @version 1.0
*/
public class SimpleAccessDeniedHandler implements AccessDeniedHandler {
@SneakyThrows
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException
) throws IOException, ServletException {
//todo your business
HashMap<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;
/**
* 认证失败处理器
*
* @author 土味儿
* Date 2022/5/11
* @version 1.0
*/
public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {
@SneakyThrows
@Override
public 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;
/**
* 权限属性类
*
* @author 土味儿
* Date 2022/9/2
* @version 1.0
*/
@Data
@ConfigurationProperties(prefix = "resource-auth")
public final class AuthProperty {
private Authority authority;
/**
* 权限
*/
@Data
public 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;
/**
* 属性配置类
*
* @author 土味儿
* Date 2022/5/11
* @version 1.0
*/
@Data
@ConfigurationProperties(prefix = "jwt")
public class JwtProperty {
/*
======= 配置示例 ======
# 自定义 jwt 配置
jwt:
cert-info:
# 证书存放位置
public-key-location: myKey.cer
claims:
# 令牌的鉴发方:即授权服务器的地址
issuer: http://os:9000
*/
/**
* 证书信息(内部静态类)
* 证书存放位置...
*/
private CertInfo certInfo;
/**
* 证书声明(内部静态类)
* 发证方...
*/
private Claims claims;
@Data
public static class Claims {
/**
* 发证方
*/
private String issuer;
/**
* 有效期
*/
//private Integer expiresAt;
}
@Data
public 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;
/**
* 自定义jwt解码器
*
* @author 土味儿
* Date 2022/5/11
* @version 1.0
*/
@EnableConfigurationProperties(JwtProperty.class)
@Configuration
public class JwtDecoderConfiguration {
/**
* 注入 JwtProperties 属性配置类
*/
@Resource
private JwtProperty jwtProperty;
/**
* 校验jwt发行者 issuer 是否合法
*
* @return the jwt issuer validator
*/
@Bean
JwtIssuerValidator jwtIssuerValidator() {
return new JwtIssuerValidator(this.jwtProperty.getClaims().getIssuer());
}
/*
*
* 校验jwt是否过期
*
* @return the jwt timestamp validator
*/
/* @Bean
JwtTimestampValidator 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 委托token校验器
* @return the jwt decoder
*/
@SneakyThrows
@Bean
public 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;
/**
* 资源服务器配置
* 当解码器JwtDecoder存在时生效
*
* @author 土味儿
* Date 2022/5/11
* @version 1.0
*/
@ConditionalOnBean(JwtDecoder.class)
@EnableConfigurationProperties(AuthProperty.class)
@Configuration
public class AutoConfiguration {
@Resource
private AuthProperty authProperty;
/**
* 资源管理器配置
*
* @param http the http
* @return the security filter chain
* @throws Exception the exception
*/
@Bean
SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception {
// 拒绝访问处理器 401
SimpleAccessDeniedHandler accessDeniedHandler = new SimpleAccessDeniedHandler();
// 认证失败处理器 403
SimpleAuthenticationEntryPoint 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
*/
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
// 如果不按照规范 解析权限集合Authorities 就需要自定义key
// jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("scopes");
// OAuth2 默认前缀是 SCOPE_ Spring Security 是 ROLE_
// jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
// 用户名 可以放sub
jwtAuthenticationConverter.setPrincipalClaimName(JwtClaimNames.SUB);
return jwtAuthenticationConverter;
}
/**
* 开放一些端点的访问控制
* 不需要认证就可以访问的端口
* @return
*/
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().antMatchers(
"/actuator/**"
);
}
}
指明自动配置类的地址,在
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
令牌、权限的配置可以放在引用starter的资源服务中;如果每个资源服务的配置都一样,可以放在starter中
# 自定义 jwt 配置(校验jwt)
jwt:
cert-info:
# 公钥证书存放位置
public-key-location: myjks.cer
claims:
# 令牌的鉴发方:即授权服务器的地址
issuer: http://os.com:9000
# 自定义权限配置
resource-auth:
# 权限
authority:
# 角色名称;不用加ROlE_,提取用户角色权限时,自动加
roles:
# 授权范围;不用加SCOPE_,保持与认证中心中定义的一致即可;
# 后台自动加 SCOPE_
scopes:
- client_pc
- micro_service
# 细粒度权限
auths:
把认证中心的公钥文件myjks.cer
放到resources目录下
把starter安装install到本地maven仓库中
在资源服务中引入 tuwer-oauth2-config-spring-boot-starter