OAuth2.0第三方授权登录

83 阅读8分钟

OAuth2.0 第三方授权登录:从原理到代码实战(以 GitHub 为例)

在现代 Web 应用中,几乎所有网站都提供了“使用 GitHub / Google / 微信 登录”的选项。这就是第三方授权登录(也叫社交登录、社会化登录)。它能极大提升用户注册/登录体验,同时减轻开发者管理账号密码的负担和安全风险。

本文先从零讲解第三方授权登录的原理和标准流程,然后基于一个真实的 Spring Boot 项目代码,详细展示如何结合策略模式优雅地实现支持多平台的 OAuth2.0 第三方登录。

一、什么是第三方授权登录?标准流程是什么?

第三方授权登录的核心是:用户不用在你的网站注册新账号,而是直接用已有的第三方平台(如 GitHub、Google、微信)账号进行授权登录

它基于 OAuth 2.0 协议,最常用的是授权码模式(Authorization Code Grant),流程如下:

  1. 用户在你的网站点击“GitHub 登录”按钮。
  2. 你的网站重定向用户到 GitHub 的授权页面:
    • URL 中携带你的应用 ID(client_id)、回调地址(redirect_uri)、state(防 CSRF 参数)等。
  3. 用户在 GitHub 上确认授权(“允许此应用访问你的公开信息”)。
  4. GitHub 重定向回你的回调地址,同时携带一个临时的授权码(code)
  5. 你的后端收到 code 后,向 GitHub 的 token 接口发送请求(携带 code、client_secret 等),换取access_token
  6. 用 access_token 调用 GitHub 的用户接口,获取用户的基本信息(如用户名、头像、邮箱等)。
  7. 根据获取到的第三方用户信息,在你的系统里:
    • 如果已有绑定记录 → 直接登录
    • 如果没有 → 自动创建一个新本地账号(或引导用户绑定已有账号)
  8. 最终给前端返回你的系统登录态(如 JWT Token),后续请求正常携带即可。

整个过程用户无需输入密码,安全且便捷。

总的来说有三个过程:

  1. 用户授权登录返回授权码
  2. 第三方拿到授权码再去取 Access Token 访问令牌
  3. 获得访问令牌后去资源服务器访问资源

二、代码实战:使用策略模式实现可扩展的第三方登录

下面基于一个完整的 Spring Boot + Spring Security 项目代码,展示如何实现上述流程。

项目核心优势
  • 使用 Spring Security OAuth2 Client 自动管理各平台的配置。
  • 结合 策略模式(Strategy Pattern) 实现不同平台的差异化处理(GitHub、Google、微信等接口都不完全一样)。
  • 支持轻松扩展:新增一个平台只需写一个 Strategy 类,无需修改现有代码。
主要组件一览
  • AuthController:对外 REST 接口(获取授权 URL、处理回调)。
  • OAuth2ServiceImpl:核心业务服务,协调整个流程。
  • OAuth2ProviderStrategy:策略接口,定义三个通用行为。
  • OAuth2StrategyFactory:策略工厂,根据平台动态获取具体实现。
  • GithubStrategy:GitHub 的具体实现(可继续添加 GoogleStrategy、WechatStrategy 等)。
1. 前端获取授权 URL

前端点击“GitHub 登录”时,先调用后端接口获取 GitHub 的授权页面 URL:

@GetMapping("/oauth/url")
public Result getOAuthUrl(@RequestParam String provider) {
    String authorizationUrl = oAuth2Service.getAuthorizationUrl(provider);
    return Result.success(authorizationUrl);
}

核心逻辑:

public String getAuthorizationUrl(String provider) {
    ClientRegistration reg = clientRegistrationRepository.findByRegistrationId(provider);
    String state = UUID.randomUUID().toString();  // 实际项目应存 Redis 防 CSRF

    OAuth2ProviderStrategy strategy = oAuth2StrategyFactory.getStrategy(provider);
    return strategy.getAuthorizationUrl(reg, state);
}

GitHub 具体构建 URL:

return UriComponentsBuilder.fromUriString("https://github.com/login/oauth/authorize")
        .queryParam("client_id", reg.getClientId())
        .queryParam("redirect_uri", reg.getRedirectUri())
        .queryParam("state", state)
        .build().toUriString();
2. 处理授权回调(最核心的部分)

GitHub 授权后会回调:/auth/oauth/callback/github?code=xxx&state=xxx

@GetMapping("/oauth/callback/{provider}")
public Result oauthCallback(@PathVariable String provider, @RequestParam String code) {
    String jwt = oAuth2Service.handleOAuthCallback(provider, code);
    return Result.success(jwt);
}

业务层完整流程:

@Transactional
public String handleOAuthCallback(String provider, String code) {
    OAuth2ProviderStrategy strategy = oAuth2StrategyFactory.getStrategy(provider);
    ClientRegistration reg = clientRegistrationRepository.findByRegistrationId(provider);

    // 步骤1:用 code 换 access_token
    String accessToken = strategy.getAccessToken(code, reg);

    // 步骤2:用 access_token 获取第三方用户信息
    User userFromProvider = strategy.getUserInfo(accessToken);

    // 步骤3:查找本地用户
    User existUser = userMapper.findByUsername(userFromProvider.getUsername());

    if (existUser == null) {
        // 新用户:插入数据库 + 分配默认角色
        userMapper.insert(userFromProvider);
        userRoleMapper.insert(new UserRole(userFromProvider.getId(), defaultRoleId));
        return jwtUtil.generateToken(userFromProvider.getUsername(), userFromProvider.getId());
    } else {
        // 老用户:直接登录
        return jwtUtil.generateToken(existUser.getUsername(), existUser.getId());
    }
}
3. GitHub 策略具体实现细节

获取 access_token(GitHub 特殊:直接在查询参数返回)

String url = tokenUri + "?client_id=...&client_secret=...&code=" + code;
String response = restTemplate.getForObject(url, String.class);
// response: access_token=gho_xxx&token_type=bearer...
return response.split("&")[0].split("=")[1];

获取用户信息

HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
ResponseEntity<Map> resp = restTemplate.exchange(
    "https://api.github.com/user", HttpMethod.GET, new HttpEntity<>(headers), Map.class);

Map<String, Object> userInfo = resp.getBody();
String login = (String) userInfo.get("login");  // GitHub 用户名
// 实际邮箱需额外调用 /user/emails 接口

构造本地用户

User user = new User();
user.setUsername("github-" + login + "-" + uniqueId);  // 保证唯一
user.setPassword(passwordEncoder.encode("random"));   // 第三方登录不会用到密码
user.setEnabled(true);
4. 策略模式的核心实现

接口:

public interface OAuth2ProviderStrategy {
    String getAuthorizationUrl(ClientRegistration reg, String state);
    String getAccessToken(String code, ClientRegistration reg);
    User getUserInfo(String accessToken);
}

工厂自动注入所有实现:

@Component
public class OAuth2StrategyFactory {
    @Autowired
    private Map<String, OAuth2ProviderStrategy> strategyMap;  // key 为 Bean 名

    public OAuth2ProviderStrategy getStrategy(String provider) {
        return strategyMap.get(provider);
    }
}

GitHub 实现类:

@Component("github")  // 注意:Bean 名称必须与 provider 一致
public class GithubStrategy implements OAuth2ProviderStrategy { ... }
5. 配置文件示例(application.yml)
spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: your-client-id
            client-secret: your-client-secret
            redirect-uri: "{baseUrl}/auth/oauth/callback/{registrationId}"
            scope: read:user,user:email
        provider:
          github:
            authorization-uri: https://github.com/login/oauth/authorize
            token-uri: https://github.com/login/oauth/access_token
完整代码

AuthController

@RestController
@RequestMapping("/auth")
@Tag(name = "认证接口", description = "用户登录、注册和第三方授权登录相关接口")
public class AuthController {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private UserService userService;
    @Autowired
    private OAuth2ServiceImpl oAuth2Service;


    /*
     * 第三方授权登录--前端获取授权URL
     */
    @GetMapping("/oauth/url")
    @Operation(summary = "获取第三方授权URL", description = "获取指定提供商的OAuth2授权URL")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功获取授权URL"),
            @ApiResponse(responseCode = "400", description = "无效的提供商")
    })
    public Result getOAuthUrl(@RequestParam @Parameter(description = "提供商名称,如github") String provider){
        // 根据传递的参数,选择对应的第三方授权URL
        String authorizationUrl = oAuth2Service.getAuthorizationUrl(provider);
        return Result.success(authorizationUrl);
    }

    /*
     * 第三方授权登录--处理授权回调
     */
    @GetMapping("/oauth/callback/{provider}")
    @Operation(summary = "处理OAuth2回调", description = "处理来自第三方提供商的OAuth2回调并生成JWT令牌")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "认证成功,返回JWT令牌"),
            @ApiResponse(responseCode = "400", description = "认证失败")
    })
    public Result oauthCallback(@PathVariable @Parameter(description = "提供商名称") String provider, 
                               @RequestParam @Parameter(description = "授权码") String code){
        // 处理回调
        System.out.println("收到OAuth回调: provider=" + provider + ", code=" + code);
        String jwt =oAuth2Service.handleOAuthCallback(provider, code);
        return Result.success(jwt);
    }

}

OAuth2ServiceImpl

@Service
@RequiredArgsConstructor
public class OAuth2ServiceImpl {

    private final OAuth2StrategyFactory oAuth2StrategyFactory;
    // 获取配置文件中所有支持的第三方登录
    private final ClientRegistrationRepository clientRegistrationRepository;
    private final UserService userService;
    private final JwtUtil jwtUtil;
    private final UserRoleMapper userRoleMapper;
    private final UserMapper userMapper;

    @Value("${app.default-role-id}")
    private Long defaultRoleId;

    /*
     * 获取授权URL
     */
    public String getAuthorizationUrl(String provider) {
        // 获取配置文件的第三方登录配置信息
        ClientRegistration byRegistrationId = clientRegistrationRepository.findByRegistrationId(provider);
        if (byRegistrationId == null){
            throw new UserException(
                    ErrorCodeEnum.USER_AUTH_NOT_PROVIDER.getCode(),
                    ErrorCodeEnum.USER_AUTH_NOT_PROVIDER.getMessage()
            );
        }
        String state = UUID.randomUUID().toString();
        // TODO: 存 state 到 Redis,5分钟过期。为什么?
        // 获取策略
        OAuth2ProviderStrategy strategy = oAuth2StrategyFactory.getStrategy(provider);
        // 获取授权URL
        return strategy.getAuthorizationUrl(byRegistrationId,state);
    }

    /*
     * 处理授权回调
     */
    @Transactional
    public String handleOAuthCallback(String provider, String code) {
        // 根据授权码获取访问令牌
        OAuth2ProviderStrategy strategy = oAuth2StrategyFactory.getStrategy(provider);
        ClientRegistration byRegistrationId = clientRegistrationRepository.findByRegistrationId(provider);
        String accessToken = strategy.getAccessToken(code,byRegistrationId);
        // 根据访问令牌获取用户信息
        User user = strategy.getUserInfo(accessToken);
        // 查询用户是否存在
        User userexits = userMapper.findByUsername(user.getUsername());
        if (userexits == null){
            // 用户不存在
            // 封装用户信息并存储进数据库
            userMapper.insert(user);
            System.out.println("用户的主键ID为:" + user.getId());
            // 关联用户角色为默认的 USER
            // 使用配置文件中的默认角色ID
            userRoleMapper.insert(new UserRole(user.getId(), defaultRoleId));
            // 返回JWT令牌
            return jwtUtil.generateToken(user.getUsername(), user.getId());
        }else {
            // 用户存在,直接返回JWT令牌
            return jwtUtil.generateToken(userexits.getUsername(), userexits.getId());
        }
    }
}

OAuth2ProviderStrategy

public interface OAuth2ProviderStrategy {

    String getAuthorizationUrl(ClientRegistration clientRegistration, String state);

    String getAccessToken(String code, ClientRegistration clientRegistrationRepository);

    User getUserInfo(String accessToken);
}

OAuth2StrategyFactory

@Component
public class OAuth2StrategyFactory {

    // 存储所有的策略(key-valu,提供方与具体的实现)
    // 延迟初始化,Spring自动注入所有OAuth2Strategy实现
    @Autowired
    private Map<String, OAuth2ProviderStrategy> strategyMap;

    // 获取策略--根据提供方名称,获取对应的认证登录实现
    public OAuth2ProviderStrategy getStrategy(String provider) {
        return strategyMap.get(provider);
    }

    // 获取所有支持的第三方登录
    public Set<String> getAllSupportedProviders() {
        return strategyMap.keySet();
    }
}

GithubStrategy

@Component("github")
public class GithubStrategy implements OAuth2ProviderStrategy{

    // Github获取用户信息的接口
    private final static String USER_INFO_URL = "https://api.github.com/user";

    @Autowired
    private PasswordEncoder passwordEncoder;

    // 构建授权URL
    @Override
    public String getAuthorizationUrl(ClientRegistration reg, String state) {
        return UriComponentsBuilder.fromUriString(reg.getProviderDetails().getAuthorizationUri())
                .queryParam("response_type", "code")
                .queryParam("client_id", reg.getClientId())
                .queryParam("redirect_uri", reg.getRedirectUri())
                .queryParam("state", state)
                .build().toUriString();
    }

    // 获取访问令牌
    @Override
    public String getAccessToken(String code, ClientRegistration reg) {
        // GitHub 回调返回参数:?access_token=xxx&token_type=bearer

        // 封装请求URL
        String url = reg.getProviderDetails().getTokenUri() +
                "?client_id=" + reg.getClientId() +
                "&client_secret=" + reg.getClientSecret() +
                "&code=" + code +
                "&redirect_uri=" + reg.getRedirectUri();

        // 发起请求
        /*
        RestTemplate rest = new RestTemplate(); 创建了一个用于发送HTTP请求的客户端实例
        String response = rest.getForObject(url, String.class); 发送GET请求到指定的url,并将响应体转换为String类型返回
         */
        RestTemplate rest = new RestTemplate();
        String response = rest.getForObject(url, String.class);
        if (response == null){
            // TODO: 创建自定义的异常
            throw new RuntimeException("获取访问令牌失败");
        }
        return response.split("&")[0].split("=")[1];
    }

    // 获取用户信息
    @Override
    public User getUserInfo(String accessToken) {
        // 获取用户名
        String userName = getUserName(accessToken);
        // 获取用户邮箱
        String userEmail = getUserEmail(accessToken);
        // 封装对象
        User user = new User();
        user.setUsername("github-" + userName +"-"+ userEmail);
        user.setEnabled(true);
        user.setPassword(passwordEncoder.encode("123456"));
        // 返回
        return user;
    }

    // 获取用户名称
    private String getUserName(String accessToken){
        // 发起请求
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();        // 设置请求头
        headers.setBearerAuth(accessToken);             // 将访问令牌添加进Authorization
        HttpEntity<String> entity = new HttpEntity<>(headers);

        // 发送GET请求获取用户信息
        ResponseEntity<Map> response = restTemplate.exchange(USER_INFO_URL, HttpMethod.GET, entity, Map.class);

        // 从响应中提取用户名
        Map<String, Object> userInfo = response.getBody();
        if (userInfo != null && userInfo.containsKey("login")) {
            // 返回结果
            return (String) userInfo.get("login");
        }else {
            // TODO: 创建自定义的异常
            throw new RuntimeException("获取用户信息失败");
        }
    }

    // 获取用户邮箱
    private String getUserEmail(String accessToken) {
        String url = "https://api.github.com/user";
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        HttpEntity<String> entity = new HttpEntity<>(headers);

        // 使用 Map 类型接收响应
        ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
                url,
                HttpMethod.GET,
                entity,
                new ParameterizedTypeReference<Map<String, Object>>() {}
        );

        Map<String, Object> userInfo = response.getBody();
        if (userInfo != null && userInfo.containsKey("id")) {
            return userInfo.get("id").toString();
        }else {
            // TODO: 创建自定义的异常
            throw new RuntimeException("获取用户信息失败");
        }
    }

}

三、总结与建议

通过以上设计,我们实现了:

  • 完整的 OAuth2.0 授权码模式第三方登录。
  • 高可扩展性:新增微信、Google 等只需新增一个 Strategy 类(策略模式)。
  • 代码清晰解耦:业务层无需关心各平台接口差异。

生产环境建议改进点:

  1. state 参数必须存 Redis 并在回调时校验,防止 CSRF。
  2. 更稳健的唯一标识:建议新增第三方 ID 字段(如 github_id),避免用户名冲突。
  3. 支持已有账号绑定第三方。
  4. 完善异常处理和日志。