Github认证登录

宋正兵 on 2021-04-22

Github登录原理

本质上属于第三方登录,实质上就是 OAuth 授权。用户想要登录 A 网站,A 网站让用户提供第三方网站的数据,证明自己的身份。获取第三方网站的身份数据,就需要 OAuth 授权。

前期准备

GitHub 往下拉到底 API -> Developers -> APPS -> Buiding OAuth Apps,根据流程创建我们的 OAuth Apps,并且记得复制备份自己的 Client IDClient secret

或者直接访问网址进行填写登记。

image.png

提交表单以后,GitHub 应该会返回客户端 ID(client ID)和客户端密钥(client secret),这就是应用的身份识别码。

Web application flow

image.png

Web application flow

  1. 前端获取 code & state
  2. 拿着 code 传给后端,从后端获取 access_token
  3. access_token 获取用户信息。

1. 前端获取 code & state

根据 OAuth2.0 规范,第一步是根据 ClientId 获取到 code(后面需要这个 code + Client secrets 换取access_token)。

获取 code 的步骤很简单,只要拼接好一个 URL 访问,带用户操作完成后,Github 会重定向会第一步中设置的回调地址。

1
GET https://github.com/login/oauth/authorize

参数说明:

name type description
client_id string 必需 第一步中注册得到的 ClientID
redirect_uri string 第一步中设置的回调地址
loin string 推荐登录的 Github 账户,一般不填
scope string 这个参数指定了最后能获取到的信息,取值范围有 user 和 repo 等等,默认同时取 user 和 repo 的信息,详细取值范围见Github 文档
state string 你设定的一个随机值,用来防止 cross-sit 攻击
allow_signup string 这个参数指定是否允许用户在认证的时候注册 Github 账号,默认是 true

前端代码:

1
<a href="https://github.com/login/oauth/authorize?client_id=8f3c188a9891dd******&redirect_uri=http://localhost:8888/callback&scope=user&state=1">登录</a>

这里的回调地址是我后端代码的一个接口。

当提交完这个 http 请求之后,Github 会请求我们的回调地址,并且携带参数 code & state

1
http://localhost:8888/callback?code=c646acea9e8251f2****&state=1

2. 拿着 code 传给后端,从后端获取 access_token

第二步就是用 code + Client Secret 获取 access_token 了。这一步建议是一定要放到后端,因为如果在前端做的话,有可能会把 client secret 暴露的风险,别人一个 F12 什么都知道了。

获取 access_token 的接口同样是 http 协议:

1
POST https://github.com/login/oauth/access_token

参数说明:

Name Type Description
client_id string 必需 第一步中获取到的 ClientID
cleint_secret string 必需 第一步中获取到的 ClientSecret
code string 必需 第二步中前端获取到的 code
redirect_uri string 第一步中设置的回调地址
state string 第一步中设置的随机值

默认的响应为:

1
access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&token_type=bearer

后端代码,使用了 OKHttp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// accessTokenDTO 中包含了 Client ID、Client Secret、传回来的 code 和 state 以及回调地址
public String getAccessToken(AccessTokenDTO accessTokenDTO) {
MediaType mediaType = MediaType.get("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(mediaType, JSON.toJSONString(accessTokenDTO));
Request request = new Request.Builder().url("https://github.com/login/oauth/access_token").post(body).build();
try (Response response = client.newCall(request).execute()) {
String string = response.body().string();
// 响应的 string 内容为
// access_token=gho_lHe2f033RxDGztm92QIjj&scope=user&token_type=bearer
// 提取并返回token给调用者
String token = string.split("&")[0].split("=")[1];
return token;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

3. 用 access_token 获取用户信息

这一步也是在后端完成,API 如下:

1
2
Authorization: token OAUTH-TOKEN
GET https://api.github.com/user

需要携带参数 Authorization: token OAUTH-TOKEN,但是在最近的一次更新中官方推荐使用 access_token 安全访问 API 的方式,使用 Github 推荐的最新方式(Authorization HTTP header),旧方式(query parameter)即将被废弃。参考资料

于是后端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public GithubUser getUser(String accessToken) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://api.github.com/user")
.header("Authorization", "token " + accessToken)
.build();
try {
Response response = client.newCall(request).execute();
String string = response.body().string();
GithubUser githubUser = JSON.parseObject(string, GithubUser.class);
return githubUser;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

效果测试

我们在前端完成第 1 步,后端的回调地址中完成后 2 步,去获取 Github 账户的用户名。

完整代码:隐藏了部分我得 OAuth 信息。

前端:

1
<a href="https://github.com/login/oauth/authorize?client_id=[your_client_id]&redirect_uri=http://localhost:8888/callback&scope=user&state=1">登录</a>

后端:

DTO类:

1
2
3
4
5
6
7
public class AccessTokenDTO {
private String client_id;
private String client_secret;
private String code;
private String redirect_uri;
private String state;
}

Github 登录提供程序类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Component
public class GithubProvider {
// 携带code,从后端获取access_token
// accessTokenDTO中包含了Client ID、Client Secret、传回来的code和state以及回调地址
public String getAccessToken(AccessTokenDTO accessTokenDTO) {
MediaType mediaType = MediaType.get("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(mediaType, JSON.toJSONString(accessTokenDTO));
Request request = new Request.Builder().url("https://github.com/login/oauth/access_token").post(body).build();
try (Response response = client.newCall(request).execute()) {
String token = response.body().string().split("&")[0].split("=")[1];
return token;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 用access_token获取用户信息
public GithubUser getUser(String accessToken) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://api.github.com/user")
.header("Authorization", "token " + accessToken)
.build();
try {
Response response = client.newCall(request).execute();
String string = response.body().string();
GithubUser githubUser = JSON.parseObject(string, GithubUser.class);
return githubUser;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}

Controller:回调地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Controller
public class AuthorizeController {
@Autowired
GithubProvider githubProvider;

@GetMapping("/callback")
public String callback(@RequestParam(name = "code") String code,
@RequestParam(name = "state") String state) {
AccessTokenDTO accessTokenDTO = new AccessTokenDTO();
accessTokenDTO.setClient_id("[your_client_id]");
accessTokenDTO.setClient_secret("[your_client_secret]");
accessTokenDTO.setCode(code);
accessTokenDTO.setRedirect_uri("http://localhost:8888/callback");
accessTokenDTO.setState(state);
// 2.拿着code+client_secret获取access_token
String accessToken = githubProvider.getAccessToken(accessTokenDTO);
// 3.用access_token去获取用户信息
GithubUser user = githubProvider.getUser(accessToken);
// 测试,打印用户名
System.out.println(user.getName()); // output:zhengbing song
return "index";
}
}