OAuth2.0 第三方授权登录:从原理到代码实战(以 GitHub 为例)
在现代 Web 应用中,几乎所有网站都提供了“使用 GitHub / Google / 微信 登录”的选项。这就是第三方授权登录(也叫社交登录、社会化登录)。它能极大提升用户注册/登录体验,同时减轻开发者管理账号密码的负担和安全风险。
本文先从零讲解第三方授权登录的原理和标准流程,然后基于一个真实的 Spring Boot 项目代码,详细展示如何结合策略模式优雅地实现支持多平台的 OAuth2.0 第三方登录。
一、什么是第三方授权登录?标准流程是什么?
第三方授权登录的核心是:用户不用在你的网站注册新账号,而是直接用已有的第三方平台(如 GitHub、Google、微信)账号进行授权登录。
它基于 OAuth 2.0 协议,最常用的是授权码模式(Authorization Code Grant),流程如下:
- 用户在你的网站点击“GitHub 登录”按钮。
- 你的网站重定向用户到 GitHub 的授权页面:
- URL 中携带你的应用 ID(client_id)、回调地址(redirect_uri)、state(防 CSRF 参数)等。
- 用户在 GitHub 上确认授权(“允许此应用访问你的公开信息”)。
- GitHub 重定向回你的回调地址,同时携带一个临时的授权码(code)。
- 你的后端收到 code 后,向 GitHub 的 token 接口发送请求(携带 code、client_secret 等),换取access_token。
- 用 access_token 调用 GitHub 的用户接口,获取用户的基本信息(如用户名、头像、邮箱等)。
- 根据获取到的第三方用户信息,在你的系统里:
- 如果已有绑定记录 → 直接登录
- 如果没有 → 自动创建一个新本地账号(或引导用户绑定已有账号)
- 最终给前端返回你的系统登录态(如 JWT Token),后续请求正常携带即可。
整个过程用户无需输入密码,安全且便捷。
总的来说有三个过程:
- 用户授权登录返回授权码
- 第三方拿到授权码再去取 Access Token 访问令牌
- 获得访问令牌后去资源服务器访问资源
二、代码实战:使用策略模式实现可扩展的第三方登录
下面基于一个完整的 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 类(策略模式)。
- 代码清晰解耦:业务层无需关心各平台接口差异。
生产环境建议改进点:
- state 参数必须存 Redis 并在回调时校验,防止 CSRF。
- 更稳健的唯一标识:建议新增第三方 ID 字段(如 github_id),避免用户名冲突。
- 支持已有账号绑定第三方。
- 完善异常处理和日志。