OAuth授权登录从原理到实战

简单介绍

OAuth(全称:Open Authorization)是目前最流行的授权机制,用来授权第三方应用,获取用户数据。先前登录任何一个网站必须要注册,而繁琐的信息填写让用户很是无奈,现在可以通过OAuth协议来授权登录第三方应用,免去了注册流程,一定程度上提高了用户留存率。

OAuth授权流程分析

授权码方式授权大体上分为四步:

  1. 第三方应用引导用户访问授权服务器,试图获取用户授权,用户选择是否授权
  2. 授权服务器获取到用户授权后,给应用服务器返回授权码
  3. 应用服务器携带授权码向授权服务器请求令牌,授权服务器对授权码及客户端秘钥认证成功后发放令牌
  4. 应用服务器携带令牌向授权服务器请求用户信息,授权服务器对令牌进行认证后返回所需信息

下面是我画的一个简易时序图:

可以看到用户只要一次授权即可完成登录,而授权码、令牌等交给应用服务器去处理,非常方便!

为什么授权服务器要返回code而不是直接返回access_token,这是一步冗余操作吗?
首先它不是冗余操作,这和它的请求方式是有关系的,是为了保证安全,在后边实战环节是可以看到的。

  • 引导用户访问环节,就是一个超链接,那是一个get请求,在授权服务器拿到用户授权后,是通过超链接中redirect_uri传回来的,在浏览器地址栏就可以看到,如果直接返回access_token的话,是很不安全的。
  • 所以OAuth引入授权码,授权码可见无所谓,因为去请求令牌时还会再次校验(更高层次的校验,请求时不光要授权码还要应用本身的的一些身份信息),但是这次授权服务器响应没经过浏览器,直接到达后端,这样虽然多了一次请求但是保证了令牌的安全。

OAuth授权实战

写在前面,我对接了QQGitHub、微博登录,流程基本一致,但是在一些API参数上有出入,需要自己去查询文档,如QQ请求用户信息要用户OpenID,微博要uid等等。GitHub是最简单的,可以localhost本地测试,像微博、QQ需要IP和域名了,微信貌似不支持个人开发。

以下以GitHub为例开始实战环节:

  1. GitHub上登记应用信息,获取client idclient secret
    如何创建
  2. 摆放链接,引导用户点击
    详细文档 (这个文档整个授权过程中参数、请求方式都有)
    <a id="github" class="github" href="https://github.com/login/oauth/authorize?client_id=xx&redirect_uri=http://127.0.0.1:8085/githubcallback&scope=user&state=1">
        <i class="fa fa-github"></i>
    </a>
    复制代码
  3. 后台接收授权码code,并携带code申请令牌
    // 封装申请令牌的参数
    @Data
    public class GitHubAccessTokenDTO {
        // 注册时收到的客户端ID
        private String client_id;
        // 注册时收到的客户密码
        private String client_secret;
        // 授权码
        private String code;
        // 重定向uri   授权后让用户跳转到哪里
        private String redirect_uri;
        // 自己提供的随机字符串   防止跨站攻击
        private String state;
    }
    
    // 发送Post请求,申请令牌(使用的是okhttp3)
    public String getAccessToken(GitHubAccessTokenDTO 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 str = response.body().string();
            // 截取有用的部分
            String token = str.split("&")[0].split("=")[1];
            return token;
        } catch (IOException e) {
            log.error("get GitHub access_token error, {}", e);
        }
        return null;
    }
    
    // Controller接收code,state,请求令牌
    @GetMapping("/githubcallback")
    public String github_callback(@RequestParam(name = "code") String code,
                                  @RequestParam(name = "state") String state) {
        GitHubAccessTokenDTO accessTokenDTO = new GitHubAccessTokenDTO();
        accessTokenDTO.setClient_id(github_clientId);
        accessTokenDTO.setClient_secret(github_secret);
        accessTokenDTO.setState(state);
        accessTokenDTO.setCode(code);
        accessTokenDTO.setRedirect_uri(github_redirectUri);
        String accessToken = gitHubProvider.getAccessToken(accessTokenDTO);     
    }
    复制代码
  4. 根据令牌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 str = response.body().string();// json格式,需转换(使用的是FastJSON)
            GitHubUser gitHubUser = JSON.parseObject(str, GitHubUser.class);
            return gitHubUser;
        } catch (IOException e) {
            log.error("get GitHub User error, {}", e);
        }
        return null;
    }
    
    // 在github_callback()接着请求
    GitHubUser gitHubUser = gitHubProvider.getUser(accessToken);
    复制代码
  5. 拿到用户信息,可以落库持久化
    // 在github_callback()中处理
    if (gitHubUser != null) {// 登录成功
        // 数据库user拿想要的信息(也可以登录态持久化,添加Cookie等一系列操作。。。)
        // userService.insert(user);。。。
        
        // 登录成功,回到首页
        return "redirect:/";
    }
    复制代码

总结

在对接其他授权服务的过程中,需要查阅相关文档,有的文档描述不清晰的,还需要手动Debug去测试,还是挺锻炼一个人能力的,当然全部对接完成后还是有一定成就感的!
那以上就是我的一些个人见解,如有误,还请大家指正!!!

猜你喜欢

转载自juejin.im/post/5e731cf2f265da571e263bed