【中】SpringSecurity最新学习,spring-security-oauth2-authorization-server【spring-security-oauth2升级】
视频学习地址: https://www.bilibili.com/video/BV1Wy411B7xK
这篇文章主要是简单的进行一个SpringSecurity的入门,基于它提供一个客户端的oauth2的认证,包括JWT和Opaque,也可以看成是spring-security-oauth2的升级,因为spring-security-oauth2已不再维护了。
效果展示
认证规范有很多,这里我用的是客户端认证
一、理论
假如最终目的是请求 /hello 接口,那基于oauth2的访问流程看起来就是这样的

JWT和Opaque
简单理解成 Jwt是公开的,直接在线就可以解析看到里面的数据(但不能修改里面的数据,所以它是安全的),Opaque一个不透明的token,在看起来就是一个字符串
二、服务端
2-1、依赖引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.4.1</version>
</dependency>
SpringBoot的版本用的是 2.7.17
2-2、一个简单的OpaqueToken生成
配置文件添加
对应源码目录: com.xdx97.config.oauth1
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;
import java.time.Duration;
import java.util.UUID;
/**
* oauth2服务器配置
*/
@Configuration
public class OpaqueTokenSimpleConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http.apply(new OAuth2AuthorizationServerConfigurer());
http.csrf().disable()
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/oauth2/*").permitAll()
.antMatchers("/introspect/*").permitAll()
.anyRequest().authenticated()
)
.formLogin();
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("xdx97")
.clientSecret("{noop}xdx97")
.clientAuthenticationMethods(authMethods -> {
authMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
})
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true)
.requireProofKey(false)
.build())
.tokenSettings(TokenSettings.builder()
.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
.accessTokenTimeToLive(Duration.ofHours(1))
.reuseRefreshTokens(true)
.build())
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
测试请求
curl --location --request POST 'http://127.0.0.1:12345/oauth2/token?client_id=xdx97&client_secret=xdx97&grant_type=client_credentials'

2-3、一个简单的JwtToken生成
配置文件添加
对应源码目录: com.xdx97.config.oauth2
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;
@Configuration
public class JwtTokenSimpleConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http.apply(new OAuth2AuthorizationServerConfigurer());
http.csrf().disable()
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/oauth2/*").permitAll()
.antMatchers("/introspect/*").permitAll()
.antMatchers("/issuer/*").permitAll()
.anyRequest().authenticated()
)
.formLogin();
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("xdx97")
.clientSecret("{noop}xdx97")
.clientAuthenticationMethods(authMethods -> {
authMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
})
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 必须要配置一个重定向地址
.redirectUri("xxxxxxx")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).requireProofKey(false).build())
.tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofHours(1)).build())
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
测试请求
curl --location --request POST 'http://127.0.0.1:12345/oauth2/token?client_id=xdx97&client_secret=xdx97&grant_type=client_credentials'

因为Jwt是明文的,可以解析出来看看

三、资源端
测试用的HelloController
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello World!";
}
}
3-1、依赖引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.14</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>9.14</version>
</dependency>
3-2、简单OpaqueToken校验
配置文件添加
对应源码目录: com.xdx97.config.oauth1 (和上面 authService对应)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
import org.springframework.security.web.SecurityFilterChain;
@Configuration(proxyBeanMethods = false)
@EnableMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OpaqueResourceSimpleConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer.opaqueToken(opaqueToken ->
opaqueToken.introspector(opaqueTokenIntrospector())
)
);
return http.build();
}
public NimbusOpaqueTokenIntrospector opaqueTokenIntrospector() {
return new NimbusOpaqueTokenIntrospector(
"http://localhost:12345/oauth2/introspect",
"xdx97",
"xdx97"
);
}
}
测试
- token 取自上一个接口
- token要加一个前缀
curl --location --request GET 'http://127.0.0.1:12346/hello' \
--header 'Authorization: Bearer 4KtwpET1hhzMKghkK7Z9oUwcULewPIRF5mgy7cI7GZBRKuEW12NewAJ2YwAxJfT1tpNLWRui_jELQo_S48YlWLIdVXyggt2y27DbsdbpQxpHPgr5f5dTFXFWGzKKbLJR'

如果一直访问401,可以看看下面 N-2
3-3、简单Jwt校验
配置文件添加
对应源码目录: com.xdx97.config.oauth1 (和上面 authService对应)
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
import javax.annotation.Resource;
import java.time.Duration;
@Configuration(proxyBeanMethods = false)
@EnableMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class JwtSimpleConfig {
@Resource
private RestTemplateBuilder restTemplateBuilder;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.oauth2ResourceServer()
.jwt()
.decoder(jwtDecoder(restTemplateBuilder));
return http.build();
}
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
// 授权服务器 jwk 的信息
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri("http://127.0.0.1:12345/oauth2/jwks")
// 设置获取 jwk 信息的超时时间
.restOperations(
builder.setReadTimeout(Duration.ofSeconds(3))
.setConnectTimeout(Duration.ofSeconds(3))
.build()
)
.build();
// 对jwt进行校验
decoder.setJwtValidator(JwtValidators.createDefault());
return decoder;
}
}
测试,和3-2一模一样的
四、进一步体验
上面已经简单使用了SpringSecurity的oauth2客户端认证功能,但还存在一些问题,下面来一一学习
4-1、关于 scope
SpringSecurity认证可以区分更细粒度,比如我们把 /hello 开头的接口定义为, hello资源,只有拥有这个资源的权限才可以访问,而不单单只是有个token
改造Controller
@RestController
@PreAuthorize("hasAuthority('SCOPE_hello')")
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello World!";
}
}
旧版本使用 @PreAuthorize("#oauth2.hasScope('hello')")
服务端改造



在早期的SpringSecurity Oauth2 校验scope是不需要带 SCOPE_ 前缀的,在新版中如果你想去除也可以
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
// 去掉 SCOPE_ 的前缀
authoritiesConverter.setAuthorityPrefix("");
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}

4-2、Opaque 认证的client_id、client_secret 处理
在上面演示的时候,有一段代码是,这是去资源端请求认证服务解析 token
public NimbusOpaqueTokenIntrospector opaqueTokenIntrospector() {
return new NimbusOpaqueTokenIntrospector(
"http://localhost:12345/oauth2/introspect",
"xdx97",
"xdx97"
);
}
如果我们的资源要被多个不同的客户端访问,该怎么办呢?上面的 clientId 和clientSecret 写死了
目前我有一个办法就是,写一个 filter,这两个参数要么让用户传递过来,要么从数据库中取(可以在生成token的时候,把token存到Redis、 id和secret作为值)
- 写一个Filter(就是Java中的Filter)获取到了 id、secret后就存在ThreadLocal中
- 重写 OpaqueTokenIntrospector ,从上下文中获取id、secret
也可以使用Jwt,Jwt因为是明文的所以不需要id和secret
OpaqueTokenIntrospector 代码如下, Filter自己实现这个很简单
public class CustomOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final String introspectionUri;
public CustomOpaqueTokenIntrospector(String introspectionUri) {
this.introspectionUri = introspectionUri;
}
@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
// TODO 改为从 ThreadLocal 中获取
String clientId = "";
String clientSecret ="";
System.out.println(clientId);
System.out.println(clientSecret);
clientId = clientId == null ? "xdx97" : clientId;
clientSecret = clientSecret == null ? "xdx97" : clientSecret;
NimbusOpaqueTokenIntrospector delegate = new NimbusOpaqueTokenIntrospector(
introspectionUri,
clientId,
clientSecret
);
return delegate.introspect(token);
}
}
完整目录在 resource-service: com.xdx97.config.oauth3
4-3、自定义获取token
默认获取token是在 header中,还要拼接一个前缀 Bearer, 假如想改为从 url中获取 access_token, 只需要重写BearerTokenResolver
public class CustomUriBearerTokenResolver implements BearerTokenResolver {
private static final String BEARER_TOKEN_PARAM = "access_token"; // URI 中 Token 的参数名
@Override
public String resolve(HttpServletRequest request) {
// 从 URI 参数中获取 Token
String token = request.getParameter(BEARER_TOKEN_PARAM);
return (token != null && !token.isEmpty()) ? token : null;
}
}

4-4、自定义个性参数 (重要)
上面的流程完成了授权和鉴权,但我们拿不到有用的参数,何为有用的参数比如: 用户的 userId 不管是Opaque还是Jwt都可以在里面设置一些我们自己的参数——把参数放到 【claims】
Opaque
@Bean
public OAuth2TokenCustomizer<OAuth2TokenClaimsContext> tokenCustomizer() {
return context -> {
OAuth2TokenClaimsSet.Builder claims = context.getClaims();
// 将权限信息放入jwt的claims中,这里可以注入一个 Mapper去查询数据库 (可以从 context中拿到 client_id)
claims.claim("companyId","自定义参数");
};
}
Jwt
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> oAuth2TokenCustomizer() {
return context -> {
JwtClaimsSet.Builder claims = context.getClaims();
// 将权限信息放入jwt的claims中,这里可以注入一个 Mapper去查询数据库 (可以从 context中拿到 client_id)
claims.claim("companyId", "自定义参数");
};
}

这个参数也很好拿到,直接存到上下文了,资源端从上下文就可以获取到
4-5、自定义注册RegisteredClient
InMemoryRegisteredClientRepository 支持List集合,可以把数据库的全部的客户端都查询出来构造一个List RegisteredClient丢进去
N、其它
N-1、oauth2/token 接口404
使用 post请求
N-2、使用Opaque访问 401 问题排查
按照上述配置应该不会 401,但防止配错了需要排除问题,我自己断点调试,发现 resourceService,请求 authService的HTTP如下
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
public class IntrospectionTest {
public static void main(String[] args) {
String introspectionEndpoint = "http://localhost:12345/oauth2/introspect";
String clientId = "xdx97";
String clientSecret = "xdx97";
String token = "7LdsIQlDLI7c2Ugw9yAAEbitc97SmZDhCobLEsQbq3HiJJ1GJgA8VpNvCgcfXhF6GmzcWaViicKjZPNOdznSufTP9OBlLXjXE806O9yT7nZyfEdnt6FvYIx9ttpOZRX1";
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth(clientId, clientSecret);
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("token", token);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
ResponseEntity<String> response = restTemplate.exchange(introspectionEndpoint, HttpMethod.POST, request, String.class);
System.out.println(response.getBody());
}
}
通过 active 来判断是否校验通过
{
"active": true,
"sub": "xdx97",
"aud": [
"xdx97"
],
"nbf": 1719137941,
"iss": "http://127.0.0.1:12345",
"exp": 1719141541,
"iat": 1719137941,
"jti": "f75bb022-fa4b-453b-9d3f-8576465a1cb0",
"client_id": "xdx97",
"token_type": "Bearer"
}
N-3、Opaque服务器重启和Jwt服务器重启会不会丢失
不透明token,如果重启服务端会丢失,Jwt则不会
N-4、建议
如果不熟悉的小伙伴,建议不要修改代码的内容,直接复制运行,运行成功后再去修改
N-5、源码
关注公众号:小道仙97 回复关键字:SpringSecurityDemo