首页 > 其他分享 >Spring Cloud整合Spring Security Oauth2

Spring Cloud整合Spring Security Oauth2

时间:2024-03-25 21:29:46浏览次数:32  
标签:ci Oauth2 utf8mb4 Spring token SET new Security NULL

前言

在当今数字化时代,随着企业业务规模和复杂性的不断增加,传统的单体应用架构已经难以满足日益增长的需求。微服务架构的兴起,以其高度模块化、可扩展性和可维护性的优势,逐渐成为企业架构升级的首选方案。然而,随着微服务的普及,如何保障服务的安全性、实现用户身份的统一管理和认证,成为了摆在企业和开发者面前的重要课题。
OAuth 2.1作为一种开放标准,为微服务架构下的用户身份认证和授权提供了强有力的支持。它允许第三方应用获取用户在特定服务上的有限访问权限,而无需获取用户的用户名和密码。通过OAuth 2.1,企业可以构建安全、灵活的身份认证体系,实现用户身份的统一管理,确保服务间的数据安全和隐私保护。
本文档旨在探讨微服务集成OAuth 2.1的实践与应用。我们将从OAuth 2.1的基本原理入手,介绍其核心概念和工作流程。接着,我们将详细阐述如何在微服务架构中集成OAuth 2.1,包括认证服务器的搭建、客户端的配置、以及授权流程的实现。此外,我们还将分享一些在实际应用中可能遇到的问题和解决方案,帮助读者更好地理解和应用OAuth 2.1。
通过本文档的学习,读者将能够掌握微服务集成OAuth 2.1的关键技术和方法,为企业构建安全、高效的微服务架构提供有力支持。我们相信,随着OAuth 2.1在微服务领域的广泛应用,它将为企业带来更多的商业价值和发展机遇。

步骤

初始化数据库

/*
 Navicat Premium Data Transfer

 Source Server         : [email protected]
 Source Server Type    : MySQL
 Source Server Version : 80200
 Source Host           : 154.8.144.189:13306
 Source Schema         : unified_certification

 Target Server Type    : MySQL
 Target Server Version : 80200
 File Encoding         : 65001

 Date: 25/03/2024 13:24:15
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for authorities
-- ----------------------------
DROP TABLE IF EXISTS `authorities`;
CREATE TABLE `authorities`  (
  `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `authority` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  UNIQUE INDEX `ix_auth_username`(`username`, `authority`) USING BTREE,
  CONSTRAINT `fk_authorities_users` FOREIGN KEY (`username`) REFERENCES `users` (`username`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for oauth2_authorization
-- ----------------------------
DROP TABLE IF EXISTS `oauth2_authorization`;
CREATE TABLE `oauth2_authorization`  (
  `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `authorization_grant_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `authorized_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `attributes` blob NULL,
  `state` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_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_0900_ai_ci NULL DEFAULT NULL,
  `access_token_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_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,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_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_0900_ai_ci NOT NULL,
  `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `authorities` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  PRIMARY KEY (`registered_client_id`, `principal_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_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_0900_ai_ci NOT NULL,
  `client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `client_id_issued_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `client_secret` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `client_secret_expires_at` timestamp NULL DEFAULT NULL,
  `client_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `client_authentication_methods` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `authorization_grant_types` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `redirect_uris` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `client_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `token_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users`  (
  `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `PASSWORD` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `enabled` tinyint(1) NOT NULL,
  PRIMARY KEY (`username`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

引入相关依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-authorization-server</artifactId>
  <version>0.4.0</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.8.26</version>
</dependency>
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.18.30</version>
</dependency>

添加相关配置

server:
  port: 9000

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/unified_certification?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    hikari:
      pool-name: HikariCP
      minimum-idle: 5
      maximum-pool-size: 15
      auto-commit: true
      idle-timeout: 30000
      connection-timeout: 30000
      connection-test-query: SELECT 1
      max-lifetime: 25200000

授权服务配置

@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
    private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        // 定义授权服务配置器
        OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer();
        configurer.tokenEndpoint(tokenEndpoint -> {
            tokenEndpoint.accessTokenRequestConverter(new OAuth2PasswordAuthenticationConverter());
        })
        // 自定义授权页面
        //                .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))
        // Enable OpenID Connect 1.0, 启用 OIDC 1.0
        .oidc(Customizer.withDefaults());


        // 获取授权服务器相关的请求端点
        RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher();

        http
        // 拦截对授权服务器相关端点的请求
        .requestMatcher(endpointsMatcher)
        // 拦载到的请求需要认证
        .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
        // 忽略掉相关端点的 CSRF(跨站请求): 对授权端点的访问可以是跨站的
        .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
        .exceptionHandling(exceptions ->
                           exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
                          )
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
        // 应用授权服务器的配置
        .apply(configurer)
        .and()
        // 登出处理
        .logout().deleteCookies("JSESSIONID")
        .invalidateHttpSession(true); // SSO登出成功处理;
        DefaultSecurityFilterChain securityFilterChain = http.build();
        // 注入自定义授权模式实现
        http.authenticationProvider(
            new OAuth2PasswordAuthenticationProvider(
                http.getSharedObject(OAuth2AuthorizationService.class),
                http.getSharedObject(JwtGenerator.class),
                new OAuth2RefreshTokenGenerator(),
                http.getSharedObject(AuthenticationManager.class)
            ));

        return securityFilterChain;
    }

    /**
     * 注册客户端应用, 对应 oauth2_registered_client 表
     */
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
        // JWT(Json Web Token)的配置项:TTL、是否复用refreshToken等等
        TokenSettings tokenSettings = TokenSettings.builder()
        // 令牌存活时间:2小时
        .accessTokenTimeToLive(Duration.ofHours(2))
        // 令牌可以刷新,重新获取
        .reuseRefreshTokens(true)
        // 刷新时间:30天(30天内当令牌过期时,可以用刷新令牌重新申请新令牌,不需要再认证)
        .refreshTokenTimeToLive(Duration.ofDays(30))
                .build();
        // 客户端相关配置
        ClientSettings clientSettings = ClientSettings.builder()
                // 是否需要用户授权确认
                .requireAuthorizationConsent(true)
                .build();
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                // 客户端ID和密码
                .clientId("messaging-client")
//                .clientSecret("{bcrypt}" + new BCryptPasswordEncoder().encode("secret"))
                .clientSecret("{noop}secret")
                // 授权方法
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                // 授权模式(授权码模式)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                // 刷新令牌(授权码模式)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                // 回调地址:授权服务器向当前客户端响应时调用下面地址, 不在此列的地址将被拒绝, 只能使用IP或域名,不能使用 localhost
                .redirectUri("http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc")
                // OIDC 支持
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                // 授权范围(当前客户端的授权范围)
                .scope("message.read")
                .scope("message.write")
                // JWT(Json Web Token)配置项
                .tokenSettings(tokenSettings)
                // 客户端配置项
                .clientSettings(clientSettings)
                .build();
        if (registeredClientRepository.findByClientId("messaging-client") == null) {
            registeredClientRepository.save(registeredClient);
        }
        return registeredClientRepository;
    }

    /**
     * 令牌的发放记录, 对应 oauth2_authorization 表
     */
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 把资源拥有者授权确认操作保存到数据库, 对应 oauth2_authorization_consent 表
     */
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 加载 JWT 资源, 用于生成令牌
     */
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        // 生成密钥对
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    
        // 从密钥对中获取公钥和私钥
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
    
        // 创建 RSAKey 对象,设置公钥、私钥和 keyID
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
    
        // 创建 JWKSet 对象,并添加 rsaKey
        JWKSet jwkSet = new JWKSet(rsaKey);
    
        // 返回 JWKSource 对象,用于选择 JWKSet 中的密钥
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    /**
     * JWT 解码
     */
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    /**
     * AuthorizationServerS 的相关配置
     */
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }
}

WebSecurity相关配置

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class ServerSecurityConfig {

    @Resource
    private DataSource dataSource;

    /**
     * Spring Security 的过滤器链,用于 Spring Security 的身份认证
     */
    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        // 配置放行的请求
                        .antMatchers("/api/**", "/login").permitAll()
                        // 其他任何请求都需要认证
                        .anyRequest().authenticated()
                )
                // 设置登录表单页面
                .formLogin(Customizer.withDefaults());
//                .formLogin(formLoginConfigurer -> formLoginConfigurer.loginPage("/login"));

        return http.build();
    }

    @Bean
        // 创建一个 UserDetailsManager 对象
    UserDetailsManager userDetailsManager() {
        // 使用 JdbcUserDetailsManager 来实现 UserDetailsManager 接口
        JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager(dataSource);

        // 创建一个 UserDetails 对象
        UserDetails userDetails = User.builder()
                // 设置密码编码器为 BCryptPasswordEncoder
                .passwordEncoder(s -> "{bcrypt}" + new BCryptPasswordEncoder().encode(s))
                // 设置用户名
                .username("user")
                // 设置密码
                .password("password")
                // 设置角色为 ADMIN
                .roles("ADMIN")
                // 构建 UserDetails 对象
                .build();

        // 判断用户是否存在
        if (!userDetailsManager.userExists(userDetails.getUsername())) {
            // 如果用户不存在,则创建用户
            userDetailsManager.createUser(userDetails);
        }

        // 返回 UserDetailsManager 对象
        return userDetailsManager;
    }

}

自定义密码授权转换器

public class OAuth2PasswordAuthenticationConverter implements AuthenticationConverter {

    public final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";

    @Override
    public Authentication convert(HttpServletRequest request) {
        //验证是否是密码模式
        String grantType = request.getParameter("grant_type");
        if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) {
            return null;
        } else {
            // 获取客户端认证信息
            Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
            //获取请求中所有参数
            MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
            //获取用户名信息
            String username = parameters.getFirst("username");
            //验证用户名是否为空,且只有一个
            if (!StringUtils.hasText(username) || parameters.get("password").size() != 1) {
                OAuth2EndpointUtils.throwError("invalid_request", "username", null);
            }
            //获取密码信息
            String password = parameters.getFirst("password");
            //验证密码是否为空,且只有一个
            if (!StringUtils.hasText(password) || parameters.get("password").size() != 1) {
                OAuth2EndpointUtils.throwError("invalid_request", "password", null);
            }
            //获取其他参数
            Map<String, Object> additionalParameters = new HashMap<>();
            parameters.forEach((key, value) -> {
                if (!key.equals("grant_type") && !key.equals("client_id") && !key.equals("username") && !key.equals("password")) {
                    additionalParameters.put(key, value.get(0));
                }
            });
            // 获取 scope 参数
            String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
            if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
                OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE, ACCESS_TOKEN_REQUEST_ERROR_URI);
            }
            // 解析 scope 参数值
            Set<String> requestedScopes = null;
            if (StringUtils.hasText(scope)) {
                requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
            }
            // 返回 OAuth2PasswordAuthenticationToken 对象
            return new OAuth2PasswordAuthenticationToken(username, clientPrincipal, password, additionalParameters, requestedScopes);
        }
    }

}

自定义密码授权实体

public class OAuth2PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {

    @Getter
    private final String username;
    @Getter
    private final String password;
    @Getter
    private final Set<String> scopes;

    public OAuth2PasswordAuthenticationToken(
            // 用户名
            String username,
            // 客户端主体
            Authentication clientPrincipal,
            // 密码
            String password,
            // 附加参数
            Map<String, Object> additionalParameters,
            // 授权范围
            Set<String> scopes) {
        // 调用父类构造函数,设置授权类型为密码模式
        super(AuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters);
        // 断言用户名不为空
        Assert.hasText(username, "code cannot be empty");
        // 设置用户名
        this.username = username;
        // 设置密码
        this.password = password;
        // 设置授权范围,并确保不可修改
        this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
    }


}

自定义密码授权处理器

public class OAuth2PasswordAuthenticationProvider implements AuthenticationProvider {

    private static final Logger LOGGER = LogManager.getLogger(OAuth2PasswordAuthenticationProvider.class);

    private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";

    private final OAuth2AuthorizationService authorizationService;

    private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;

    private final AuthenticationManager authenticationManager;

    private final OAuth2TokenGenerator<? extends OAuth2Token> refreshTokenGenerator;

    public OAuth2PasswordAuthenticationProvider(OAuth2AuthorizationService authorizationService,
                                                JwtGenerator tokenGenerator,
                                                OAuth2RefreshTokenGenerator refreshTokenGenerator,
                                                AuthenticationManager authenticationManager) {
        // 断言authorizationService不为空
        Assert.notNull(authorizationService, "authorizationService cannot be null");
        // 断言tokenGenerator不为空
        Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
        // 将传入的authorizationService赋值给当前对象的authorizationService成员变量
        this.authorizationService = authorizationService;
        // 将传入的tokenGenerator赋值给当前对象的tokenGenerator成员变量
        this.tokenGenerator = tokenGenerator;
        // 将传入的refreshTokenGenerator赋值给当前对象的refreshTokenGenerator成员变量
        this.refreshTokenGenerator = refreshTokenGenerator;
        // 将传入的authenticationManager赋值给当前对象的authenticationManager成员变量
        this.authenticationManager = authenticationManager;
    }


    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 类型转换
        OAuth2PasswordAuthenticationToken authenticationToken = (OAuth2PasswordAuthenticationToken) authentication;
        // 获取客户端认证信息
        OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(
                authenticationToken);
        // 获取客户端信息
        RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
        // 断言客户端信息不为空
        assert registeredClient != null;
        //验证是否支持密码模式
        if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) {
            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
        }
        // 获取客户端认证方法
        Set<String> authorizedScopes = new LinkedHashSet<>();
        if (!CollectionUtils.isEmpty(authenticationToken.getScopes())) {
            for (String requestedScope : authenticationToken.getScopes()) {
                if (!registeredClient.getScopes().contains(requestedScope)) {
                    throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
                }
            }
            authorizedScopes = new LinkedHashSet<>(authenticationToken.getScopes());
        }
        // 获取用户名
        String username = authenticationToken.getUsername();
        // 获取密码
        String password = authenticationToken.getPassword();
        // 创建用户名密码认证实体
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        // 认证
        Authentication usernamePasswordAuthentication = authenticationManager
                .authenticate(usernamePasswordAuthenticationToken);
        // 创建token上下文
        DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
                .registeredClient(registeredClient)
                .principal(usernamePasswordAuthentication)
                .authorizationServerContext(AuthorizationServerContextHolder.getContext())
                .authorizedScopes(authorizedScopes)
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .authorizationGrant(authenticationToken);
        // 创建授权
        OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization
                .withRegisteredClient(registeredClient)
                .principalName(usernamePasswordAuthentication.getName())
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .authorizedScopes(authorizedScopes);
        // 创建token上下文
        OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
        // 生成token
        OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
        // 判断生成的token是否为空
        if (generatedAccessToken == null) {
            OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
                    "The token generator failed to generate the access token.", ERROR_URI);
            throw new OAuth2AuthenticationException(error);
        }
        // 创建accessToken
        OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
                generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
                generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
        // 判断生成的token是否为ClaimAccessor类型
        if (generatedAccessToken instanceof ClaimAccessor) {
            authorizationBuilder.token(accessToken, (metadata) -> {
                        metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
                    })
                    .attribute(Principal.class.getName(), usernamePasswordAuthentication);
        } else {
            authorizationBuilder.accessToken(accessToken);
        }
        // 创建refreshToken
        OAuth2RefreshToken refreshToken = null;
        // 判断是否支持刷新令牌
        if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) && !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
            tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
            OAuth2Token generatedRefreshToken = this.refreshTokenGenerator.generate(tokenContext);
            if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
                OAuth2Error error = new OAuth2Error("server_error", "The token generator failed to generate the refresh token.", "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2");
                throw new OAuth2AuthenticationException(error);
            }
            refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
            authorizationBuilder.refreshToken(refreshToken);
        }
        // 创建授权
        OAuth2Authorization authorization = authorizationBuilder.build();
        // 保存授权
        this.authorizationService.save(authorization);

        LOGGER.debug("returning OAuth2AccessTokenAuthenticationToken");
        // 创建认证
        return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, Collections.emptyMap());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 判断传入的authentication对象是否是指定类型或其子类的实例
        return OAuth2PasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

}

用到的工具类

public class OAuth2AuthenticationProviderUtils {
    private OAuth2AuthenticationProviderUtils() {
    }

    public static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
        // 定义一个OAuth2ClientAuthenticationToken类型的变量clientPrincipal,用于存储客户端主体
        OAuth2ClientAuthenticationToken clientPrincipal = null;
        // 判断authentication中的主体是否为OAuth2ClientAuthenticationToken类型
        if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
            // 如果是,则将其转换为OAuth2ClientAuthenticationToken类型,并赋值给clientPrincipal
            clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
        }
    
        // 判断clientPrincipal是否不为null且已经认证通过
        if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
            // 如果是,则返回clientPrincipal
            return clientPrincipal;
        } else {
            // 否则,抛出OAuth2AuthenticationException异常,表示客户端无效
            throw new OAuth2AuthenticationException("invalid_client");
        }
    }

}
public class OAuth2EndpointUtils {

    public static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
        // 获取请求参数映射表
        Map<String, String[]> parameterMap = request.getParameterMap();
        // 创建一个可修改的键值对集合
        MultiValueMap<String, String> parameters = new LinkedMultiValueMap(parameterMap.size());
        // 遍历参数映射表
        parameterMap.forEach((key, values) -> {
            // 如果参数值数组长度大于0
            if (values.length > 0) {
                // 定义一个变量来引用参数值数组
                String[] var3 = values;
                // 获取参数值数组的长度
                int var4 = values.length;
    
                // 遍历参数值数组
                for (int var5 = 0; var5 < var4; ++var5) {
                    // 获取当前参数值
                    String value = var3[var5];
                    // 将参数名和参数值添加到集合中
                    parameters.add(key, value);
                }
            }
    
        });
        // 返回参数集合
        return parameters;
    }


    public static void throwError(String errorCode, String parameterName, String errorUri) {
        // 创建一个OAuth2Error对象,包含错误码、错误信息和错误URI
        OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
        // 抛出一个OAuth2AuthenticationException异常,包含上述创建的OAuth2Error对象
        throw new OAuth2AuthenticationException(error);
    }

}

测试

授权码模式

  1. 请求授权地址

http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=message.read&redirect_uri=http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc

OIDC模式需要将scope值设置为openid

  1. 重定向到登录,输入用户名密码登录
  2. 跳转到授权页面,勾选授权信息
  3. 重定向到回调地址并携带code
  4. 获取token

image.png
image.png

  1. OIDC获取token

image.png

刷新token

  1. 刷新token

image.png
image.png
OIDC刷新token
image.png

客户端模式

image.png
image.png

密码模式

image.png
image.png

获取用户信息

下面展示的是OIDC授权默认的获取用户信息接口,如果是其他模式授权需要自定义获取用户信息接口

image.png

总结

该集成方案是依据官方提供的例子完成的,额外支持了密码模式。


标签:ci,Oauth2,utf8mb4,Spring,token,SET,new,Security,NULL
From: https://blog.csdn.net/qq_38036909/article/details/137018125

相关文章

  • Spring-mybatis
    新建spring-dao.xml文件<?xmlversion="1.0"encoding="UTF-8"?><beansxmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http......
  • 基于SpringBoot+Vue的大学生兼职管理系统的详细设计和实现(源码+lw+部署文档+讲解等)
    文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我自己的网站自己的小程序(小蔡coding)代码参考数据库参考源码获取前言......
  • 基于SpringBoot+Vue的大学校园旧物捐赠网站的详细设计和实现(源码+lw+部署文档+讲解等
    文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我自己的网站自己的小程序(小蔡coding)代码参考数据库参考源码获取前言......
  • 基于SpringBoot+Vue的大学生二手闲置物品置换交易管理系统的详细设计和实现(源码+lw+
    文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我自己的网站自己的小程序(小蔡coding)代码参考数据库参考源码获取前言......
  • 基于SpringBoot+Vue的食品安全信息管理系统的详细设计和实现(源码+lw+部署文档+讲解等
    文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我自己的网站自己的小程序(小蔡coding)代码参考数据库参考源码获取前言......
  • SpringBoot如何优雅的进行参数校验
    写在前面上一篇文章中我们学会了如何优雅的接收前端参数,传送门SpringBoot如何优雅的接收前端参数接收到参数后,接下来要做的就是校验参数的合法性。这一步的重要性就不用多说了。即使前端已经对数据进行了校验,我们后端还是要再对接收到的数据进行一遍彻底的校验。这样可以避免......
  • 关于Spring+的测试
    使用了Spring+的产品,默认需要使用集成测试了。Spring通过SpringTestContextFramework对集成测试提供顶级支持,其不依赖于特定的测试框架。下面示例使用了Spring+中不同产品的测试:1、使用了Spring,参考Spring配置之常用配置概述中关于Profile的部分。对于测试部分具体说明......
  • SpringBoot3项目使用Knife4j时访问doc.html出现Knife4j文档请求异常且开发者工具网络
    1.在各个pom.xml中替换Knife4j的依赖版本,升级为4.0以上,如果找不到依赖可以在Maven配置中多添加几个镜像,或者使用汉化插件重启IDEA;<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId......
  • Java版商城:Spring Cloud+SpringBoot b2b2c实现多商家入驻直播带货及 免 费 小程序商城
    随着互联网的快速发展,越来越多的企业开始注重数字化转型,以提升自身的竞争力和运营效率。在这个背景下,鸿鹄云商SAAS云产品应运而生,为企业提供了一种简单、高效、安全的数字化解决方案。鸿鹄云商SAAS云产品是一种基于云计算的软件服务,旨在帮助企业实现业务流程的自动化和优化。......
  • Java版企业电子招投标系统源代码,支持二次开发,采用Spring cloud技术
     在数字化时代,采购管理也正经历着前所未有的变革。全过程数字化采购管理成为了企业追求高效、透明和规范的关键。该系统通过SpringCloud、SpringBoot2、Mybatis等先进技术,打造了从供应商管理到采购招投标、采购合同、采购执行的全过程数字化管理。通过待办消息、招标公告、......