You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot中OAuth2与Basic Auth共存时401问题的解决

在单个Spring Boot应用中实现Basic认证与OAuth2资源共存的解决方案

问题背景

我有一个精简的Spring Boot Web应用,需求如下:

  • 路径/resource通过OAuth2/OIDC不透明令牌(opaque token)introspection机制保护,令牌校验后为匹配的客户端ID赋予oauth2角色
  • 路径/helloworld采用HTTP Basic认证保护,认证成功后映射到demo角色

目前/resource可以通过正确的Bearer令牌正常访问,但使用配置的用户名密码(均为demo)访问/helloworld时,始终返回401未授权,与未携带认证信息的请求结果一致。需要解决两种认证方式在同一应用中共存并正常工作的问题。

现有代码

DemoApplication.java

@SpringBootApplication
@RestController
public class DemoApplication {

    @PreAuthorize("hasRole('demo')")
    @GetMapping("/helloworld")
    public String hello() {
        return "Hello World!";
    }

    @PreAuthorize("hasRole('oauth2')")
    @GetMapping("/resource")
    public String resource() {
        return "Protected resource";
    }

    public static void main(String... args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

DemoSecurityConfiguration.java

public class DemoSecurityConfiguration {

        @Configuration
        @Order(10)
        @EnableWebSecurity
        public static class HelloWorldBasicSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
                @Override
                protected void configure(HttpSecurity http) throws Exception {

                        http.antMatcher("/helloworld")
                                .httpBasic()
                                .and()
                                .authorizeRequests().antMatchers("/helloworld").authenticated()
                        ;
                }
        }

        @Configuration
        @Order(0)
        @EnableWebSecurity
        public static class ResourceSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
                @Autowired
                private Environment environment;

                @Override
                protected void configure(HttpSecurity http) throws Exception {

                        http.antMatcher("/resource")
                                .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(
                                opaqueToken -> opaqueToken.introspector(new DemoAuthoritiesOpaqueTokenIntrospector())))
                                .authorizeRequests().antMatchers("/resource").authenticated()
                        ;
                }
                private class DemoAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
                        private final OpaqueTokenIntrospector delegate;

                        private final String demoClientId;

                        public DemoAuthoritiesOpaqueTokenIntrospector() {
                                String introSpectionUri = environment
                                        .getProperty("spring.security.oauth2.resourceserver.opaque-token.introspection-uri");
                                String clientId = environment
                                        .getProperty("spring.security.oauth2.resourceserver.opaque-token.client-id");
                                String clientSecret = environment
                                        .getProperty("spring.security.oauth2.resourceserver.opaque-token.client-secret");
                                demoClientId = environment.getProperty("demo.security.oauth2.credentials-grant.client-id");

                                delegate = new NimbusOpaqueTokenIntrospector(introSpectionUri, clientId, clientSecret);
                        }

                        public OAuth2AuthenticatedPrincipal introspect(String token) {
                                OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);

                                return new DefaultOAuth2AuthenticatedPrincipal(principal.getName(), principal.getAttributes(),
                                        extractAuthorities(principal));
                        }

                        private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
                                String userId = principal.getAttribute("client_id");

                                if (demoClientId.equals(userId)) {
                                        return Collections.singleton(new SimpleGrantedAuthority("ROLE_oauth2"));
                                }

                                return Collections.emptySet();
                        }
                }
        }

}

application.yaml

spring:
  security:
    basic:
      enabled: true
    user:
      name: demo
      password: demo
      roles: demo
    oauth2:
      resourceserver:
        opaque-token:
          introspection-uri: "https://...oauth2"
          client-id: "abba"
          client-secret: "secret"

demo:
  security:
    oauth2:
      credentials-grant:
        client-id: "rundmc

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.6'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation("org.springframework.security:spring-security-oauth2-resource-server")
    implementation("org.springframework.security:spring-security-oauth2-jose")
    implementation("org.springframework.security:spring-security-oauth2-client")
}

问题分析

  1. Basic认证用户未被正确加载:自定义的HelloWorldBasicSecurityConfigurerAdapter没有配置用户认证来源,导致Spring Boot自动配置的内存用户(spring.security.user下的配置)未生效,系统无法识别提供的用户名密码。
  2. application.yaml语法错误:demo.security.oauth2.credentials-grant.client-id的引号未闭合,会导致配置加载失败,可能影响OAuth2相关逻辑。
  3. 废弃配置冗余:spring.security.basic.enabled在Spring Boot 2.x版本中已被弃用,无需配置。

解决方案

1. 修正Basic认证配置类

在HelloWorldBasicSecurityConfigurerAdapter中添加用户认证配置,显式加载Spring Boot配置的内存用户:

@Configuration
@Order(10)
@EnableWebSecurity
public static class HelloWorldBasicSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    @Value("${spring.security.user.name}")
    private String username;

    @Value("${spring.security.user.password}")
    private String password;

    @Value("${spring.security.user.roles}")
    private String role;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser(username)
                .password("{noop}" + password) // {noop}表示不加密,生产环境需使用BCrypt等密码编码器
                .roles(role);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/helloworld")
                .httpBasic()
                .and()
                .authorizeRequests()
                .antMatchers("/helloworld").hasRole(role); // 与@PreAuthorize的角色校验对应
    }
}

2. 修正application.yaml

  • 闭合demo.client-id的引号
  • 移除废弃的spring.security.basic.enabled配置
spring:
  security:
    user:
      name: demo
      password: demo
      roles: demo
    oauth2:
      resourceserver:
        opaque-token:
          introspection-uri: "https://...oauth2"
          client-id: "abba"
          client-secret: "secret"

demo:
  security:
    oauth2:
      credentials-grant:
        client-id: "rundmc"

3. 确认配置优先级

两个配置类的@Order顺序正确:ResourceSecurityConfigurerAdapter的@Order(0)优先级更高,先处理/resource路径;HelloWorldBasicSecurityConfigurerAdapter的@Order(10)处理/helloworld路径,二者互不干扰。

测试验证

  1. 启动应用后,测试Basic认证:
curl -u demo:demo http://localhost:8080/helloworld

应返回Hello World!

  1. 测试OAuth2不透明令牌认证:
curl -H "Authorization: Bearer <有效令牌>" http://localhost:8080/resource

应返回Protected resource

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 20:20:30