1. 概述

本文将演示如何使用 Spring Boot 和 Spring Security OAuth 构建一个应用,实现将用户认证委托给第三方(如 GitHub)或自定义授权服务器(Authorization Server)。

✅ 核心重点是:如何通过 Spring 提供的 PrincipalExtractorAuthoritiesExtractor 接口,自定义提取用户身份(Principal)和权限(Authorities)

如果你对 Spring Security OAuth2 基础还不熟悉,建议先阅读相关入门文章。

⚠️ 注意:本文基于 Spring Security OAuth2 Client 模块,适用于 OAuth2 登录场景。该模块在较新版本中已被 Spring Security 5.7+ 的原生 OAuth2 支持逐步取代,但目前仍广泛使用。


2. Maven 依赖

首先,引入关键依赖:

<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.6.8</version>
</dependency>

这个依赖会自动配置 OAuth2 客户端所需的核心组件,包括登录流程、令牌获取、用户信息拉取等。


3. 使用 GitHub 实现 OAuth 认证

我们先配置基于 GitHub 的 OAuth 登录。

安全配置类

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.antMatcher("/**")
            .authorizeRequests()
            .antMatchers("/login**")
            .permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .formLogin()
            .disable()
            .oauth2Login();
        return http.build();
    }
}

📌 简单解释下关键点:

  • antMatcher("/**"):拦截所有请求
  • /login** 允许匿名访问(GitHub 回调地址通常是 /login/oauth2/code/github
  • oauth2Login():启用 OAuth2 登录,Spring 会自动处理登录跳转和回调

配置 GitHub 客户端信息

application.ymlapplication.properties 中添加:

spring.security.oauth2.client.registration.github.client-id=89a7c4facbb3434d599d
spring.security.oauth2.client.registration.github.client-secret=9b3b08e4a340bd20e866787e4645b54f73d74b6a
spring.security.oauth2.client.registration.github.scope=read:user,user:email

spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token
spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize
spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user

📌 说明:

  • client-idclient-secret 是你在 GitHub Developer Settings 中注册应用时获得的
  • scope 指定需要访问的资源范围,read:useruser:email 可获取用户名和邮箱
  • 三个 URI 分别对应 OAuth2 流程中的授权、令牌获取和用户信息接口

通过这种方式,我们把用户认证甩锅给 GitHub,省去了账号系统开发的麻烦,专注业务逻辑。


4. 自定义提取 Principal 与 Authorities

当你的应用作为 OAuth 客户端时,整个流程大致分为三步:

  1. ✅ 用户在第三方平台完成认证(如 GitHub 登录)
  2. ✅ 用户授权你的应用访问其数据(通过 scope 控制)
  3. ✅ 使用获取到的 access token 调用 user-info-uri 获取用户信息

Spring 默认会根据返回的用户信息自动构建 PrincipalAuthorities,但往往不够用。比如:

  • 默认 Principal 是一个 OAuth2User 对象,不好直接用
  • Authorities 通常是空的或固定值,无法体现用户角色

解决方案:自定义 Extractor

Spring 提供了两个接口让你接管提取逻辑:

  • PrincipalExtractor:自定义 Principal 内容
  • AuthoritiesExtractor:自定义 Authorities(权限/角色)

✅ 注意:这两个接口属于 spring-security-oauth2-autoconfigure,不是 Spring Security 核心模块。

默认实现是 FixedPrincipalExtractorFixedAuthoritiesExtractor,策略固定,灵活性差。我们可以通过注册自定义 Bean 来覆盖默认行为。


4.1 自定义 GitHub 认证逻辑

我们知道 GitHub 的用户接口返回结构(参考 GitHub API 文档),可以据此定制。

自定义 PrincipalExtractor

我们希望 Principal 就是用户的 GitHub 用户名(login 字段):

public class GithubPrincipalExtractor implements PrincipalExtractor {

    @Override
    public Object extractPrincipal(Map<String, Object> map) {
        return map.get("login");
    }
}

简单粗暴,直接从用户信息 JSON 中取 login 字段作为 Principal。

自定义 AuthoritiesExtractor

根据用户是否为付费账户(plan 字段),分配不同权限:

public class GithubAuthoritiesExtractor implements AuthoritiesExtractor {
    
    List<GrantedAuthority> GITHUB_FREE_AUTHORITIES =
        AuthorityUtils.commaSeparatedStringToAuthorityList(
            "GITHUB_USER,GITHUB_USER_FREE");
    
    List<GrantedAuthority> GITHUB_SUBSCRIBED_AUTHORITIES =
        AuthorityUtils.commaSeparatedStringToAuthorityList(
            "GITHUB_USER,GITHUB_USER_SUBSCRIBED");

    @Override
    public List<GrantedAuthority> extractAuthorities(Map<String, Object> map) {
        if (Objects.nonNull(map.get("plan"))) {
            LinkedHashMap<String, Object> plan = (LinkedHashMap<String, Object>) map.get("plan");
            if (!"free".equals(plan.get("name"))) {
                return GITHUB_SUBSCRIBED_AUTHORITIES;
            }
        }
        return GITHUB_FREE_AUTHORITIES;
    }
}

📌 踩坑提醒:map.get("plan") 返回的是 LinkedHashMap,注意类型转换。

注册为 Spring Bean

SecurityConfig 中注册这两个组件:

@Configuration
public class SecurityConfig {
    
    // ... 其他配置

    @Bean
    public PrincipalExtractor githubPrincipalExtractor() {
        return new GithubPrincipalExtractor();
    }

    @Bean
    public AuthoritiesExtractor githubAuthoritiesExtractor() {
        return new GithubAuthoritiesExtractor();
    }
}

只要 Bean 名称或类型匹配,Spring 会自动注入并替换默认实现。


4.2 使用自定义授权服务器

你也可以不依赖 GitHub,而是使用自己的授权服务器(例如基于 Spring Security OAuth2 搭建的 SSO 系统)。

配置自定义客户端

spring.security.oauth2.client.registration.baeldung.client-id=SampleClientId
spring.security.oauth2.client.registration.baeldung.client-secret=secret

spring.security.oauth2.client.provider.baeldung.token-uri=http://localhost:8081/auth/oauth/token
spring.security.oauth2.client.provider.baeldung.authorization-uri=http://localhost:8081/auth/oauth/authorize
spring.security.oauth2.client.provider.baeldung.user-info-uri=http://localhost:8081/auth/user/me

这里我们注册了一个名为 baeldung 的客户端,指向本地运行的授权服务器。

自定义 PrincipalExtractor

假设用户信息接口返回的 JSON 中包含 name 字段,我们用它作为 Principal:

public class BaeldungPrincipalExtractor implements PrincipalExtractor {

    @Override
    public Object extractPrincipal(Map<String, Object> map) {
        return map.get("name");
    }
}

自定义 AuthoritiesExtractor

假设授权服务器在 /user/me 接口中返回了用户的权限列表(格式为 List<Map<String, String>>),我们可以将其转换为 GrantedAuthority

public class BaeldungAuthoritiesExtractor implements AuthoritiesExtractor {

    @Override
    public List<GrantedAuthority> extractAuthorities(Map<String, Object> map) {
        return AuthorityUtils.commaSeparatedStringToAuthorityList(asAuthorities(map));
    }

    private String asAuthorities(Map<String, Object> map) {
        List<String> authorities = new ArrayList<>();
        authorities.add("BAELDUNG_USER"); // 所有通过该服务器登录的用户都拥有此权限
        
        List<LinkedHashMap<String, String>> authz = 
            (List<LinkedHashMap<String, String>>) map.get("authorities");
        
        for (LinkedHashMap<String, String> entry : authz) {
            authorities.add(entry.get("authority"));
        }
        
        return String.join(",", authorities);
    }
}

注册自定义 Extractor Bean

@Configuration
public class SecurityConfig {

    // ...

    @Bean
    public PrincipalExtractor baeldungPrincipalExtractor() {
        return new BaeldungPrincipalExtractor();
    }

    @Bean
    public AuthoritiesExtractor baeldungAuthoritiesExtractor() {
        return new BaeldungAuthoritiesExtractor();
    }
}

✅ 提示:Spring 会根据当前使用的 OAuth2 客户端自动选择对应的 Extractor。多个 Extractor 可共存,按需生效。


5. 总结

本文演示了:

  • 如何通过 Spring Security OAuth2 实现 GitHub 第三方登录
  • 如何对接自定义授权服务器
  • 如何通过 PrincipalExtractorAuthoritiesExtractor 接口,完全掌控用户身份和权限的提取逻辑

✅ 这种方式在需要精细化权限控制、与内部系统集成时非常实用。

示例代码已上传至 GitHub:https://github.com/eugenp/tutorials/tree/master/spring-security-modules/spring-security-oauth2

本地运行时,访问 http://localhost:8082 即可测试。


原始标题:OAuth - Extracting Principal and Authorities using Spring Security OAuth