一、授权服务器
1、项目结构

2、建立物理表authorities、和users
2.1 authorities表字段

2.2 users表字段

3、建立oauth2_registered_client 表 用于存储客户端的,字段如下

主要字段包括id,client_id,client_id_issued_at,client_secret,client_secret_expires_at,client_name,client_authentication_methods,authorization_grant_types,redirect_uris,post_logout_redirect_uris,scopes,client_settings和 token_settings
4、授权服务器安装依赖
<!-- 核心依赖-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.82</version>
</dependency>
然后在yml配置文件中进行配置
spring:
application:
name: security_oauth2_server
datasource:
url: jdbc:mysql://localhost:3306/security_test
username: root
password: xxxxxx
driver-class-name: com.mysql.cj.jdbc.Driver
jwt:
private-key: MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCcObyk91qgbqEzLUZrR42YdN1Yt4t7uW2W7wnvCYsin9L94XoU5Cf09x+9Sc51VhreY8GnEWzOnbQuBnVpUJTzQBmWEiQcO3XmjaP4ChjBSIvmSsnGgjEby8FnYRXm/pWYwqR2sa5aX11gcpiK35ZazxCXSRHVKF7NfHEA4qnUlGMHcKeabKnO7bYX4XITkRN7Vk+9Z3rBFzv+0WblwQWJUTd3f9iejHYlmZYu3Gv3Zp9NM2UrWuTNL92tGkNU/aP9owEJwYv5x+okalV26JtEUkF7+WqyFqdJVGMK10USHQ+6Eoi+H2KD1TXz6plFosVDclK4K7s8j38ISmDfqrAnAgMBAAECggEAEsoq80MWMQI9UdTEictNr3mBfyshk/GbHwfZulimgF/tVkBpQ4am0PgWvC3arr16y9FUYvizN9fl6jyNS0ENjoLg0O5A+O2kjOSaRQ9J0kLHI3Kj+W6CjjpKelmbSHDJo1mTGFI0izAPILYyZ07kXEBYBCoeAp+GuiZDx9HQj/2nYDYrCJJ0dp5pOAGVgFcOCRDVo1+ITj7BqzTzTN8tPcv0yGfyPmsLhmCqlAzxF0VDo8OlhCVX5NZUiFKuoMb9t6YrRk5dOqIgvJs6/9Dyn7EHWevbCiTw6eeOl3g6K0R/SCAe6UW0lHRL1SeJiF9n7TdaxObhiKgYQfQNNFG/wQKBgQC84cDjGyHr1Wk4uwhbi4U+jtrbxp1t08FqCVP0/PCEqhR8vXuMEYkZVyDbMhaOh92DPYCp7BRuROVCPo/zK9Kpuz1g/GTntDoNpXg4TQOEVpyHwrgHZXpvC+w1+yf7qGNKX/tdYOzkQwf9zbJlf23W/lHSbOBrnxpfQ/XgyRYwIQKBgQDTvUvkBRWNpT30E6ZR2VdqsjXtiDgFAn9y0nhDuJohz4iDdaCoc2wSjH4MIDi+rCNkdAMw8/xWONf+AIVCT3XrhrOFH6ML+RnUAZU6za59uKnWVo9QYCbPmgzuGNPhFB1O2mLPCfKdRmROm/X3/x7Y1hDWKCSdrIoK4deeVaR3RwKBgB01sFWuwX0uILqUOgOmPPHit7fbLEdqwvN6A9DUOQHbJ5Xu26daouAXWE5tnY1nN3tvTHF6v+IZp3aqJCrP8SEsgp54CcbHWV744vGZ/1w85LIeC8WhDOEVb+8dKx7A+LHsy/ux3JCkSR0X7WnC3iKsa8zl54LdNP/ci666ikLhAoGAFSzHHCSOBT5TNceJSIItxdPQpNKMl6OqTzdRI6SgdWUlx8/Jo073rdsy9895HUhlubQIZargv1ar8xDmQr1jk4xDA7soORhORu9plxneq1/+TBsX9ffHqddEF8OP3OCAFdStMPtTsfrKKTpbnNN8qD6wLFqTXtlUbMYtI/54lh0CgYAqzYAXTUIwfUDSYNuBLLDRU0s2mYpt+/BImrXNyu2LF+sygE4rrW+eHPe6+DV6zlxXmfYwp746I7xL95jB+vZQFw4e1eGY8EKhgbHFJwQm2foDFC9RQomGgH6trJGKYtdKtevxyctpB0o+gXK0nQdV6/4Nn7Y8va9TBXU30EJylA==
public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnDm8pPdaoG6hMy1Ga0eNmHTdWLeLe7ltlu8J7wmLIp/S/eF6FOQn9PcfvUnOdVYa3mPBpxFszp20LgZ1aVCU80AZlhIkHDt15o2j+AoYwUiL5krJxoIxG8vBZ2EV5v6VmMKkdrGuWl9dYHKYit+WWs8Ql0kR1ShezXxxAOKp1JRjB3Cnmmypzu22F+FyE5ETe1ZPvWd6wRc7/tFm5cEFiVE3d3/Ynox2JZmWLtxr92afTTNlK1rkzS/drRpDVP2j/aMBCcGL+cfqJGpVduibRFJBe/lqshanSVRjCtdFEh0PuhKIvh9ig9U18+qZRaLFQ3JSuCu7PI9/CEpg36qwJwIDAQAB
#???? 127345
server:
port: 9922
5、新建工具类RsaTools
package com.example.security_oauth2_server.utils;
import com.nimbusds.jose.jwk.RSAKey;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RsaTools {
/**
* 私钥和公钥
* @param privateKeyPem
* @param publicKeyPem
* @return
*/
public static RSAKey generateRsaKey(String privateKeyPem, String publicKeyPem) {
try {
// 从 PEM 文件加载密钥
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
// 加载私钥
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(
Base64.getDecoder().decode(privateKeyPem)
);
RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(privateKeySpec);
// 加载公钥
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(
Base64.getDecoder().decode(publicKeyPem)
);
RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(publicKeySpec);
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID("fixed-key-id") // 固定 Key ID
.build();
} catch (Exception e) {
throw new RuntimeException("Failed to load RSA key", e);
}
}
}
6、新建跨域类CorsConfig
package com.example.security_oauth2_server.config;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import java.util.List;
public class CorsConfig implements CorsConfigurationSource {
//全局跨域配置
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration config = new CorsConfiguration();
// config.setAllowedOrigins(List.of("http://localhost:4200"));
config.addAllowedOrigin("*");
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
return config;
}
}
7、新建jwt配置类JwkConfig
package com.example.security_oauth2_server.config;
import com.example.security_oauth2_server.utils.RsaTools;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
@Configuration
public class JwkConfig {
// 将密钥硬编码或从配置读取
@Value("${jwt.private-key}")
private String privateKeyPem;
@Value("${jwt.public-key}")
private String publicKeyPem;
@Bean
public JWKSource<SecurityContext> jwkSource() {
// 从固定的密钥文件加载
RSAKey rsaKey = RsaTools.generateRsaKey(privateKeyPem,publicKeyPem);
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
// //配置JWT解码器,用于资源服务器的
// @Bean
// public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
// return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
// }
}
8、新建类OAuth2TokenDispose
package com.example.security_oauth2_server.config;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.stereotype.Component;
//对token进行处理
@Component
public class OAuth2TokenDispose implements OAuth2TokenCustomizer<JwtEncodingContext> {
//向token中塞值
@Override
public void customize(JwtEncodingContext context) {
if (context.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) {
// 添加自定义声明
Authentication principal = context.getPrincipal();
// principal.getName();//用户用户账号,然后执行一系列的逻辑即可
context.getClaims().claims(claims -> {
claims.put("custom_field", "custom_value");
claims.put("user_id", "12345678911");
});
}
}
}
9、新建类SecurityAuthenticationConfig
package com.example.security_oauth2_server.config;
import com.example.security_oauth2_server.server_config.CustomAuthenticationProvider;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityAuthenticationConfig {
@Resource
@Lazy
private CustomAuthenticationProvider customAuthenticationProvider;
@Bean
@Order(2)
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> {
auth
.requestMatchers("/login", "/css/**", "/js/**").permitAll() // 放行登录页和静态资源
.anyRequest().authenticated();
})
.formLogin(Customizer.withDefaults())
.csrf(v->v.disable())
.authenticationProvider(customAuthenticationProvider)
.cors(v->{
v.configurationSource(new CorsConfig());
});
return http.build();
}
}
10、新建用户管理类UserManagementConfig
package com.example.security_oauth2_server.config;//package com.example.security_oauth2_server.config;
//
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import javax.sql.DataSource;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class UserManagementConfig {
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
String usersByUsernameQuery = "select username,password,enabled from users where enabled=1 and username = ?";
String authsByUserQuery="select b.username,a.authority from authorities a left join users b on a.user_id=b.id where b.username = ?";
JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource);
jdbcUserDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
jdbcUserDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);
return jdbcUserDetailsManager;
}
@Bean
public PasswordEncoder passwordEncoder() throws NoSuchAlgorithmException {
Map<String, PasswordEncoder> encoderMap=new LinkedHashMap<>();
encoderMap.put("bcrypt", new BCryptPasswordEncoder());
encoderMap.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoderMap.put("noop", NoOpPasswordEncoder.getInstance());
return new DelegatingPasswordEncoder("bcrypt",encoderMap);
}
}
11、新建认证用户类CustomAuthenticationProvider
package com.example.security_oauth2_server.server_config;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Resource
@Lazy
private UserDetailsService userDetailsService;
@Resource
@Lazy
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails user = userDetailsService.loadUserByUsername(username);
if(passwordEncoder.matches(password,user.getPassword())){
System.out.println("认证成功。用户: " + username);
return new UsernamePasswordAuthenticationToken(username,password,user.getAuthorities());
}else {
throw new BadCredentialsException("密码不正确");
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
12、新建授权服务器类AuthServerConfig
package com.example.security_oauth2_server.oauth2_config;
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.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
@Configuration
@EnableWebSecurity
public class AuthServerConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) // 授权服务器过滤器链保持最高优先级
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 1. 创建授权服务器配置器实例
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
// 2. 获取授权服务器端点的请求匹配器
// 这个匹配器用于识别哪些请求应该由授权服务器链处理
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
// 3. 手动应用原先由 applyDefaultSecurity 完成的配置
http
// 关键:指定此过滤器链只处理授权服务器相关端点的请求
.securityMatcher(endpointsMatcher)
// 要求所有访问授权端点的请求都必须经过认证
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated() // 其他所有接口需要认证
)
// 对授权服务器端点禁用CSRF保护(因为是API接口)
// .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.csrf(csrf -> csrf.disable())
.exceptionHandling(exceptions -> exceptions
// 关键修复:配置当未认证时重定向到登录页面,必不可少
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// 将配置器应用到HttpSecurity
.with(authorizationServerConfigurer, Customizer.withDefaults());
// 4. (可选)进行个性化定制,例如启用OIDC支持
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // 启用 OpenID Connect 1.0
return http.build();
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
13、新建客户端配置类ClientRegistrationConfig,生产环境改用数据库
package com.example.security_oauth2_server.oauth2_config;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
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.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import java.time.Duration;
import java.util.UUID;
@Configuration
public class ClientRegistrationConfig {
@Resource
private PasswordEncoder passwordEncoder;
//向授权服务器注册客户端
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("client") // 客户端ID
// .clientSecret("{noop}secret") // 对客户端密码进行编码
.clientSecret(passwordEncoder.encode("secret")) // 对客户端密码进行编码
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // 客户端认证方式
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) // 授权码模式
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) // 支持刷新令牌
.redirectUri("http://localhost:8921/login/oauth2/code/authserver") // 重定向URI
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("read")
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(false) // 要求用户授权确认
.requireProofKey(false)
.build())
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(2)) // 访问令牌有效期
.refreshTokenTimeToLive(Duration.ofDays(1)) // 刷新令牌有效期
.build())
.build();
return new InMemoryRegisteredClientRepository(client);
// return new JdbcRegisteredClientRepository(jdbcTemplate);
}
}
14、生成公钥和私钥的方法
private void generateKeysExample() throws Exception {
// 生成 RSA 密钥对
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
KeyPair keyPair = generator.generateKeyPair();
// 获取 PKCS8 格式的私钥
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); // PKCS8
byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); // X.509
// Base64 编码
String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKeyBytes);
String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKeyBytes);
System.out.println("=== 复制到 application.yml ===");
System.out.println("\n# 私钥 (PKCS8 格式)");
System.out.println("jwt:");
System.out.println(" private-key: " + privateKeyBase64);
System.out.println("\n# 公钥 (X.509 格式)");
System.out.println(" public-key: " + publicKeyBase64);
// 也显示 PEM 格式
System.out.println("\n=== PEM 格式(如果需要)===");
System.out.println("私钥 PEM:");
System.out.println("-----BEGIN PRIVATE KEY-----");
for (int i = 0; i < privateKeyBase64.length(); i += 64) {
System.out.println(privateKeyBase64.substring(i, Math.min(i + 64, privateKeyBase64.length())));
}
System.out.println("-----END PRIVATE KEY-----");
}
二、资源服务器
1、项目结构

2、安装依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
3、yml的配置文件
spring:
application:
name: security_oauth2_resource
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:9922/oauth2/jwks
server:
port: 9938
4、新建类CustomTokenResolver
package com.example.security_oauth2_resource.config;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class CustomTokenResolver implements BearerTokenResolver {
private final OrRequestMatcher publicPathMatcher;
public CustomTokenResolver(List<String> publicPathPatterns) {
// 使用 PathPatternRequestMatcher 替代废弃的 AntPathRequestMatcher
List<RequestMatcher> matchers = publicPathPatterns.stream()
.map(pattern -> PathPatternRequestMatcher.withDefaults().matcher(pattern))
.collect(Collectors.toList());
this.publicPathMatcher = new OrRequestMatcher(matchers);
}
@Override
public String resolve(HttpServletRequest request) {
// 检查是否为公开路径
if (publicPathMatcher.matches(request)) {
return null; // 公开路径,不需要token
}
// 1. 优先从自定义 Header 获取
String token = request.getHeader("TP-Auth");
System.out.println("token的值");
System.out.println(token);
// 2. 如果自定义 Header 没有,再用标准的 Authorization
if (token == null || token.isBlank()) {
throw new RuntimeException("token值不存在");
}
if(token.contains("Bearer")){
return token.substring(7);
}
return token;
}
}
5、新建资源服务器配置类ResourceServerConfig
package com.example.security_oauth2_resource.config;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Resource
@Lazy
private CustomTokenResolver customTokenResolver;
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
String jwkSetUri;
@Bean
public List<String> publicPathPatterns() {
//实际的公开路径
return Arrays.asList(
"/public/**"
);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
List<String> publicPaths = publicPathPatterns();
http
.authorizeHttpRequests(authorize -> {
for (String pattern : publicPaths) {
authorize.requestMatchers(pattern).permitAll();
}
authorize.anyRequest().authenticated();
})
.oauth2ResourceServer(oauth2 -> oauth2
.bearerTokenResolver(customTokenResolver)
.jwt(Customizer.withDefaults()) // 使用JWT令牌
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// 从配置文件中读取JWK Set URI
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
}
6、新建类TokenParsingFilter,用于解析token
package com.example.security_oauth2_resource.config;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Optional;
@Component
public class TokenParsingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 从 SecurityContext 获取 Authentication
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof Jwt jwt) {
try {
// 解析 Token 信息
System.out.println("解析token");
System.out.println(jwt.getClaim("user_id").toString());
// 将解析结果存储在 request 中
request.setAttribute("TOKEN_CONTEXT", jwt.getClaim("user_id").toString());
} finally {
// 确保在 finally 中清除 ThreadLocal
filterChain.doFilter(request, response);
}
} else {
filterChain.doFilter(request, response);
}
}
}
7、新建工具类TokenContextHolder,用于解析token的最终结果
package com.example.security_oauth2_resource.utils;
import org.springframework.security.oauth2.jwt.Jwt;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class TokenContextHolder {
public static Map<String, Object> getJwt(Jwt jwt) {
// 预解析并缓存常用声明
if (jwt != null) {
Map<String, Object> commonClaims = new LinkedHashMap<>();
commonClaims.put("user_id", jwt.getClaim("user_id"));
return commonClaims;
}
return new LinkedHashMap<>();
}
}
8、新建拦截器类InterceptorHandler,用于进一步处理
@Slf4j
public class InterceptorHandler implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws Exception {
String userId=(String) request.getAttribute("TOKEN_CONTEXT");
System.out.println("拦截器");
System.out.println(userId);
return true;
//System.out.println("处理器前方法");
// 返回true,不会拦截后续的处理
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
log.info("处理后");
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
log.info("移除");
// BaseContext.remove();
}
}
9、改造启动类,用于注册拦截器
@SpringBootApplication
public class SecurityOauth2ResourceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(SecurityOauth2ResourceApplication.class, args);
}
@Override
public void addInterceptors(InterceptorRegistry registry){
InterceptorRegistration ir = registry.addInterceptor(new InterceptorHandler());
// ir.excludePathPatterns("/systemLanguageConfig/getMultipleLanguageConfigMap/**");
}
}
10、测试控制器
package com.example.security_oauth2_resource.controller;
import com.example.security_oauth2_resource.utils.TokenContextHolder;
import jdk.jfr.Enabled;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
@GetMapping("/public")
public String publicA(){
return "publicA";
}
@GetMapping("/getTokenMap")
public Map<String, Object> getTokenMap(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof Jwt jwt) {
return TokenContextHolder.getJwt(jwt);
}
return new LinkedHashMap<>();
}
}
三、后端客户端
1、项目结构

2、安装依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3、yml配置文件
spring:
application:
name: security_oauth2_client
# main:
# allow-bean-definition-overriding: true
security:
oauth2:
client:
provider:
authserver:
issuer-uri: http://localhost:9922
user-name-attribute: sub
registration:
authserver:
client-id: client
client-name: client
client-secret: secret
authorization-grant-type: authorization_code
scope: openid,profile,read
redirect-uri: "http://localhost:8921/login/oauth2/code/authserver"
# redirect-uri: "https://www.baidu.com"
client-authentication-method: client_secret_basic
logging:
level:
org:
springframework:
security: DEBUG
web:
client : DEBUG
server:
port: 8921
servlet:
session:
cookie:
same-site: lax
secure: false # 开发环境可设为false
4、新建配置类SecurityAuthenticationConfig
package com.example.security_oauth2_client.config;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Configuration
@EnableWebSecurity
public class SecurityAuthenticationConfig {
private final Map<String, OAuth2AuthorizationRequest> requestStore =
new ConcurrentHashMap<>();
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> {
auth.anyRequest().authenticated();
})
// .oauth2Login(Customizer.withDefaults())
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authz -> authz
.authorizationRequestRepository(authorizationRequestRepository())
)
.failureHandler((request, response, exception) -> {
// 打印详细错误
System.err.println("OAuth2 登录失败: " + exception.getMessage());
exception.printStackTrace();
response.sendRedirect("/login?error");
})
)
.csrf(v -> v.disable());
return http.build();
}
private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {
return new AuthorizationRequestRepository<OAuth2AuthorizationRequest>() {
@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
String state = request.getParameter("state");
if (state != null) {
OAuth2AuthorizationRequest req = requestStore.get(state);
System.out.println("从内存加载授权请求,state: " + state + ", 找到: " + (req != null));
return req;
}
return null;
}
@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest,
HttpServletRequest request, HttpServletResponse response) {
if (authorizationRequest != null) {
String state = authorizationRequest.getState();
requestStore.put(state, authorizationRequest);
System.out.println("保存授权请求到内存,state: " + state);
}
}
@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(
HttpServletRequest request, HttpServletResponse response) {
String state = request.getParameter("state");
if (state != null) {
OAuth2AuthorizationRequest req = requestStore.remove(state);
System.out.println("从内存移除授权请求,state: " + state);
return req;
}
return null;
}
};
}
// @Bean
// public AuthorizationRequestRepository<OAuth2AuthorizationRequest>
// authorizationRequestRepository() {
// return new HttpSessionOAuth2AuthorizationRequestRepository();
// }
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedHeader("*");
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
5、新建测试控制器
package com.example.security_oauth2_client.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MainController {
@GetMapping("/main")
public String main() {
return "mainOK";
}
@GetMapping("/")
public String index() {
return "indexOK";
}
}