GitHub 和 Gitee 第三方登录实现

现在很多站点都支持第三方登录。本文记录基于 OAuth2 授权码模式接入 GitHub 和 Gitee 登录的完整过程,以 Java + Spring Boot 为例。前提:先有一个 GitHub / Gitee 账号。

创建 OAuth 应用

GitHub

  1. 登录 GitHub,点击 Settings:

  1. 点击 Developer settings(开发者设置):

  1. 新建一个 OAuth App(授权应用):

  1. 填写应用信息(回调地址填自己服务的回调接口):

获取 Client ID 和 Client Secret

创建完成后即可看到:

提示:Client Secret 只在创建时展示一次,注意保存;不要提交到代码仓库或写在博客里(本文中一律用占位符代替)。

Gitee 的流程类似:设置 → 第三方应用 → 创建应用,同样会得到 Client ID 和 Client Secret。

授权流程

整个 OAuth2 授权码流程分三步:

  1. 跳转授权页:引导用户重定向到 GitHub 的授权页面:

    https://github.com/login/oauth/authorize?client_id=xxxxxxxxxxxxxxxxxx&scope=user,public_repo

  2. 回调换 token:用户同意授权后,GitHub 跳转到回调地址并携带 code 参数:

    http://localhost:8080/login?code=xxxxxxxxxxxxxxxxxxxx

    后端拿 client_id、client_secret 和 code 换取 access_token:

    https://github.com/login/oauth/access_token?client_id=xxxxxxxxxxxxxxxxxxx&client_secret=xxxxxxxxxxxxxxxxx&code=xxxxxxxxxxxxxxxxxxx

  3. 获取用户信息:用 access_token 请求用户信息接口:

    https://api.github.com/user?access_token=xxxxxxxxxxxxxxxxx

完整代码

登录页与跳转

先写一个简单的登录页面:

 1<!DOCTYPE html>
 2<html lang="en" xmlns:th="http://www.thymeleaf.org">
 3<head>
 4    <meta charset="UTF-8">
 5    <title>登录</title>
 6</head>
 7<body>
 8<form method="get" action="/oauth/login">
 9    <input type="submit" th:name="type" th:value="github">
10    <input type="submit" th:name="type" th:value="gitee"/>
11</form>
12</body>
13</html>

controller 页面跳转:

1@GetMapping(value = "/login")
2public String login() {
3    return "login";
4}

OAuth 接口抽象

把三个平台的差异(授权地址、token 地址、用户信息地址)抽象成接口:

 1public interface BaseOauth {
 2
 3    /**
 4     * 授权地址
 5     *
 6     * @return 授权地址
 7     */
 8    String authorize();
 9
10    /**
11     * 获取 accessToken
12     *
13     * @param code 请求编码
14     * @return accessToken
15     */
16    String accessToken(String code);
17
18    /**
19     * 获取用户信息
20     *
21     * @param accessToken token
22     * @return user
23     */
24    String userInfo(String accessToken);
25}

Gitee 实现:

 1@Component
 2public class GiteeOauth implements BaseOauth {
 3
 4    private static final String GITEE_CLIENT_ID = "[你的 Gitee Client ID]";
 5    private static final String GITEE_CLIENT_SECRET = "[你的 Gitee Client Secret]";
 6
 7    private static final String REDIRECT_URI = "http://127.0.0.1:8012/gitee/callback";
 8
 9    @Override
10    public String authorize() {
11        return "https://gitee.com/oauth/authorize?client_id=" + GITEE_CLIENT_ID + "&response_type=code&redirect_uri=" + REDIRECT_URI;
12    }
13
14    @Override
15    public String accessToken(String code) {
16        return "https://gitee.com/oauth/token?grant_type=authorization_code&code=" + code + "&client_id=" + GITEE_CLIENT_ID + "&redirect_uri=" + REDIRECT_URI + "&client_secret=" + GITEE_CLIENT_SECRET;
17    }
18
19    @Override
20    public String userInfo(String accessToken) {
21        return "https://gitee.com/api/v5/user?access_token=" + accessToken;
22    }
23}

GitHub 实现:

 1@Component
 2public class GithubOauth implements BaseOauth {
 3
 4    private static final String GITHUB_CLIENT_ID = "[你的 GitHub Client ID]";
 5    private static final String GITHUB_CLIENT_SECRET = "[你的 GitHub Client Secret]";
 6
 7    /**
 8     * 登陆授权类型
 9     */
10    @Override
11    public String authorize() {
12        return "https://github.com/login/oauth/authorize?client_id=" + GITHUB_CLIENT_ID + "&scope=user,public_repo";
13    }
14
15    @Override
16    public String accessToken(String code) {
17        return "https://github.com/login/oauth/access_token?client_id=" + GITHUB_CLIENT_ID + "&client_secret=" + GITHUB_CLIENT_SECRET + "&code=" + code;
18    }
19
20    @Override
21    public String userInfo(String accessToken) {
22        return "https://api.github.com/user?access_token=" + accessToken + "&scope=public_repo%2Cuser&token_type=bearer";
23    }
24}

Service 层

1public interface BaseOauthService {
2    String authorizeUri();
3
4    String getAccessToken(String code);
5
6    JSONObject getUserInfo(String accessToken);
7}
 1@Service
 2public class GiteeServiceImpl implements BaseOauthService {
 3
 4    @Autowired
 5    GiteeOauth giteeOauth;
 6
 7    @Autowired
 8    RestTemplate restTemplate;
 9
10    @Override
11    public String authorizeUri() {
12        return giteeOauth.authorize();
13    }
14
15    @Override
16    public String getAccessToken(String code) {
17        String token = giteeOauth.accessToken(code);
18        ResponseEntity<Object> entity = restTemplate.postForEntity(token, httpEntity(), Object.class);
19        Object body = entity.getBody();
20        assert body != null;
21        String string = body.toString();
22        String[] split = string.split("=");
23        String accessToken = split[1].toString().split(",")[0];
24        return accessToken;
25    }
26
27    @Override
28    public JSONObject getUserInfo(String accessToken) {
29        String userInfo = giteeOauth.userInfo(accessToken);
30        ResponseEntity<JSONObject> forEntity = restTemplate.exchange(userInfo, HttpMethod.GET, httpEntity(), JSONObject.class);
31        return forEntity.getBody();
32    }
33
34    public static HttpEntity httpEntity() {
35        HttpHeaders headers = new HttpHeaders();
36        headers.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36");
37
38        HttpEntity<HttpHeaders> request = new HttpEntity<>(headers);
39        return request;
40    }
41}
 1@Service
 2public class GithubServiceImpl implements BaseOauthService {
 3    @Autowired
 4    GithubOauth githubOauth;
 5    @Autowired
 6    private RestTemplate restTemplate;
 7
 8    @Override
 9    public String authorizeUri() {
10        return githubOauth.authorize();
11    }
12
13    @Override
14    public String getAccessToken(String code) {
15        String token = githubOauth.accessToken(code);
16        ResponseEntity<Object> forEntity = restTemplate.exchange(token, HttpMethod.GET, httpEntity(), Object.class);
17        String[] split = Objects.requireNonNull(forEntity.getBody()).toString().split("=");
18        String accessToken = split[1].split(",")[0];
19        return accessToken;
20    }
21
22    @Override
23    public JSONObject getUserInfo(String accessToken) {
24        String userInfo = githubOauth.userInfo(accessToken);
25        ResponseEntity<JSONObject> entity = restTemplate.exchange(userInfo, HttpMethod.GET, httpEntity(), JSONObject.class);
26        return entity.getBody();
27    }
28}

GitHub 接口要求请求带 User-Agent,所以统一封装了 httpEntity()。

Controller

 1@Controller
 2public class IndexController {
 3
 4    @Autowired
 5    private GiteeServiceImpl giteeService;
 6
 7    @Autowired
 8    GithubServiceImpl githubService;
 9
10    @GetMapping(value = "/")
11    public String index() {
12        return "index";
13    }
14
15    @GetMapping(value = "/login")
16    public String login() {
17        return "login";
18    }
19
20    @GetMapping(value = "/oauth/login")
21    public String login(@RequestParam("type") String type) {
22        if (type.toUpperCase().equals(LoginType.GITHUB.toString())) {
23            return "redirect:" + githubService.authorizeUri();
24        }
25        return "redirect:" + giteeService.authorizeUri();
26    }
27
28    @GetMapping(value = "/github/callback")
29    public String callback(HttpServletRequest request, Model model) {
30        String code = request.getParameter("code");
31        JSONObject userInfo = githubService.getUserInfo(githubService.getAccessToken(code));
32        UserInfo info = JSON.parseObject(userInfo.toString(), UserInfo.class);
33        model.addAttribute("github", info);
34        return "index";
35    }
36
37    @GetMapping(value = "/gitee/callback")
38    public String giteeCallBack(HttpServletRequest request, Model model) {
39        String code = request.getParameter("code");
40        JSONObject userInfo = giteeService.getUserInfo(giteeService.getAccessToken(code));
41        UserInfo info = JSON.parseObject(userInfo.toString(), UserInfo.class);
42        model.addAttribute("gitee", info);
43        return "index";
44    }
45}

用户信息实体

1@Data
2public class UserInfo {
3    private Integer id;
4    private String avatarUrl;
5    private String login;
6    private String bio;
7    private String createdAt;
8    private String htmlUrl;
9}
1public enum LoginType {
2    /**
3     * 登录类型
4     */
5    GITEE,
6    GITHUB;
7}

显示用户信息

 1<!DOCTYPE html>
 2<html lang="en" xmlns:th="http://www.thymeleaf.org">
 3<head>
 4    <meta charset="UTF-8">
 5    <title>首页</title>
 6    <style>
 7        .container {
 8            width: 500px;
 9            margin: 0 auto;
10        }
11    </style>
12</head>
13<body>
14<div class="container">
15    <h1>Gitee</h1>
16    <p th:text="${gitee}"></p>
17
18    <h1>GitHub</h1>
19    <p th:text="${github}"></p>
20</div>
21</body>
22</html>

参考文档