spring security6+spring boot 3.5.9最新版本集成oauth2.1

一、授权服务器

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";
    }
}
相关推荐
无名-CODING2 小时前
HandlerMapping和HandlerAdapter完全指南
spring
弹简特2 小时前
【JavaEE10-后端部分】SpringMVC05-综合案例1-从加法计算器看前后端交互:接口文档与HTTP通信详解
java·spring boot·spring·http
恋猫de小郭2 小时前
Android 17 有什么需要适配的?2026 Android 禁止侧载又是什么?
android·前端·flutter
躲在云朵里`2 小时前
同一账号在同一客户端类型只能登录一次
前端·spring·bootstrap
测试工坊2 小时前
Android CPU 整机 42% 却 ANR?单核分析揭开均值背后的真相
android
troublea2 小时前
Laravel5.x核心特性全解析
数据库·spring boot·后端·mysql
啦啦啦_99992 小时前
SpringAI Alibaba(SAA) 之 SSE
spring
三无少女指南3 小时前
Spring Boot项目中Maven编译参数source、target与release的区别及配置实践
java·spring boot·maven