原创

OAuth2服务端

温馨提示:
本文最后更新于 2024年12月07日,已超过 133 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

OAuth 2.0 是一种广泛使用的授权框架,用于第三方应用程序安全地访问用户资源,而无需用户提供其用户名和密码。OAuth 2.0 主要用于授权,而不是认证。本文将详细介绍 OAuth 2.0 的内存集成和持久化实现,并解决一些常见的问题。

一.在内存中集成

在内存中的 OAuth 2.0 实现通常用于测试环境,因为它简单且易于实现。内存中的实现通常包含以下数据:

  • 访问令牌(Access Token):用于访问受保护资源的令牌。
  • 刷新令牌(Refresh Token):用于获取新的访问令牌,以延长访问权限的有效期。
  • 客户端的标识(client_id):客户端的唯一标识符。
  • 客户端密钥(client_secret):客户端的密钥,用于验证客户端身份。
  • 重定向 URI:客户端指定的重定向地址,授权服务器在授权成功后将用户重定向到该地址。
  • 授权码(Authorization Code):用于换取访问令牌的临时代码。

1.代码

在 pom.xml 文件中添加以下依赖

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.1.0</version>
  <relativePath/>
</parent>

<!--OAuth 2.0 授权服务器-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>

授权服务配置

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.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.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

@Configuration
public class AuthorizationServerConfig {

    /**
     * Spring Security的过滤器链,用于协议端点的
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    // 设置此过滤链的优先级为1,确保在其他过滤链之前执行
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        /**
         *  OAuth 2.0 授权服务器应用默认的安全配置,如下
         *  1.配置授权端点的安全性:
         * 	    授权端点(如 /oauth2/authorize)会被配置为需要身份验证。这意味着只有经过身份验证的用户才能访问这些端点。
         * 	    令牌端点(如 /oauth2/token)会被配置为需要客户端认证。这意味着只有合法的客户端才能请求访问令牌。
         *  2.配置错误处理:
         * 	    当请求未通过身份验证或授权时,会自动重定向到登录页面或返回相应的错误响应。
         *  3.配置 CORS 支持:
         * 	    默认情况下,会启用跨域资源共享(CORS)支持,以便客户端可以从不同的域请求授权服务器的资源。
         *  4.配置 CSRF 保护:
         * 	    默认情况下,会启用跨站请求伪造(CSRF)保护,以防止恶意攻击者利用用户的会话进行未经授权的操作。
         *  5.配置 JWT 支持:
         * 	    如果启用了 JWT 支持,会自动配置 JWT 编码和解码的相关设置。
         */
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        // 启用 OpenID Connect 1.0 支持
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());	// Enable OpenID Connect 1.0

        // 当未通过身份验证时,从授权端点重定向到登录页面
        http.exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                // 指定登录页面的URL
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                // 匹配HTML请求
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                )

                // 接受访问令牌以获取用户信息或客户端注册信息
                .oauth2ResourceServer((resourceServer) -> resourceServer
                        // 使用JWT进行资源服务器保护
                        .jwt(Customizer.withDefaults()));

        // 构建并返回 SecurityFilterChain
        return http.build();
    }

    /**
     * 管理客户端
     * @return
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        // 生成唯一的客户端 ID
        RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
                // 设置客户端 ID
                .clientId("oidc-client")
                // 设置客户端密钥,使用明文
                .clientSecret("{noop}secret")
                // 设置客户端认证方法
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                // 允许授权码授予类型
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                // 允许刷新令牌授予类型
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                // 允许客户端凭证授予类型
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // 添加重定向 URI
                .redirectUri("https://www.baidu.com")
                // 添加另一个重定向 URI
                .redirectUri("http://localhost:9001/login/oauth2/code/oidc-client")
                // 添加第三个重定向 URI
                .redirectUri("http://localhost:9001/api/login/welcome")
                // 设置注销后的重定向 URI
                .postLogoutRedirectUri("http://127.0.0.1:8080/")
                /**
                 * 添加 OpenID 范围
                 * 用于请求用户的基本身份验证信息,是使用 OpenID Connect 1.0 时的必需范围
                 */
                .scope(OidcScopes.OPENID)
                /**
                 * 添加用户资料范围
                 * 用于请求用户的个人资料信息,是可选范围,客户端可以根据需要请求这个范围来获取更多的用户信息。
                 */
                .scope(OidcScopes.PROFILE)
                // 自定义范围:读取消息
                .scope("message.read")
                // 自定义范围:写入消息
                .scope("message.write")
                // 自定义范围:所有权限
                .scope("all")
                // 设置客户端需要页面审核授权
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .build();

        // 返回内存中的客户端注册仓库
        return new InMemoryRegisteredClientRepository(oidcClient);
    }

    /**
     * 签名访问令牌
     * @return
     */
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        // 生成 RSA 密钥对
        KeyPair keyPair = generateRsaKey();
        // 获取公钥
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        // 获取私钥
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                // 设置私钥
                .privateKey(privateKey)
                // 设置键 ID
                .keyID(UUID.randomUUID().toString())
                .build();
        // 创建 JWKSet
        JWKSet jwkSet = new JWKSet(rsaKey);
        // 返回不可变的 JWKSet
        return new ImmutableJWKSet<>(jwkSet);
    }

    /**
     * 启动时生成的带有密钥的KeyPair实例,用于创建上面的JWKSource
     * @return
     */
    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            // 获取 RSA 密钥对生成器
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            // 初始化密钥长度为 2048 位
            keyPairGenerator.initialize(2048);
            // 生成密钥对
            keyPair = keyPairGenerator.generateKeyPair();
        }
        catch (Exception ex) {
            // 如果生成密钥对失败,抛出异常
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    /**
     * JwtDecoder的一个实例,用于解码已签名的访问令牌
     * @param jwkSource
     * @return
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        // 创建 JWT 解码器
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     * 配置Spring Authorization Server的AuthorizationServerSettings实例
     * @return
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        // 创建授权服务器设置
        return AuthorizationServerSettings.builder().build();
    }
}

SpringSecurity配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

/**
 * Spring Security的过滤器链,用于Spring Security的身份认证
 */
@EnableWebSecurity
/**
 * 禁用代理 Bean 方法以提高性能
 * 优化配置类的性能,通过减少方法调用检查的开销来提升性能。使用这个注解时,开发人员需要确保 @Bean 方法的行为是幂等的,以避免潜在的问题。
 */
@Configuration(proxyBeanMethods = false)
public class DefaultSecurityConfig {

    @Bean
    // 设置此过滤链的优先级为 2,确保它在授权服务器过滤链之后执行
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {
        http
                .authorizeHttpRequests((authorize) -> authorize
                        // 允许访问 /actuator/**
                        .requestMatchers(new AntPathRequestMatcher("/actuator/**"),
                                // 允许访问 /oauth2/**
                                new AntPathRequestMatcher("/oauth2/**"),
                                // 允许访问所有 .json 文件
                                new AntPathRequestMatcher("/**/*.json"),
                                // 允许访问所有 .html 文件
                                new AntPathRequestMatcher("/**/*.html"))
                        // 上述路径无需身份验证
                        .permitAll()
                        // 所有其他请求都需要身份验证
                        .anyRequest().authenticated()
                )
                // 启用 CORS 支持
                .cors(Customizer.withDefaults())
                // 禁用 CSRF 保护
                .csrf((csrf) -> csrf.disable())
                // 启用表单登录,并且表单登录处理从授权服务器过滤链重定向到登录页面
                .formLogin(Customizer.withDefaults())
        ;

        // 构建并返回 SecurityFilterChain
        return http.build();
    }

    /**
     * 用户身份验证
     *
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService() {
        // 使用默认的密码编码器
        UserDetails userDetails = User.withDefaultPasswordEncoder()
                // 设置用户名
                .username("user")
                // 设置密码
                .password("password")
                // 设置用户角色
                .roles("USER")
                .build();

        // 返回内存中的用户详细信息服务
        return new InMemoryUserDetailsManager(userDetails);
    }

}

2.接口

1.查看授权服务配置接口

获取授权服务器的配置信息,包含授权端点、令牌端点、用户信息端点、密钥集端点。

地址

GET http://127.0.0.1:8080/.well-known/openid-configurati

响应

{
  "issuer": "http://127.0.0.1:8080",
  "authorization_endpoint": "http://127.0.0.1:8080/oauth2/authorize",
  "device_authorization_endpoint": "http://127.0.0.1:8080/oauth2/device_authorization",
  "token_endpoint": "http://127.0.0.1:8080/oauth2/token",
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post",
    "client_secret_jwt",
    "private_key_jwt"
  ],
  "jwks_uri": "http://127.0.0.1:8080/oauth2/jwks",
  "userinfo_endpoint": "http://127.0.0.1:8080/userinfo",
  "end_session_endpoint": "http://127.0.0.1:8080/connect/logout",
  "response_types_supported": [
    "code"
  ],
  "grant_types_supported": [
    "authorization_code",
    "client_credentials",
    "refresh_token",
    "urn:ietf:params:oauth:grant-type:device_code"
  ],
  "revocation_endpoint": "http://127.0.0.1:8080/oauth2/revoke",
  "revocation_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post",
    "client_secret_jwt",
    "private_key_jwt"
  ],
  "introspection_endpoint": "http://127.0.0.1:8080/oauth2/introspect",
  "introspection_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post",
    "client_secret_jwt",
    "private_key_jwt"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "scopes_supported": [
    "openid"
  ]
}

2.获取授权码或访问令牌

用户同意授权后获取授权码或访问令牌的步骤如下:

  1. 用户访问端点:用户访问客户端提供的授权请求 URL,被重定向到授权服务器的登录页面进行身份验证,通常需要输入用户名和密码。
  2. 身份验证成功:用户在身份验证成功后,会被提示是否同意授权客户端访问特定的资源或范围(scopes)。
  3. 用户同意授权:如果用户同意授权,授权服务器会将用户重定向回客户端提供的重定向 URI,并附带授权码或访问令牌(取决于使用的授权类型)。

地址

GET http://localhost:8080/oauth2/authorize?response_type=code&client_id=oidc-client&scope=message.read openid&redirect_uri=https://www.baidu.com

响应

使用user/password登录

3.获取访问令牌接口

客户端向授权服务器请求访问令牌(access token)。这个端点的主要作用是验证客户端的身份和授权请求,并返回相应的访问令牌。访问令牌可以用于后续的 API 请求,以证明客户端具有访问特定资源的权限。

Authorization:Basic b2lkYy1jbGllbnQ6c2VjcmV0(将 client_id 和 client_secret 通过 : 拼接起来后进行 Base64 编码后得到的字符串,在该字符串前拼接 Basic )

POST
http://localhost:8080/oauth2/token?grant_type=authorization_code&code=FouwKhWPOTGdMo11c5uKAE3F7ilLNbY7ExLvuS2VBDuIpVwiADqPQeh_iX3W2g8lwm3UVHG23z6tQ7TBsH7FzQ_j9NtTSKSWyoEcAf3ZQmmZKwVQOE3kPV0aFfxCWNox&redirect_uri=https://www.baidu.com

form-data
client_id=oidc-client
client_secret=secret

header
Authorization=Basic b2lkYy1jbGllbnQ6c2VjcmV0

响应

{
    "access_token": "eyJraWQiOiI2OWUxZDllZS01YmJiLTRiNTgtOWE0YS03MDgzMjczNWYxOTYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoib2lkYy1jbGllbnQiLCJuYmYiOjE3MzA0NjgyMzUsInNjb3BlIjpbIm9wZW5pZCIsIm1lc3NhZ2UucmVhZCJdLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJleHAiOjE3MzA0Njg1MzUsImlhdCI6MTczMDQ2ODIzNX0.OErfPE7xARYx05vJZXqSc2HuV6CEW6O8C_yeEJo9_Lx3k28KSRBOc0bdTzg_lDhGs08D91fFlFhpzlcv11uJVlWLkuPSxmvvv5OPM6C_i0Z5S8YHDsWRZARvl7CFmSE5Raq3rfD4cxwKh5TjQDuXrcE_aD_nzdsg2SJpKTy_AvoFUSkY-VIl0o1OESkuN-x6JFxMehEYJghRABMXqo1JYs3q9o4jBmzjjA5qNtahbB24cO1o1ilyZOeJGNMwGwJksJoPIbxmKNFS4RKvgP2QI-hFEkOkER3QkBtn-flw_Ylr1SfY-sGrKULtDAbbY5bxb8Z2nvitbKsRmHdRHrCmwg",
    "refresh_token": "sHsM7NQhhscD-z3wzd29MG2ALZxt4klkSjIaM6kW-Sf_9S1oaMvtqsBgoLP6RzdGN-M1LotY3ER469hl8k5IQNAMWjU0a0riE7Adw_y5IgGaQG6zG5_2GfL8zIK6vY6_",
    "scope": "openid message.read",
    "id_token": "eyJraWQiOiI2OWUxZDllZS01YmJiLTRiNTgtOWE0YS03MDgzMjczNWYxOTYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoib2lkYy1jbGllbnQiLCJhenAiOiJvaWRjLWNsaWVudCIsImF1dGhfdGltZSI6MTczMDQ2ODEzMCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiZXhwIjoxNzMwNDcwMDM1LCJpYXQiOjE3MzA0NjgyMzUsInNpZCI6IklZRHhuNDFLb3o0SXhCS2xmT2xUd25qNG8zYzd3TEpJbVA5a05VTWRiWlUifQ.U15LoaAhdYQSpgGHHePkKmI7XwOpIUxKZur1Kay44tf4StiJqEOiOxwBa6fNr6gwiIwAZBWp8iy59hZpTuXXPA83Df0frBK0VoK43pQUHCKK2KrGk-H068_sKUjDbhGvAtkA8fbprgOAJQ-1IeAdHITczgZqqT5vtKRJkePSruPtw0PeGPHiGOzjcV2RPnokJ4Q7p9Ix2kfUdWFhyPGa4_CM5VweHOU2fJWX7J6zmuYY8YrpdvexACMMXIWY5posNC6gHXLZNHzUoh-VepzgDnV2tgG7nl9E1NeRDePeMao7WCkj19nUfhZrBWr6uihaih8z5Gb5gRIfsXT74rU0PA",
    "token_type": "Bearer",
    "expires_in": 300
}

4.获取用户信息接口

获取用户信息接口允许客户端使用访问令牌(access token)查询用户的详细信息。这些信息通常包括用户的唯一标识符(sub)、姓名、电子邮件等。

地址

POST http://localhost:8080/userinfo

请求头

  • AuthorizationBearer ACCESS_TOKEN(将 ACCESS_TOKEN 替换为实际的访问令牌)

响应

{
    "sub": "user",
    "name": "John Doe",
    "email": "john.doe@example.com",
    "picture": "https://example.com/profile.jpg"
}

3.状态码

1.客户端错误

  1. 400 Bad Request
    • 意义:请求无效或格式错误。
    • 示例:客户端发送的请求缺少必要的参数,或者参数格式不正确。
  1. 401 Unauthorized
    • 意义:请求需要用户认证。
    • 示例:客户端尝试访问受保护的资源,但未提供有效的访问令牌。
  1. 403 Forbidden
    • 意义:请求被拒绝,即使提供了正确的认证信息。
    • 示例:客户端拥有有效的访问令牌,但没有足够的权限访问请求的资源。
  1. 404 Not Found
    • 意义:请求的资源不存在。
    • 示例:客户端请求的端点不存在。

2.服务器错误

  1. 500 Internal Server Error
    • 意义:服务器遇到意外情况,无法完成请求。
    • 示例:授权服务器内部发生错误,无法处理客户端的请求。
  1. 501 Not Implemented
    • 意义:服务器不支持请求的方法。
    • 示例:客户端使用了授权服务器不支持的 HTTP 方法。
  1. 502 Bad Gateway
    • 意义:服务器作为网关或代理,从上游服务器收到了无效的响应。
    • 示例:授权服务器作为代理,从其他服务收到了无效的响应。
  1. 503 Service Unavailable
    • 意义:服务器暂时无法处理请求,通常是因为过载或维护。
    • 示例:授权服务器正在进行维护,暂时无法处理请求。

3.常见的 OAuth 2.0 错误代码

除了 HTTP 状态码,OAuth 2.0 还定义了一些标准的错误代码,这些错误代码通常在 400 状态码的响应中使用:

  1. invalid_request
    • 意义:请求参数无效或缺失。
    • 示例:客户端发送的请求缺少 client_id 参数。注意不同参数传递方式传同一个参数也会报错。
  1. invalid_client
    • 意义:客户端认证失败。
    • 示例:客户端提供的 client_idclient_secret 无效。
  1. invalid_grant
    • 意义:授权码或刷新令牌无效。
    • 示例:客户端提供的授权码已过期或已被使用。
  1. unauthorized_client
    • 意义:客户端没有权限使用请求的授权类型。
    • 示例:客户端尝试使用 authorization_code 授权类型,但未被授权使用该类型。
  1. unsupported_grant_type
    • 意义:授权服务器不支持请求的授权类型。
    • 示例:客户端请求使用 password 授权类型,但授权服务器不支持该类型。
  1. invalid_scope
    • 意义:请求的范围无效。
    • 示例:客户端请求的范围 read write 无效或未被授权。

二.持久化实现

在生产环境中,为了确保数据的持久性和可靠性,OAuth2 的实现通常会依赖于持久化存储,通常包含以下数据:

  • 访问令牌(Access Token):用于访问受保护资源的令牌。
  • 刷新令牌(Refresh Token):用于获取新的访问令牌,以延长访问权限的有效期。
  • 客户端的标识(client_id):客户端的唯一标识符。
  • 客户端密钥(client_secret):客户端的密钥,用于验证客户端身份。
  • 重定向 URI:客户端指定的重定向地址,授权服务器在授权成功后将用户重定向到该地址。
  • 授权码(Authorization Code):用于换取访问令牌的临时代码。
  • 用户信息:如用户名、用户ID、角色等

1.代码

在 pom.xml 文件中添加以下依赖

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.1.0</version>
  <relativePath/>
</parent>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>

  <!--OAuth 2.0 授权服务器-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
  </dependency>

  <!-- MySQL 数据库驱动 -->
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
  </dependency>
  <!-- Alibaba Druid 数据源 -->
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.23</version>
  </dependency>
  <!-- MyBatis Plus Spring Boot Starter -->
  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.9</version>
  </dependency>

  <!-- Alibaba FastJSON -->
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.53</version>
  </dependency>

  <!-- Spring Boot Starter Logging (默认包含 Logback) -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-logging</artifactId>
  </dependency>

  <!-- Lombok 用于简化日志记录代码 -->
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.34</version>
    <scope>provided</scope>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

在 application.yml 文件中添加以下配置

spring:
  application:
    name: oauth2Demo
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    # allowPublicKeyRetrieval=true允许从服务器检索公钥,解决java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed问题
    url: jdbc:mysql://127.0.0.1:3306/oauth2-demo?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: Video201@

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/*.xml

logging:
  file:
    # 设置日志文件的存储路径
    path: logs
  level:
    # 设置根日志级别
    root: info
    # 为特定包设置日志级别
    com.example: debug

为了实现 OAuth 2.0 授权服务器并将相关数据存储在数据库中,我们需要创建以下表结构。这些表用于存储授权码、访问令牌、刷新令牌、客户端信息和用户信息。

-- ----------------------------
-- Table structure for oauth2_authorization
-- ----------------------------
DROP TABLE IF EXISTS `oauth2_authorization`;
CREATE TABLE `oauth2_authorization`  (
  `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `authorization_grant_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `authorized_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attributes` blob NULL,
  `state` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `authorization_code_value` blob NULL,
  `authorization_code_issued_at` timestamp NULL DEFAULT NULL,
  `authorization_code_expires_at` timestamp NULL DEFAULT NULL,
  `authorization_code_metadata` blob NULL,
  `access_token_value` blob NULL,
  `access_token_issued_at` timestamp NULL DEFAULT NULL,
  `access_token_expires_at` timestamp NULL DEFAULT NULL,
  `access_token_metadata` blob NULL,
  `access_token_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `access_token_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `oidc_id_token_value` blob NULL,
  `oidc_id_token_issued_at` timestamp NULL DEFAULT NULL,
  `oidc_id_token_expires_at` timestamp NULL DEFAULT NULL,
  `oidc_id_token_metadata` blob NULL,
  `refresh_token_value` blob NULL,
  `refresh_token_issued_at` timestamp NULL DEFAULT NULL,
  `refresh_token_expires_at` timestamp NULL DEFAULT NULL,
  `refresh_token_metadata` blob NULL,
  `user_code_value` blob NULL,
  `user_code_issued_at` timestamp NULL DEFAULT NULL,
  `user_code_expires_at` timestamp NULL DEFAULT NULL,
  `user_code_metadata` blob NULL,
  `device_code_value` blob NULL,
  `device_code_issued_at` timestamp NULL DEFAULT NULL,
  `device_code_expires_at` timestamp NULL DEFAULT NULL,
  `device_code_metadata` blob NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for oauth2_authorization_consent
-- ----------------------------
DROP TABLE IF EXISTS `oauth2_authorization_consent`;
CREATE TABLE `oauth2_authorization_consent`  (
  `registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `authorities` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  PRIMARY KEY (`registered_client_id`, `principal_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for oauth2_authorized_client
-- ----------------------------
DROP TABLE IF EXISTS `oauth2_authorized_client`;
CREATE TABLE `oauth2_authorized_client`  (
  `client_registration_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `access_token_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `access_token_value` blob NOT NULL,
  `access_token_issued_at` timestamp NOT NULL,
  `access_token_expires_at` timestamp NOT NULL,
  `access_token_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `refresh_token_value` blob NULL,
  `refresh_token_issued_at` timestamp NULL DEFAULT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`client_registration_id`, `principal_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for oauth2_registered_client
-- ----------------------------
DROP TABLE IF EXISTS `oauth2_registered_client`;
CREATE TABLE `oauth2_registered_client`  (
  `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `client_id_issued_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `client_secret` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `client_secret_expires_at` timestamp NULL DEFAULT NULL,
  `client_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `client_authentication_methods` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `authorization_grant_types` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `redirect_uris` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `post_logout_redirect_uris` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `client_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `token_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` bigint NOT NULL COMMENT '主键',
  `username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名',
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',
  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',
  `status` tinyint NOT NULL COMMENT '状态',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `create_user` bigint NOT NULL COMMENT '创建人',
  `update_time` datetime NOT NULL COMMENT '修改时间',
  `update_user` bigint NOT NULL COMMENT '修改人',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

授权服务器配置,包括安全配置、客户端注册、授权服务、JWT 签名和解码等。

/**
 * 配置 OAuth 2.0 授权服务器,包括安全配置、客户端注册、授权服务、JWT 签名和解码等
 */
@Configuration
public class AuthorizationServerConfig {
    @Autowired
    private UserService userService;

    /**
     * Spring Security的过滤器链,用于协议端点的
     *
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    // 设置此过滤链的优先级为1,确保在其他过滤链之前执行
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        /**
         *  OAuth 2.0 授权服务器应用默认的安全配置,如下
         *  1.配置授权端点的安全性:
         * 	    授权端点(如 /oauth2/authorize)会被配置为需要身份验证。这意味着只有经过身份验证的用户才能访问这些端点。
         * 	    令牌端点(如 /oauth2/token)会被配置为需要客户端认证。这意味着只有合法的客户端才能请求访问令牌。
         *  2.配置错误处理:
         * 	    当请求未通过身份验证或授权时,会自动重定向到登录页面或返回相应的错误响应。
         *  3.配置 CORS 支持:
         * 	    默认情况下,会启用跨域资源共享(CORS)支持,以便客户端可以从不同的域请求授权服务器的资源。
         *  4.配置 CSRF 保护:
         * 	    默认情况下,会启用跨站请求伪造(CSRF)保护,以防止恶意攻击者利用用户的会话进行未经授权的操作。
         *  5.配置 JWT 支持:
         * 	    如果启用了 JWT 支持,会自动配置 JWT 编码和解码的相关设置。
         */
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        // 用户信息映射器
        Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper = (context) -> {
            OidcUserInfoAuthenticationToken authentication = context.getAuthentication();
            JwtAuthenticationToken principal = (JwtAuthenticationToken) authentication.getPrincipal();
            return new OidcUserInfo(userService.getUserInfoMap(principal.getName()));
        };

        // 配置OAuth 2.0授权服务器的接口
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                // 设置客户端授权中失败的handler处理
                .clientAuthentication((auth) -> auth.errorResponseHandler(new Oauth2FailureHandler()))
                // token相关配置,如/oauth2/token接口
                .tokenEndpoint((token) -> token.errorResponseHandler(new Oauth2FailureHandler()))
                // 启用 OpenID Connect 1.0 支持, 包括用户信息等
                .oidc((oidc) -> {
                    oidc.userInfoEndpoint((userInfo) -> {
                        // 设置用户信息映射器,用于将认证上下文转换为用户信息
                        userInfo.userInfoMapper(userInfoMapper);
                        // 设置用户信息响应处理器,用于处理成功的用户信息请求
                        userInfo.userInfoResponseHandler(new Oauth2SuccessHandler());
                    });
                });

        // 当未通过身份验证时,从授权端点重定向到登录页面
        http.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(
                // 指定登录页面的URL
                new LoginUrlAuthenticationEntryPoint("/login"),
                // 匹配HTML请求
                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)));

        // 接受访问令牌以获取用户信息或客户端注册信息
        http.oauth2ResourceServer((resourceServer) -> resourceServer
                // 使用JWT进行资源服务器保护
                .jwt(Customizer.withDefaults()));

        // 构建并返回 SecurityFilterChain
        return http.build();
    }

    /**
     * 注册客户端
     *
     * @param jdbcTemplate
     * @return
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }


    /**
     * 授权
     *
     * @param jdbcTemplate
     * @param registeredClientRepository
     * @return
     */
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        // 创建一个基于 JDBC 的 OAuth2 授权服务实例
        JdbcOAuth2AuthorizationService jdbcOAuth2AuthorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
        // 创建一个自定义的 OAuth2 授权行映射器
        JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper authorizationRowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);
        // 设置 LOB 处理器,用于处理大对象(如 BLOB 和 CLOB)
        authorizationRowMapper.setLobHandler(new DefaultLobHandler());

        // 创建一个 ObjectMapper 实例,用于 JSON 序列化和反序列化
        ObjectMapper objectMapper = new ObjectMapper();
        // 获取当前类加载器
        ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
        // 获取 Spring Security 提供的 Jackson 模块,这些模块包含了一些安全相关的序列化和反序列化逻辑
        List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
        // 注册这些安全模块到 ObjectMapper 中
        objectMapper.registerModules(securityModules);
        // 注册 OAuth2 授权服务器的 Jackson 模块
        objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
        // 注册自定义的 MixIn 类,用于自定义 `CustomUserDetails` 类的序列化和反序列化行为
        objectMapper.addMixIn(CustomUserDetails.class, CustomUserMixin.class);
        // 将配置好的 ObjectMapper 设置到授权行映射器中
        authorizationRowMapper.setObjectMapper(objectMapper);

        // 将自定义的授权行映射器设置到 JDBC 授权服务中
        jdbcOAuth2AuthorizationService.setAuthorizationRowMapper(authorizationRowMapper);

        // 返回配置好的 OAuth2 授权服务实例
        return jdbcOAuth2AuthorizationService;
    }

    /**
     * 签名访问令牌
     *
     * @return
     */
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        // 生成 RSA 密钥对
        KeyPair keyPair = generateRsaKey();
        // 获取公钥
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        // 获取私钥
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                // 设置私钥
                .privateKey(privateKey)
                // 设置键 ID
                .keyID(UUID.randomUUID().toString()).build();
        // 创建 JWKSet
        JWKSet jwkSet = new JWKSet(rsaKey);
        // 返回不可变的 JWKSet
        return new ImmutableJWKSet<>(jwkSet);
    }

    /**
     * 启动时生成的带有密钥的KeyPair实例,用于创建上面的JWKSource
     *
     * @return
     */
    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            // 获取 RSA 密钥对生成器
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            // 初始化密钥长度为 2048 位
            keyPairGenerator.initialize(2048);
            // 生成密钥对
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            // 如果生成密钥对失败,抛出异常
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    /**
     * JwtDecoder的一个实例,用于解码已签名的访问令牌
     *
     * @param jwkSource
     * @return
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        // 创建 JWT 解码器
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     * 配置Spring Authorization Server的AuthorizationServerSettings实例
     *
     * @return
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        // 创建授权服务器设置
        return AuthorizationServerSettings.builder().build();
    }
}

Spring Security的身份认证配置

/**
 * Spring Security的过滤器链,用于Spring Security的身份认证
 */
@EnableWebSecurity
/**
 * 禁用代理 Bean 方法以提高性能
 * 优化配置类的性能,通过减少方法调用检查的开销来提升性能。使用这个注解时,开发人员需要确保 @Bean 方法的行为是幂等的,以避免潜在的问题。
 */
@Configuration(proxyBeanMethods = false)
public class DefaultSecurityConfig {

    @Bean
    // 设置此过滤链的优先级为 2,确保它在授权服务器过滤链之后执行
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {
        http
                .authorizeHttpRequests((authorize) -> authorize
                        // 允许访问 /actuator/**
                        .requestMatchers(new AntPathRequestMatcher("/actuator/**"),
                                // 允许访问 /oauth2/**
                                new AntPathRequestMatcher("/oauth2/**"),
                                // 允许访问所有 .json 文件
                                new AntPathRequestMatcher("/**/*.json"),
                                // 允许访问所有 .html 文件
                                new AntPathRequestMatcher("/**/*.html"))
                        // 上述路径无需身份验证
                        .permitAll()
                        // 所有其他请求都需要身份验证
                        .anyRequest().authenticated()
                )
                // 启用 CORS 支持
                .cors(Customizer.withDefaults())
                // 禁用 CSRF 保护
                .csrf((csrf) -> csrf.disable())
                // 启用表单登录,并且表单登录处理从授权服务器过滤链重定向到登录页面
                .formLogin(Customizer.withDefaults())
        ;

        // 构建并返回 SecurityFilterChain
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

处理 OAuth 2.0 认证失败情况

/**
 * 处理 OAuth 2.0 认证失败情况
 */
@Component
public class Oauth2FailureHandler implements AuthenticationFailureHandler {


    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String message;
        if (exception instanceof OAuth2AuthenticationException auth2AuthenticationException) {
            OAuth2Error error = auth2AuthenticationException.getError();
            message = "认证信息错误:" + error.getErrorCode() + error.getDescription();
        } else {
            message = exception.getMessage();
        }

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.OK.value());
        response.getWriter().write(JSONObject.toJSONString(ApiResponse.error(401, message)));
        response.getWriter().flush();

    }
}

处理 OAuth 2.0 认证成功的情况

/**
 * 处理 OAuth 2.0 认证成功的情况
 */
@Component
public class Oauth2SuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        // 将 Authentication 对象转换为 OidcUserInfoAuthenticationToken 对象
        OidcUserInfoAuthenticationToken userInfoAuthenticationToken = (OidcUserInfoAuthenticationToken) authentication;

        // 设置响应内容类型为 JSON,字符集为 UTF-8
        response.setContentType("application/json;charset=UTF-8");
        // 设置响应状态码为 200 OK
        response.setStatus(HttpStatus.OK.value());
        // 将 JSON 字符串写入响应体
        response.getWriter().write(JSONObject.toJSONString(ApiResponse.success(userInfoAuthenticationToken.getUserInfo())));
        // 刷新响应输出流,确保数据被发送
        response.getWriter().flush();
    }
}

处理用户身份验证和获取用户信息

/**
 * 处理用户身份验证和获取用户信息
 */
@Slf4j
@Service
public class UserService implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    /**
     * 当进行身份验证时,Spring Security会调用此方法以获取用户的详细信息
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        User user = userMapper.selectOne(queryWrapper);
        return CustomUserDetails.fromUserEntity(username, user.getPassword());
    }

    public Map<String, Object> getUserInfoMap(String username) throws UsernameNotFoundException {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", username);
        User user = userMapper.selectOne(queryWrapper);
        Map<String, Object> result = new HashMap<>();
        result.put("id", user.getId());
        result.put("username", user.getUsername());
        result.put("name", user.getName());
        return result;
    }

}

2.问题

1.重定向到客户端

报错:org.springframework.security.access.AccessDeniedException: Access Denied

解决方案:客户端允许匿名访问重定向接口

/**
 * 启用Spring Security的安全配置
 */
@EnableWebSecurity
// 不要为配置类生成CGLIB代理,从而减少运行时开销。你需要确保在配置类中不会因为多次调用Bean方法而导致重复创建Bean实例。
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize ->
                        // 配置HTTP请求的授权规则
                        authorize
								// 允许匿名访问/logged-out和/callback路径
                                .requestMatchers("/logged-out", "/callback").permitAll()
								// 所有其他请求都需要经过身份验证
                                .anyRequest().authenticated()
                )
				// 配置注销功能,这里没有具体配置,只是启用了默认的注销功能
                .logout(logout -> {
                })
				// 配置OAuth2客户端功能,这里没有具体配置,只是启用了默认的客户端功能
                .oauth2Client(client -> {
                })
				// 配置OAuth2登录功能,这里没有具体配置,只是启用了默认的登录功能
                .oauth2Login(login -> {
                });
        return http.build();
    }
}
正文到此结束
本文目录