You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于Spring拆分OAuth2授权服务器与资源服务器的改造方法问询

拆分Spring OAuth2授权服务器与资源服务器的实现指南

我之前也做过类似的拆分改造,其实核心就是把原来单应用里的授权逻辑和资源保护逻辑彻底分开成两个独立Spring Boot项目,下面一步步给你讲具体怎么改:


第一步:搭建独立的授权服务器(Auth Server)

这个服务器专门负责用户认证、存储用户/客户端信息、颁发Token。

1. 项目依赖与初始化

新建一个Spring Boot项目,引入这些核心依赖:

  • spring-boot-starter-security:基础安全框架
  • spring-boot-starter-oauth2-authorization-server(Spring Security 5.7+推荐)或旧版spring-security-oauth2-autoconfigure
  • spring-boot-starter-web:提供必要的端点访问
  • spring-boot-starter-data-jpa + 数据库驱动(比如MySQL):存储用户、客户端、Token信息
  • spring-boot-starter-security里的PasswordEncoder相关依赖

2. 核心配置改造

(1)用户认证配置:从数据库读取用户信息

原来教程可能用内存用户,现在改成从数据库查询,实现UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("用户不存在:" + username));
        return User.withUsername(user.getUsername())
                .password(user.getPassword())
                .authorities(user.getRoles().split(","))
                .build();
    }
}

同时配置密码编码器:

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

(2)授权服务器核心配置

继承AuthorizationServerConfigurerAdapter(旧版)或者用新的AuthorizationServerSettings(Spring Security 6+),这里给你旧版兼容的示例:

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private DataSource dataSource;

    // 客户端信息存储(用数据库存生产环境更安全)
    @Bean
    public ClientDetailsService clientDetailsService() {
        return new JdbcClientDetailsService(dataSource);
    }

    // Token存储(用数据库或Redis,不推荐内存)
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenStore(tokenStore());
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 测试阶段可以用内存客户端,生产换成数据库
        clients.inMemory()
                .withClient("my-client")
                .secret(passwordEncoder.encode("my-client-secret"))
                .authorizedGrantTypes("password", "refresh_token")
                .scopes("read", "write")
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(86400);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()") // 允许获取公钥(JWT场景用)
                .checkTokenAccess("isAuthenticated()") // 资源服务器验证Token需要认证
                .allowFormAuthenticationForClients(); // 允许表单方式提交客户端信息
    }
}

(3)WebSecurity配置

需要暴露AuthenticationManager给授权服务器使用:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/oauth/**", "/login/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().permitAll();
    }
}

第二步:搭建独立的资源服务器(Resource Server)

这个服务器只负责保护API资源,验证Token的合法性。

1. 项目依赖

新建另一个Spring Boot项目,引入:

  • spring-boot-starter-security
  • spring-boot-starter-oauth2-resource-server(或旧版spring-security-oauth2
  • spring-boot-starter-web:提供API接口

2. 核心配置改造

(1)Token验证配置

分两种场景:

场景A:用JWT Token(推荐生产环境)

直接通过授权服务器的JWKS端点或公钥验证Token:

@Configuration
@EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/api/**").authenticated()
                .and()
                .oauth2ResourceServer()
                .jwt()
                .decoder(jwtDecoder());
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        // 替换成你的授权服务器地址
        return JwtDecoders.fromIssuerLocation("http://localhost:8080");
    }
}
场景B:用普通Token(内存/数据库存储)

通过授权服务器的/oauth/check_token端点验证:

@Configuration
@EnableWebSecurity
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public ResourceServerTokenServices tokenServices() {
        RemoteTokenServices tokenServices = new RemoteTokenServices();
        // 授权服务器的Token验证端点
        tokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
        // 和授权服务器配置的客户端信息一致
        tokenServices.setClientId("my-client");
        tokenServices.setClientSecret("my-client-secret");
        return tokenServices;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/api/**").authenticated()
                .and()
                .resourceServer()
                .tokenServices(tokenServices());
    }
}

(2)添加测试API

比如写一个简单的受保护接口:

@RestController
@RequestMapping("/api")
public class ResourceController {

    @GetMapping("/user")
    public Principal getCurrentUser(Principal principal) {
        return principal;
    }
}

第三步:关键注意事项

  1. 端口区分:授权服务器和资源服务器要用不同端口,比如授权服务器用8080,资源服务器用8081
  2. 数据库表结构:如果用JDBC存储Token和客户端信息,Spring Security OAuth2有默认的表结构(比如oauth_client_detailsoauth_access_token),可以直接生成
  3. 跨域配置:两个独立应用需要配置CORS,允许对方域名访问,比如在资源服务器添加:
@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("http://localhost:8080"); // 授权服务器地址
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}
  1. 测试流程:先启动授权服务器,再启动资源服务器 → 用POST请求http://localhost:8080/oauth/token,携带客户端信息和用户账号密码获取Token → 用Token请求资源服务器的http://localhost:8081/api/user接口,验证是否能正常访问

内容的提问来源于stack exchange,提问作者Magno C

火山引擎 最新活动