SpringSecurity认证服务器:OAuth2授权服务器实现

在这里插入图片描述

文章目录

    • 引言
    • 一、Spring Authorization Server基础
    • 二、基础配置与授权服务器设置
    • 三、用户认证与客户端管理
    • 四、令牌定制与自定义声明
    • 五、授权确认页面定制
    • 总结

引言

在微服务架构中,统一的身份认证和授权机制至关重要。OAuth2作为行业标准的授权框架,被广泛应用于各类应用系统。Spring Authorization Server项目提供了对OAuth2授权服务器的原生支持,本文将探讨如何使用Spring Security实现一个功能完备的OAuth2授权服务器,包括客户端注册、授权流程配置以及令牌管理,帮助开发者构建安全可靠的认证授权中心。

一、Spring Authorization Server基础

Spring Authorization Server是Spring Security团队开发的框架,用于构建符合OAuth2.1和OpenID Connect 1.0规范的授权服务器。要使用它,首先需要添加相关依赖:

// 在pom.xml中添加以下依赖
<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>1.1.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Spring Authorization Server支持OAuth2.1规范中的多种授权类型,包括授权码模式、客户端凭证模式、刷新令牌模式等,还支持OpenID Connect扩展,可以签发ID令牌用于身份认证。

二、基础配置与授权服务器设置

配置OAuth2授权服务器需要设置安全过滤链和基本参数。以下是核心配置示例:

@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
            .oidc(Customizer.withDefaults());    // 启用OpenID Connect
        
        http
            .exceptionHandling(exceptions -> exceptions
                .authenticationEntryPoint(
                    new LoginUrlAuthenticationEntryPoint("/login"))
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults()));
        
        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/assets/**", "/login").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());
        
        return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
        RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("client")
            .clientSecret(passwordEncoder.encode("secret"))
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .redirectUri("http://127.0.0.1:8080/login/oauth2/code/client")
            .scope(OidcScopes.OPENID)
            .scope("read")
            .scope("write")
            .clientSettings(ClientSettings.builder()
                .requireAuthorizationConsent(true)
                .build())
            .tokenSettings(TokenSettings.builder()
                .accessTokenTimeToLive(Duration.ofMinutes(30))
                .refreshTokenTimeToLive(Duration.ofDays(1))
                .build())
            .build();
        
        return new InMemoryRegisteredClientRepository(client);
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(UUID.randomUUID().toString())
            .build();
        
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
            .issuer("http://auth-server:9000")
            .build();
    }
    
    // 生成RSA密钥对
    private static KeyPair generateRsaKey() {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            return keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }
}

这个配置类定义了两个安全过滤链(一个用于授权服务器端点,另一个用于常规Web安全),注册了客户端信息,配置了JWT签名密钥,并设置了授权服务器的基本参数。

三、用户认证与客户端管理

授权服务器需要管理用户和客户端信息。以下是用户认证服务的实现:

@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
    UserDetails user = User.builder()
        .username("user")
        .password(passwordEncoder.encode("password"))
        .roles("USER")
        .build();
    
    UserDetails admin = User.builder()
        .username("admin")
        .password(passwordEncoder.encode("admin"))
        .roles("USER", "ADMIN")
        .build();
    
    return new InMemoryUserDetailsManager(user, admin);
}

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

在实际应用中,通常需要将客户端信息存储在数据库中。可以实现JdbcRegisteredClientRepository或自定义RegisteredClientRepository:

@Component
public class JpaRegisteredClientRepository implements RegisteredClientRepository {
    private final ClientRepository clientRepository;
    
    // 实现findById、findByClientId和save方法
    // 将RegisteredClient对象与数据库实体进行转换
    // ...
}

四、令牌定制与自定义声明

OAuth2授权服务器默认使用JWT作为访问令牌格式。可以自定义令牌内容,添加额外的用户信息或业务相关的声明:

@Component
public class CustomJwtTokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {

    @Override
    public void customize(JwtEncodingContext context) {
        if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
            Authentication principal = context.getPrincipal();
            
            JwtClaimsSet.Builder claims = context.getClaims();
            claims.claim("user_id", principal.getName());
            
            // 添加用户角色
            Set<String> authorities = principal.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.toSet());
            claims.claim("roles", authorities);
            
            // 添加其他自定义声明
            claims.claim("custom_claim", "custom_value");
        }
    }
}

要启用这个自定义器,需要在配置类中注册它:

@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() {
    return new CustomJwtTokenCustomizer();
}

五、授权确认页面定制

在授权码流程中,用户需要确认授予客户端的权限范围。可以自定义授权确认页面,提供更好的用户体验:

@Controller
public class AuthorizationConsentController {

    private final RegisteredClientRepository clientRepository;
    
    @GetMapping("/oauth2/consent")
    public String consentPage(
            @RequestParam("client_id") String clientId,
            @RequestParam("scope") String scope,
            Model model) {
        
        RegisteredClient client = clientRepository.findByClientId(clientId);
        model.addAttribute("clientName", client.getClientName());
        model.addAttribute("scopes", Arrays.asList(scope.split(" ")));
        
        return "consent";
    }
}

在配置中启用自定义确认页面:

authorizationServerConfigurer.authorizationEndpoint(
    authorizationEndpoint -> authorizationEndpoint.consentPage("/oauth2/consent")
);

总结

Spring Authorization Server为构建OAuth2授权服务器提供了强大而灵活的支持。通过适当的配置,可以快速实现一个功能完备的认证授权中心,支持多种授权类型和客户端认证方法。本文介绍了Spring Authorization Server的基础配置、客户端注册、用户认证、令牌定制以及授权确认页面的定制方法。在实际应用中,还需要考虑令牌撤销、刷新策略、安全加固等问题,以构建一个安全可靠的OAuth2认证授权体系。合理利用Spring Security的模块化设计和丰富功能,结合业务需求进行定制,可以构建出既满足安全要求又具有良好用户体验的认证授权服务。

你可能感兴趣的:(Spring,全家桶,Java,服务器,运维,spring,java)