本文提供相关源码,请放心食用,详见网页侧边栏或底部,有疑问请评论或 Issue
关于 OAuth2.0 的理论基础参考阮一峰老师的《理解 OAuth 2.0》,其中关于授权码模式
就是本篇文章的重点。
本文着重于代码,关于理论不再赘述,关于不同公司的三方登录流程,只要遵循 OAuth2.0
规范,都大同小异。本文介绍 GitHub 和 QQ 两种,因为这两种无需审核,即可食用。
一、GitHub 登录
1.1 注册应用
进入 Github 的 Setting 页面,点击 Developer settings
,如图所示:

进入后点击 New Oauth App
,如图所示:

在其中填写主页 URL
和 回调 URL
,回调 URL 尤为重要,如果不太明白可以先和我一致。

点击注册后,上方会生成 Client ID
和 Client Secret
,这两个后面要用到。

1.2 HTML 页面
页面十分简单,只有两个跳转链接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>三方登录</title> </head> <body> <h1>三方登录Demo</h1> <div> <a href="/githubLogin">GitHub登录</a> <a href="/qqLogin">QQ登录</a> </div> </body> </html>
|
1.3 Github 登录方法
在这个方法中,我们需要访问 GitHub 的认证服务器,使用 Get 请求,这里使用重定向来实现。
遵循 Oauth 2.0 规范,需要携带以下参数:
-
response_type
: 对于授权码模式,该值固定为 code
-
client_id
: 注册应用时的 Client ID
-
state
:回调时会原样返回
-
redirect_uri
: 回调 URL,注册应用时填写的
这里的 state
参数我要额外说明下,因为该参数会在后面的回调 URL 中被原样携带回来,绝大多数的开发者会忽略该字段,阮一峰老师的文章也没有着重提及这一点。但是忽略该参数是会导致 CSRF
攻击的,在回调函数中应当对该字段进行校验!
关于如何校验,我一开始的想法是使用 session 来存储 state 进行校验的,但是我发现使用重定向后 session 不是同一个 session,方案一失败。
然后我想通过 ajax 请求,在页面中使用 window.location.href
方法跳转到认证服务器,使用 session 存储,但是很不幸这样也不是同一个 session,方案二失败。
最后我的解决办法是使用 redis 缓存,使用 set 存储,回调时判断是否存在。当然你也可以用 HashMap 来存储,这也是一个解决办法。
关于 Redis,可以参考《Redis 初探(1)——Redis 的安装》
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private static String GITHUB_CLIENT_ID = "0307dc634e4c5523cef2"; private static String GITHUB_CLIENT_SECRET = "707647176eb3bef1d4c2a50fcabf73e0401cc877"; private static String GITHUB_REDIRECT_URL = "http://127.0.0.1:8080/githubCallback";
@RequestMapping("/githubLogin") public void githubLogin(HttpServletResponse response) throws Exception { String url = "https://github.com/login/oauth/authorize"; String state = oauthService.genState(); String param = "response_type=code&" + "client_id=" + GITHUB_CLIENT_ID + "&state=" + state + "&redirect_uri=" + GITHUB_REDIRECT_URL; response.sendRedirect(url + "?" + param); }
|
1.4 Github 回调方法
在上一步中,浏览器会被跳转到 Github 的授权页,当用户登录并点击确认后,GitHub认证服务器会跳转到我们填写的回调URL中,我们在程序中处理回调。
在回调方法中,步骤如下:
-
首先验证 state
与发送时是否一致,如果不一致,可能遭遇了 CSRF
攻击。
-
得到 code
,向 GitHub 认证服务器申请令牌(token
)
这一步使用模拟的 POST 请求,携带参数包括:
-
grant_type
: 授权码模式固定为 authorization_code
-
code
:上一步中得到的 code
-
redirect_uri
: 回调URL
-
client_id
:注册应用时的Client ID
-
client_secret
:注册应用时的Client Secret
-
得到令牌(access_token
)和令牌类型(token_type
),向GitHub资源服务器获取资源(以 user_info 为例)
这一步使用模拟的 GET 请求,携带参数包括:
-
access_token
: 令牌
-
token_type
:令牌类型
-
输出结果
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
|
@RequestMapping("/githubCallback") public void githubCallback(String code, String state, HttpServletResponse response) throws Exception { if(!oauthService.checkState(state)) { throw new Exception("State验证失败"); }
String url = "https://github.com/login/oauth/access_token"; String param = "grant_type=authorization_code&code=" + code + "&redirect_uri=" + GITHUB_REDIRECT_URL + "&client_id=" + GITHUB_CLIENT_ID + "&client_secret=" + GITHUB_CLIENT_SECRET;
String result = HttpClientUtils.sendPostRequest(url, param);
Map<String, String> resultMap = HttpClientUtils.params2Map(result); if(resultMap.containsKey("error")) { throw new Exception(resultMap.get("error_description")); }
if(!resultMap.containsKey("access_token")) { throw new Exception("获取token失败"); }
String accessToken = resultMap.get("access_token"); String tokenType = resultMap.get("token_type");
String userUrl = "https://api.github.com/user"; String userParam = "access_token=" + accessToken + "&token_type=" + tokenType; String userResult = HttpClientUtils.sendGetRequest(userUrl, userParam);
response.setContentType("text/html;charset=utf-8"); response.getWriter().write(userResult); }
|
二、QQ 登录
2.1 注册应用
进入 QQ 互联管理中心,创建一个新应用(需要先审核个人身份):

然后注册应用信息,和 GitHub 的步骤大差不差:


注册后,可以看到应用的 APP ID、APP Key,以及你被允许的接口,当然只有一个获取用户信息。
官方开发文档点击这里:开发攻略
注意:审核状态为审核中和审核失败也是可以使用的,不用担心(只是无法实际上线而已,作为 Demo 足够了)。

2.2 QQ 登录方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private static String QQ_APP_ID = "101474821"; private static String QQ_APP_KEY = "00d91cc7f636d71faac8629d559f9fee"; private static String QQ_REDIRECT_URL = "http://127.0.0.1:8080/qqCallback";
@RequestMapping("/qqLogin") public void qqLogin(HttpServletResponse response) throws Exception { String url = "https://graph.qq.com/oauth2.0/authorize"; String state = oauthService.genState(); String param = "response_type=code&" + "client_id=" + QQ_APP_ID + "&state=" + state + "&redirect_uri=" + QQ_REDIRECT_URL;
response.sendRedirect(url + "?" + param); }
|
2.3 QQ 回调方法
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
|
@RequestMapping("/qqCallback") public void qqCallback(String code, String state, HttpServletResponse response) throws Exception { if(!oauthService.checkState(state)) { throw new Exception("State验证失败"); }
String url = "https://graph.qq.com/oauth2.0/token"; String param = "grant_type=authorization_code&code=" + code + "&redirect_uri=" + QQ_REDIRECT_URL + "&client_id=" + QQ_APP_ID + "&client_secret=" + QQ_APP_KEY;
String result = HttpClientUtils.sendPostRequest(url, param);
Map<String, String> resultMap = HttpClientUtils.params2Map(result); if(!resultMap.containsKey("access_token")) { throw new Exception("获取token失败"); } String accessToken = resultMap.get("access_token");
String meUrl = "https://graph.qq.com/oauth2.0/me"; String meParams = "access_token=" + accessToken; String meResult = HttpClientUtils.sendGetRequest(meUrl, meParams); String openid = getQQOpenid(meResult);
String userInfoUrl = "https://graph.qq.com/user/get_user_info"; String userInfoParam = "access_token=" + accessToken + "&oauth_consumer_key=" + QQ_APP_ID + "&openid=" + openid; String userInfo = HttpClientUtils.sendGetRequest(userInfoUrl, userInfoParam);
response.setContentType("text/html;charset=utf-8"); response.getWriter().write(userInfo); }
private String getQQOpenid(String str) { String json = str.substring(str.indexOf("{"), str.indexOf("}") + 1); Map<String, String> map = JsonUtils.jsonToPojo(json, Map.class); return map.get("openid"); }
|
三、项目源码
QQ 登录的具体流程我就不啰嗦了,都差不多。代码只列出了关键方法,具体程序还包含工具类和 redis 的配置。具体请参考文章开头源码,该项目采用 SpringBoot 搭建,需要 Redis 支持。
Web 三方登录实现(基于OAuth2.0,包含Github和QQ登录,附源码)