Spring oauth2 资源服务器 - Servlet
2024-09-14 本文已影响0人
轻轻敲醒沉睡的心灵
前面记录了认证服务器 Spring Authorization Server的配置。这里我们记录一下 资源服务器ResourceServer。其实资源服务器的改动并不大,我用这个是分成了 2个步骤 来理解的。
1. 资源服务器的2个作用
- 1是验证Token,主要是 验证签名、Token的有效期以及Token中的一些字段信息。
当然token中也可以包含权限信息,可以用oauth2.1自带的逻辑来验证权限,但是这个效果不太理想,所以将验证权限单独拿出来,放到第2步了; - 2是验证权限,权限一般放缓存,但要想拿权限,就要从token中解析一个key用,所以第一步才要验签来保证key的准确。拿到权限后就可以自己写验证逻辑了
2. 资源服务器和jwt的流程
官方文档
我们先看一下资源服务器的整个流程:

- 当用户提交一个Token时,BearerTokenAuthenticationFilter通过从HttpServletRequest中提取出来的Token来创建一个BearerTokenAuthenticationToken。
- BearerTokenAuthenticationToken被传递到AuthenticationManager调用Authenticated方法进行身份验证。
- 如果失败则调用失败处理器
- 如果成功则可以继续访问资源接口
jwt的验证流程

- 将Token提交给ProviderManager。
- ProviderManager会找到JwtAuthenticationProvider,并调用authenticate方法。
- authenticate方法里面通过JwtDecoder来验证token,并转换为Jwt对象。
- authenticate方法里面通过JwtAuthenticationConverter将Jwt对象转换为已验证的Token对象。
- 验证成功就返回JwtAuthenticationToken对象。
3. 资源服务器配置
资源服务器有2种使用场景,当把微服务作为资源时,资源服务器有2种加法,
- 一是直接用在微服务上,每个微服务都加;
- 一是将资源服务器加在网关上,在网关统一鉴权。
在微服务鉴权一般是基于Servlet的,而在网关鉴权(如果网关是Gateway)是基于webFlux的,两者在一些api上是有区别的。
这里我们先说基于servlet的。Springboot版本是3.3.3。
3.1 pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>
3.2 ResourceServer配置类
要想配置这个呢,必须要清楚几点:
-
1. jwt验证来源
jwt的解析需要公钥的,我们可以用认证服务器的接口(issuer-uri
或者jwk-set-uri
)获取元数据,从而拿到公钥。也可以将公钥配置在资源服务器中使用。公钥验签... -
2. jwtDecoder
这个就是解析jwt的,也就是验证jwt的。jar包提供了默认解析,我们也可以 实现解析接口,自己写逻辑。 -
3. jwtAuthenticationConverter
这是个jwt转换,主要干什么呢?
OAuth2授权服务器发出的JWT的claim中通常会有一个 scope 或 scp 属性,表明它被授予的scope(或权限),例如:scope: ["s1", "s2"]。而资源服务器默认将scope内容转换成一个权限列表,并且在每个scope前面加上 "SCOPE_" 字符串,变成:"SCOPE_s1","SCOPE_s2"。然后用这个来做权限对比。所以,如果我们使用默认的权限验证方式的话,是这么写的:
request.requestMatchers("/user/list").hasAuthority("SCOPE_s1")
而这个jwt转换,可以配置2个地方:- 配置权限在claim的哪个字段中(不一定在scope,可以配置成 perm);
- 配置权限列表用不用前缀,用哪个前缀(可以不用前缀,也可以将前缀配置成perm_);
实际开发中,我没用这个进行权限鉴定,所以一般不用配这个。
这个转换一般在资源服务器初始化的时候就会执行。
- 配置权限列表用不用前缀,用哪个前缀(可以不用前缀,也可以将前缀配置成perm_);
-
4. AuthorizationManager
这个就很重要了,我权限鉴定的逻辑都在这个里面。
3.2.1 最简单的配置
- 配置文件application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
- 配置类ResourceServerConfig
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuerUri;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.httpBasic(AbstractHttpConfigurer::disable);
// csrf关闭
http.csrf(csrf -> csrf.disable());
// 跨域处理
http.cors(Customizer.withDefaults());
// 资源服务器配置
http.oauth2ResourceServer(server -> server
// 使用jwt默认配置
.jwt(Customizer.withDefaults())
);
http.authorizeHttpRequests(request -> request
// 放行接口
.requestMatchers(
"/webjars/**",
"/doc.html",
"/swagger-resources/**",
"/v3/api-docs/**",
"/swagger-ui/**",
"/actuator/**",
"/instances/**").permitAll()
// 需要指定权限的接口
.requestMatchers("/user/list").hasAuthority("SCOPE_s1")
// 其他都需要登陆鉴权
.anyRequest().authenticated()
);
return http.build();
}
}
基本上都是默认的,资源服务器会自动拿token,验证签名的;还放行了一些接口。
3.2.2 加入自定义配置,我一般用这个
注意:若在资源服务器配置了公钥,就不用issuer-uri
了。
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuerUri;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.httpBasic(AbstractHttpConfigurer::disable);
// csrf关闭
http.csrf(csrf -> csrf.disable());
// 跨域处理
http.cors(Customizer.withDefaults());
// 资源服务器配置
http.oauth2ResourceServer(server -> server
// 权限不通过时,自定义返回
.accessDeniedHandler(new MyAccessDeniedHandler())
// 未登录或者登陆验证失败时(token有问题),自定义返回
.authenticationEntryPoint(new MyAuthenticationEntryPoint())
// 使用jwt默认配置
// .jwt(Customizer.withDefaults())
// jwt自定义校验
.jwt(jwt -> jwt
// 当无法提供issuer-uri的时候,可以拿到jwk,包含有私钥
// 可以不在这配置,在decoder中也可以配置从什么地方拿私钥验签
// .jwkSetUri("http://127.0.0.1:9001/oauth2/oauth2/jwks")
.decoder(jwtDecoder())
// 指定jwt转换
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
// 权限配置
http.authorizeHttpRequests(request -> request
// 放行接口
.requestMatchers(
"/webjars/**",
"/doc.html",
"/swagger-resources/**",
"/v3/api-docs/**",
"/swagger-ui/**",
"/actuator/**",
"/instances/**").permitAll()
// 自定义权限校验逻辑
.anyRequest().access(new MyAuthorizationManager())
);
return http.build();
}
/**
* jwt转换器
*/
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
// 指定token中权限字段的名字
authoritiesConverter.setAuthoritiesClaimName("perms");
// 指定权限字符串前缀,空表示无前缀
authoritiesConverter.setAuthorityPrefix("");
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
// 自定义jwtDecoder,一般是校验Payload中的字段
JwtDecoder jwtDecoder() {
// 使用issuerUri创建decoder,decoder会自动验证token签名
// NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(issuerUri + "/oauth2/jwks").build();
// 使用本地公钥创建decoder
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(getPublicKey()).build();
// 创建验证器委托,可以包含多个验证器,官方提供有这个类DelegatingOAuth2TokenValidator,但是逻辑不符合需求
// 比如,加入2个验证器,当第一个验证器不通过时,应直接返回错误,不验证第2个了,DelegatingOAuth2TokenValidator是验证完所有再返回所有错误信息。所以参照DelegatingOAuth2TokenValidator,写了自己需要的
OAuth2TokenValidator<Jwt> orderJwtValidator = new OrderJwtValidator<>(
// 官方的默认验证器,验证token过期时间、生效时间(nbf)、X509证书。还有其他的可以使用:JwtIssuerValidator、JwtClaimValidator等(按顺序加入,先加先验证)
JwtValidators.createDefaultWithIssuer(issuerUri),
// 自定义验证逻辑,写在这个里面
new MyJwtValidator()
);
jwtDecoder.setJwtValidator(orderJwtValidator);
return jwtDecoder;
}
/**
* 字符串转PublicKey
*/
private RSAPublicKey getPublicKey() {
String publicKeyBase64 = "111222333LggDJeXlA/XN4kRPY9sW7+VQpr1MPJjB5tQYVkPLvv3L8v/7k5hcPEoHIFwQo";
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyBase64));
RSAPublicKey rsaPublicKey = null;
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
rsaPublicKey = (RSAPublicKey)keyFactory.generatePublic(keySpec);
} catch (Exception e) {
e.printStackTrace();
}
return rsaPublicKey;
}
}
3.3 自定义的异常返回2个
- MyAccessDeniedHandler
/**
* 登陆了,没有权限时,触发异常 返回信息
*/
public class MyAccessDeniedHandler implements AccessDeniedHandler {
private final HttpMessageConverter<Object> httpResponseConverter = new MappingJackson2HttpMessageConverter();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
accessDeniedException.printStackTrace();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
Result<Integer> res = new Result<Integer>().error(accessDeniedException.getLocalizedMessage());
httpResponseConverter.write(res, null, httpResponse);
}
}
- MyAuthenticationEntryPoint
/**
* 未认证(没有登录)时,返回异常 信息
*/
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final HttpMessageConverter<Object> httpResponseConverter = new MappingJackson2HttpMessageConverter();
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
authException.printStackTrace();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
Result<String> res = new Result<String>().error(CodeMsg.USER_LOGIN_ERROR);
httpResponseConverter.write(res, null, httpResponse);
}
}
3.4 自定义的decoder
- MyJwtValidator
public class MyJwtValidator implements OAuth2TokenValidator<Jwt> {
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
System.out.println("----decode校验逻辑-----");
// 校验成功,返回
return OAuth2TokenValidatorResult.success();
}
}
- OrderJwtValidator
public class OrderJwtValidator<T extends OAuth2Token> implements OAuth2TokenValidator<T> {
private final Collection<OAuth2TokenValidator<T>> tokenValidators;
public OrderJwtValidator(Collection<OAuth2TokenValidator<T>> tokenValidators) {
Assert.notNull(tokenValidators, "tokenValidators cannot be null");
this.tokenValidators = new ArrayList<>(tokenValidators);
}
@SafeVarargs
public OrderJwtValidator(OAuth2TokenValidator<T>... tokenValidators) {
this(Arrays.asList(tokenValidators));
}
@Override
public OAuth2TokenValidatorResult validate(T token) {
Collection<OAuth2Error> errors = new ArrayList<>();
for (OAuth2TokenValidator<T> validator : this.tokenValidators) {
errors = validator.validate(token).getErrors();
if (!errors.isEmpty()) {
break;
}
}
return OAuth2TokenValidatorResult.failure(errors);
}
}
3.5 自定义权限校验MyAuthorizationManager
public class MyAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
HttpServletRequest request = context.getRequest();
System.out.println(request.getRequestURI());
UserDetails user = (UserDetails) authentication.get().getPrincipal();
System.out.println(user);
return new AuthorizationDecision(true);
}
}
到这,基本配置完了,目录如下:
