授权登录流程概述:

根据官方文档进行操作:

首先根据官方文档创建一个 OAuth App,需要注意的是 Authorization callback URL 是回调地址,我们这里填入 http://localhost:8080/callback

创建完成后会给你一个 Client ID 和 Client Secret,拿到这两个值就可以进行 GitHub 的第三方登录请求了。


点击登录按钮,浏览器跳转 GitHub 用户授权页面:

这是一个 get 请求,请求的地址如下。请求需要传入的参数有 client_id(创建 OAuth App 后给的 Client ID),redirect_uri(重定向的回调地址,即Authorization callback URL 中填的值),scope (用于获取访问权限,如果要获取 user 信息则 scope=user,如果 user 和 email 信息都要则 scope=user:email),state(随意传一个字符串即可,用于防止跨站点请求伪造攻击)。

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

我们传入必要的参数,随意构造一下这个网址并在浏览器里请求一下:

https://github.com/login/oauth/authorize?client_id=0123456789&redirect_uri=http://localhost:8080/callback&scope=user&state=1

当用户同意授权后,链接地址就会转跳到我们配置页面内的 Authorization callback URL 所填写的URL地址,并且会带上一个 code参数,这个参数就是一个授权码。

http://localhost:8080/callback?code=abcd0123456789&state=1

传入授权码获取访问令牌:

这是一个 post 请求,请求的地址如下。请求需要传入的参数有 client_id(创建 OAuth App 后给的 Client ID),client_secret(创建 OAuth App 后给的 Client Secre),code(授权码),redirect_uri(重定向的回调地址,即Authorization callback URL 中填的值),state(在第一步时传入的随机字符串)。

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

我们传入必要的参数,构造出网址如下,并在浏览器里请求一下:

https://github.com/login/oauth/access_token?client_id=0123456789&client_secret=0123456789&code=abcd0123456789&redirect_uri=http://localhost:8080/callback&state=1

会返回给我们一个字符串:

access_token=abcdefg0123456789&scope=user&token_type=bearer

这里面的 access_token 就是我们需要的访问令牌。


使用访问令牌访问API:

拥有访问令牌即表示允许你代表用户向API发出请求。

这是一个 get 请求,请求的地址如下.请求需要传入的参数即访问令牌 access_token 。

https://api.github.com/user

我们传入访问令牌 access_token,构造出网址如下,并在浏览器里请求一下:

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

返回请求人的信息,如下图所示:


使用 springboot 进行实现:

点击登录按钮,浏览器跳转 GitHub 获取授权码:
<li><a href="https://github.com/login/oauth/authorize?client_id=0123456789&redirect_uri=http://localhost:8080/callback&scope=user&state=1">登录</a></li>

这就获取到 code 即授权码了。


传入授权码获取访问令牌:

用 OkHttp 进行 post 请求:

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 = string.split("&")[0].split("=")[1];
        System.out.println(string);
        System.out.println(access_token);
        return access_token;
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

AccessTokenDTO 里封装的是访问令牌所必须的参数,将其封装成一个对象,方便传参和取值:

public class AccessTokenDTO {
    private String client_id;

    private String client_secret;

    private String code;

    private String redirect_uri;

    private String state;
    
    //省略 get 和 set 方法
}

之前第一步点击登录按钮,浏览器跳转 GitHub 获取授权码,那么怎么将授权码取出放到 AccessTokenDTO 对象中去呢?

前面登录流程已经说了,当用户同意授权后,链接地址就会转跳到我们配置页面内的 Authorization callback URL 所填写的URL地址,并且会带上一个 code参数,这个参数就是一个授权码。

http://localhost:8080/callback?code=abcd0123456789&state=1

那么我们只要在 controller 层构造一个回调函数就能拿到 code 和 state 的值了。

@RequestMapping("/callback")
public String callBack(String code,String state){
    AccessTokenDTO accessTokenDTO = new AccessTokenDTO();
    accessTokenDTO.setClient_id("0123456789");
    accessTokenDTO.setClient_secret("0123456789");
    accessTokenDTO.setCode(code);
    accessTokenDTO.setRedirect_uri("http://localhost:8080/callback");
    accessTokenDTO.setState(state);
    return "index";
}

到此为止访问令牌所需要的数据都以及传入 AccessTokenDTO 对象里了,现在只需要调用 getAccessToken() 方法就能得到 access_token(访问令牌)的值了。

@RequestMapping("/callback")
public String callBack(String code,String state){
    AccessTokenDTO accessTokenDTO = new AccessTokenDTO();
    accessTokenDTO.setClient_id("0123456789");
    accessTokenDTO.setClient_secret("0123456789");
    accessTokenDTO.setCode(code);
    accessTokenDTO.setRedirect_uri("http://localhost:8080/callback");
    accessTokenDTO.setState(state);
    String accessToken = githubProvider.getAccessToken(accessTokenDTO);
    System.out.println(accessToken);
    return "index";
}

使用访问令牌访问API:

用 OkHttp 进行 get 请求:

public GithubUser getGithubUser(String accessToken){
    OkHttpClient client = new OkHttpClient();

    Request request = new Request.Builder()
        .url("https://api.github.com/user?access_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;
}

调用 getGithubUser() 方法获取用户信息:

@RequestMapping("/callback")
public String callBack(String code,String state){
    AccessTokenDTO accessTokenDTO = new AccessTokenDTO();
    accessTokenDTO.setClient_id("0123456789");
    accessTokenDTO.setClient_secret("0123456789");
    accessTokenDTO.setCode(code);
    accessTokenDTO.setRedirect_uri("http://localhost:8080/callback");
    accessTokenDTO.setState(state);
    String accessToken = githubProvider.getAccessToken(accessTokenDTO);
    GithubUser githubUser = githubProvider.getGithubUser(accessToken);
    System.out.println(githubUser.getName()+"---"+githubUser.getId()+"---"+githubUser.getBio());
    return "index";
}

这儿我们只取了 id ,name,bio。其他数据可根据返回的 json 信息获取。


Github 第三方整合实现:

在我研究第三方登录没几天过后,我就在 Github 上找到了一个整合了几乎所有常用第三方授权登录的项目,GitHub地址:

JustAuth,如你所见,它仅仅是一个第三方授权登录工具类库,它可以让我们脱离繁琐的第三方登录SDK,让登录变得So easy!


作者的代码写得很工整漂亮,我的 demo 和他的相比简直就是小学作文之于大学论文,下面让我们来分析一下他的源码实现,依旧以 GitHub 授权登录为例:


首先找到 authorization 包下的 GithubAuthorization.java:
public class GithubAuthorization implements Authorization {

    @Override
    public String getAuthorizeUrl(AuthConfig config) {
        return UrlBuilder.getGithubAuthorizeUrl(config.getClientId(), config.getRedirectUri());
    }
}

只传入了 client_id 和 回调地址 redirect_uri,由此可以想到这应该是第一步 获取授权码


点进 UrlBuilder 的 getGithubAuthorizeUrl 方法:
public static String getGithubAuthorizeUrl(String clientId, String redirectUrl) {
    return MessageFormat.format(GITHUB_AUTHORIZE_PATTERN, ApiUrl.GITHUB.authorize(), clientId, redirectUrl);
}

GITHUB_AUTHORIZE_PATTERN 是一个常量:

private static final String GITHUB_AUTHORIZE_PATTERN = "{0}?client_id={1}&state=1&redirect_uri={2}";

ApiUrl 是一个枚举类:

public enum ApiUrl {
    GITHUB {
        @Override
        public String authorize() {
            return "https://github.com/login/oauth/authorize";
        }
    }
}

到这儿已经很明显了就是我们分析的第一步,构造了这样一个网址,用于取出授权码:

https://github.com/login/oauth/authorize?client_id=0123456789&redirect_uri=http://localhost:8080/callback&scope=user&state=1

然后我们来看 request 包下的 AuthGithubRequest.java:
@Override
protected AuthToken getAccessToken(String code) {
    String accessTokenUrl = UrlBuilder.getGithubAccessTokenUrl(config.getClientId(), config.getClientSecret(), code, config.getRedirectUri());
    HttpResponse response = HttpRequest.post(accessTokenUrl).execute();
    Map<String, String> res = GlobalAuthUtil.parseStringToMap(response.body());
    if (res.containsKey("error")) {
        throw new AuthException(res.get("error") + ":" + res.get("error_description"));
    }
    return AuthToken.builder()
        .accessToken(res.get("access_token"))
        .build();
}

config 即 AuthConfig 类的对象:

public class AuthConfig {

    /**
     * 客户端id:对应个平台的appKey
     */
    private String clientId;

    /**
     * 客户端Secret:对应个平台的appSecret
     */
    private String clientSecret;java

    /**
     * 登录成功后的回调地址
     */
    private String redirectUri;
}

getGithubAccessTokenUrl() 方法即构造了我们第二步构造的网址,用于根据授权码取到访问令牌:

public static String getGithubAccessTokenUrl(String clientId, String clientSecret, String code, String redirectUri) {
    return MessageFormat.format(GITHUB_ACCESS_TOKEN_PATTERN, ApiUrl.GITHUB.accessToken(), clientId, clientSecret, code, redirectUri);
}

然后直接用了 HttpRequest 进行 post 请求:

HttpResponse response = HttpRequest.post(accessTokenUrl).execute();

GlobalAuthUtil 类的 parseStringToMap() 方法是用来解析 post 请求得到的数据的,通过分割 "&" 和 "=" 获取到 accessToken(访问令牌):

public static Map<String, String> parseStringToMap(String accessTokenStr) {
    Map<String, String> res = new HashMap<>();
    if (accessTokenStr.contains("&")) {
        String[] fields = accessTokenStr.split("&");
        for (String field : fields) {
            if (field.contains("=")) {
                String[] keyValue = field.split("=");
                res.put(GlobalAuthUtil.urlDecode(keyValue[0]), keyValue.length == 2 ? GlobalAuthUtil.urlDecode(keyValue[1]) : null);
            }
        }
    }
    return res;
}

接下来就是根据访问令牌获取 json 数据了:
@Override
protected AuthUser getUserInfo(AuthToken authToken) {
    String accessToken = authToken.getAccessToken();
    HttpResponse response = HttpRequest.get(UrlBuilder.getGithubUserInfoUrl(accessToken)).execute();
    String userInfo = response.body();
    JSONObject object = JSONObject.parseObject(userInfo);
    return AuthUser.builder()
        .uuid(object.getString("id"))
        .username(object.getString("login"))
        .avatar(object.getString("avatar_url"))
        .blog(object.getString("blog"))
        .nickname(object.getString("name"))
        .company(object.getString("company"))
        .location(object.getString("location"))
        .email(object.getString("email"))
        .remark(object.getString("bio"))
        .token(authToken)
        .source(AuthSource.GITHUB)
        .build();
}

分析下来整体思路是一样的,只是我的 demo 在他的面前就是个玩具。