OAuth2.0协议入门(一):OAuth2.0协议的基本概念以及使用受权码模式(authorization code)实现百度帐号登陆

一 OAuth2.0协议的基本概念

(1)OAuth2.0协议

OAuth协议,是一种受权协议,不涉及具体的代码,只是表示一种约定的流程和规范。OAuth协议通常用于用户决定是否把本身在某个服务商上面的资源(好比:用户基本资料、照片、视频等)受权给第三方应用访问。此外,OAuth2.0协议是OAuth协议的升级版,如今已经逐渐成为单点登陆(SSO)和用户受权的标准。php

不知道你们有没有发现,目前主流的互联网网站除了可使用“用户名+密码”模式和“手机号+验证码”模式登陆外,不少还提供了第三方帐号登陆,好比最多见的QQ登陆微博登陆百度帐号登陆GitHub登陆。而这些第三方登陆方式就是采用了OAuth2.0协议实现。html

CSDN的登陆界面

(2)为何使用OAuth2.0协议?

第一,用户再也不须要注册大量帐号。在之前,咱们每使用一个新的网站或者APP就须要注册一个帐号,创建一套新的帐户体系才能使用网站 / APP提供的服务。可是如今咱们只须要拥有几个主流应用的帐号,而后经过他们提供的第三方帐号登陆就可使用一个新的网站/APP了(固然,咱们也能够不使用腾讯百度等公司提供的受权服务,开发本身的受权服务端,这方面的内容我将放在下篇文章中介绍)。java

第二,用于单点登陆。若是某个公司有不少个须要用户登陆才能提供服务的子产品(好比:官网、M网站、APP、微信公众号、使用同一套帐户体系的产品一、产品2等等),这种状况下为每一个产品都开发一个登陆、受权模块显然是不太优雅,所以比较好的解决方案就是全部须要登陆的产品都请求同一个登陆受权中心,进行统一登陆受权处理。而OAuth2.0协议就能够实现符合上述要求的单点登陆功能。git

第三,用于分布式系统的权限控制。由于基于OAuth2.0协议得到的令牌(Access Token)同时关联了接入的第三方应用、受权用户、权限范围等信息。所以,在第三方应用拿着Token请求资源的时候,资源服务应用就能够很容易根据其访问权限返回相应的数据。web

(3)OAuth2.0协议涉及到的几个重要角色

  • 受权服务端应用(Authorization Server):服务提供商提供的专门用于处理受权的服务端应用,好比上面介绍的QQ登陆、微博登陆,固然也能够搭建本身的受权服务端。
  • 资源服务应用(Resource Server):服务提供商存放用户及其余资源的应用,通常用于接口的形式返回第三方应用请求的资源。它能够与受权服务端属于同一个应用,也能够分别属于不一样的应用。
  • 用户(User):用户在受权服务端登陆,受权服务端记录了用户的帐户体系。固然,有的网站会在你经过第三方帐号第一次登陆成功后,要求绑定你的手机号并建立昵称,这就是他们在建立本身的帐户体系(跟OAuth2.0协议无关,这里不做展开)了。
  • 接入的第三方应用(Third-party Application):接入认证的第三方应用又被称为“客户端”,好比一个普通的网站、APP。

(4)几种受权模式

  • 受权码模式(authorization code):这是功能最完整,流程最严密的模式。如今主流的使用OAuth2.0协议受权的服务提供商都采用了这种模式,我在下面举例也将采起这种模式。
  • 简化模式(implicit):跳过了请求受权码(Authorization Code)的步骤,直接经过浏览器向受权服务端请求令牌(Access Token)。这种模式的特色是全部步骤都在浏览器中完成,Token对用户可见,且请求令牌的时候不须要传递client_secret进行客户端认证。
  • 密码模式(resource owner password credentials):用户向第三方客户端提供本身在受权服务端的用户名和密码,客户端经过用户提供的用户名和密码向受权服务端请求令牌(Access Token)。

(5)受权码模式(authorization code)受权的流程

采用Authorization Code获取Access Token的受权验证流程又被称为Web Server Flow,适用于全部有Server端的应用,如Web/Wap站点、有Server端的手机/桌面客户端应用等。通常来讲整体流程包含如下几个步骤:spring

  1. 经过client_id请求受权服务端,获取Authorization Code
  2. 经过Authorization Codeclient_idclient_secret请求受权服务端,在验证完Authorization Code是否失效以及接入的客户端信息是否有效(经过传递的client_idclient_secret信息和服务端已经保存的客户端信息进行匹配)以后,受权服务端生成Access TokenRefresh Token并返回给客户端。
  3. 客户端经过获得的Access Token请求资源服务应用,获取须要的且在申请的Access Token权限范围内的资源信息。

下面,我将经过基于受权码模式的百度OAuth2.0受权来详细介绍上面这三个步骤。固然,最后我会给出实际可运行的测试代码。apache

二 使用受权码模式实现百度帐号登陆

(1)在百度开发者中心新建一个应用

申请地址:developer.baidu.com/console#app…json

接着须要记录新建应用的API KeySecret Keyapi

新建应用
新建应用

以及须要在安全设置里面配置登陆的回调地址:浏览器

配置登陆的回调地址
配置登陆的回调地址

注:若是只是在浏览器中测试,能够把回调地址改为https://www.baidu.com,这样就能够直观地在浏览器中看到重定向的结果了,好比请求https://openapi.baidu.com/oauth/2.0/authorize?client_id=n1pRXWNYFQ1MQLzpDfHyovFb&redirect_uri=https://www.baidu.com&response_type=code&scope=basic&display=popup,返回结果以下:

受权回调示例

(2)获取Authorization Code

其获取方式是经过重定向用户浏览器(或手机/桌面应用中的浏览器组件)到http://openapi.baidu.com/oauth/2.0/authorize地址,并带上如下参数:

  • client_id:必须参数,注册应用时得到的API Key
  • response_type:必须参数,此值固定为“code”。
  • redirect_uri:必须参数,受权后要回调的URI,即接收Authorization Code的URI。
  • scope:非必须参数,以空格分隔的权限列表,若不传递此参数,表明请求用户的默认权限。
  • state:非必须参数,用于保持请求和回调的状态,受权服务器在回调时(重定向用户浏览器到“redirect_uri”时),会在Query Parameter中原样回传该参数。OAuth2.0标准协议建议,利用state参数来防止CSRF攻击。
  • display:非必须参数,登陆和受权页面的展示样式,默认为“page”,具体参数定义请参考“自定义受权页面”一节。
  • force_login:非必须参数,如传递“force_login=1”,则加载登陆页时强制用户输入用户名和口令,不会从cookie中读取百度用户的登录状态。
  • confirm_login:非必须参数,如传递“confirm_login=1”且百度用户已处于登录状态,会提示是否使用已当前登录用户对应用受权。
  • login_type:非必须参数,如传递“login_type=sms”,受权页面会默认使用短信动态密码注册登录方式。

例如:client_idn1pRXWNYFa4MQLzpDfHyovFb的应用要请求某个用户的默认权限和email访问权限,并在受权后需跳转到http://localhost:7080/login,同时但愿在弹出窗口中展示用户登陆、受权界面,则应用须要重定向用户的浏览器到以下URL:

openapi.baidu.com/oauth/2.0/a…

响应数据包格式:

此时受权服务会根据应用传递参数的不一样,为用户展示不一样的受权页面。若是用户在此页面赞成受权,受权服务则将重定向用户浏览器到应用所指定的redirect_uri,并附带上表示受权服务所分配的Authorization Code的code参数,以及state参数(若是请求authorization code时带了这个参数)。

例如:继续上面的例子,假设受权服务在用户赞成受权后生成的 Authorization Code 为71c279ccd145a3dff977b38e6a8e34b4,则受权服务将会返回以下响应包以重定向用户浏览器到http://localhost:7080/login地址:

HTTP/1.1 302 Found Location: http://localhost:7080/login?code=71c279ccd145a3dff977b38e6a8e34b4

(3)经过Authorization Code获取Access Token

经过上面得到的Authorization Code,接下来即可以用其换取一个Access Token。获取方式是:应用在其服务端程序中发送请求(推荐使用POST)到 百度OAuth2.0受权服务的https://openapi.baidu.com/oauth/2.0/token地址,并带上如下5个必须参数:

  • grant_type:必须参数,此值固定为authorization_code
  • code:必须参数,经过上面第一步所得到的Authorization Code
  • client_id:必须参数,应用的API Key
  • client_secret:必须参数,应用的Secret Key
  • redirect_uri:必须参数,该值必须与获取Authorization Code时传递的redirect_uri保持一致。

例如:

openapi.baidu.com/oauth/2.0/t…

响应数据包格式:

若参数无误,服务器将返回一段JSON文本,包含如下参数:

  • access_token:要获取的Access Token。
  • expires_in:Access Token的有效期,以秒为单位(30天的有效期)。
  • refresh_token:用于刷新Access Token 的 Refresh Token,全部应用都会返回该参数(10年的有效期)。
  • scope:Access Token最终的访问范围,即用户实际授予的权限列表(用户在受权页面时,有可能会取消掉某些请求的权限)。
  • session_key:基于http调用Open API时所须要的Session Key,其有效期与Access Token一致。
  • session_secret:基于http调用Open API时计算参数签名用的签名密钥。

例如:

{
    "expires_in": 2592000,
    "refresh_token": "22.247946a05a327ia929b74354c3670cb2.315360000.1847863585.321432378-13484254",
    "access_token": "21.e2eb8577t4a68a32y23b61300eda8811.2592000.1536795385.321432378-13484254",
    "session_secret": "e8f9ee40de92862cc35c343n5da2fcfb",
    "session_key": "9mnRIQsyTR+0yfB3liSUjqGvk8F369TRfHJidz9iA0wDg\/KDBKZtGHACpXfULPjeX1YBWkKAtHSG\/OLXYKQHCuO4Zg2JiBwFtA==",
    "scope": "basic"
}
复制代码

若请求错误,服务器将返回一段JSON文本,包含如下参数:

  • error:错误码,关于错误码的详细信息请参考百度OAuth2.0错误响应
  • error_description:错误描述信息,用来帮助理解和解决发生的错误。

(4)使用Access Token获取百度用户的基本资料

使用上面获得的Access Token获取百度用户的基本资料,包括:用户名、性别、是否实名认证、是否验证手机号等等。

相关的REST API接口能够参考官方文档:developer.baidu.com/wiki/index.…

请求示例(获取用户基本信息)

openapi.baidu.com/rest/2.0/pa…

(5)在普通Java Web项目中实现百度OAuth2.0受权登陆

提示:下面只会给出关键代码逻辑,完整可用代码能够参考:gitee.com/zifangsky/B…

首先建立两个实体类,分别表示请求Access Token的返回信息以及请求百度用户基本资料的返回信息。

AuthorizationResponse.java:

package cn.zifangsky.model;

/** * Authorization返回信息 * * @author zifangsky * @date 2018/7/25 * @since 1.0.0 */
public class AuthorizationResponse {

    /** * 要获取的Access Token(30天的有效期) */
    private String access_token;

    /** * 用于刷新Access Token 的 Refresh Token(10年的有效期) */
    private String refresh_token;

    /** * Access Token最终的访问范围 */
    private String scope;

    /** * Access Token的有效期,以秒为单位(30天的有效期) */
    private Long expires_in;

    /** * 基于http调用Open API时所须要的Session Key,其有效期与Access Token一致 */
    private String session_key;

    /** * 基于http调用Open API时计算参数签名用的签名密钥 */
    private String session_secret;

    /** * 错误信息 */
    private String error;

    /** * 错误描述 */
    private String error_description;

    //省略setter和getter

    @Override
    public String toString() {
        return "AuthorizationResponse{" +
                "access_token='" + access_token + '\'' +
                ", refresh_token='" + refresh_token + '\'' +
                ", scope='" + scope + '\'' +
                ", expires_in=" + expires_in +
                ", session_key='" + session_key + '\'' +
                ", session_secret='" + session_secret + '\'' +
                ", error='" + error + '\'' +
                ", error_description='" + error_description + '\'' +
                '}';
    }
}
复制代码

BaiduUser.java:

package cn.zifangsky.model;

/** * 百度返回的用户基本信息 * * @author zifangsky * @date 2018/7/25 * @since 1.0.0 */
public class BaiduUser {

    /** * 百度的userId */
    private String userid;

    /** * 用户名 */
    private String username;

    /** * 用户性别,0表示女性,1表示男性 */
    private Integer sex;

    /** * 用户生日 */
    private String birthday;

    /** * 用户描述 */
    private String userdetail;

    /** * 是否绑定手机号 */
    private Integer is_bind_mobile;

    /** * 是否已经实名认证 */
    private Integer is_realname;

    //省略setter和getter

    @Override
    public String toString() {
        return "BaiduUser{" +
                "userid='" + userid + '\'' +
                ", username='" + username + '\'' +
                ", sex=" + sex +
                ", birthday='" + birthday + '\'' +
                ", userdetail='" + userdetail + '\'' +
                ", is_bind_mobile=" + is_bind_mobile +
                ", is_realname=" + is_realname +
                '}';
    }
}
复制代码

最后就是最关键的用户登陆逻辑了:

package cn.zifangsky.controller;

import cn.zifangsky.common.Constants;
import cn.zifangsky.model.AuthorizationResponse;
import cn.zifangsky.model.BaiduUser;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.text.MessageFormat;

/** * 登陆 * @author zifangsky * @date 2018/7/9 * @since 1.0.0 */
@Controller
public class LoginController {

    @Autowired
    private RestTemplate restTemplate;

    @Value("${baidu.oauth2.client-id}")
    private String clientId;

    @Value("${baidu.oauth2.scope}")
    private String scope;

    @Value("${baidu.oauth2.client-secret}")
    private String clientSecret;

    @Value("${baidu.oauth2.user-authorization-uri}")
    private String authorizationUri;

    @Value("${baidu.oauth2.access-token-uri}")
    private String accessTokenUri;

    @Value("${baidu.oauth2.resource.userInfoUri}")
    private String userInfoUri;

    /** * 登陆验证(实际登陆调用认证服务器) * @author zifangsky * @date 2018/7/25 16:42 * @since 1.0.0 * @param request HttpServletRequest * @return org.springframework.web.servlet.ModelAndView */
    @RequestMapping("/login")
    public ModelAndView login(HttpServletRequest request){
        //当前系统登陆成功以后的回调URL
        String redirectUrl = request.getParameter("redirectUrl");
        //当前系统请求认证服务器成功以后返回的Authorization Code
        String code = request.getParameter("code");

        //最后重定向的URL
        String resultUrl = "redirect:";
        HttpSession session = request.getSession();
        //当前请求路径
        String currentUrl = request.getRequestURL().toString();

        //code为空,则说明当前请求不是认证服务器的回调请求,则重定向URL到百度OAuth2.0登陆
        if(StringUtils.isBlank(code)){
            //若是存在回调URL,则将这个URL添加到session
            if(StringUtils.isNoneBlank(redirectUrl)){
                session.setAttribute("redirectUrl",redirectUrl);
            }

            resultUrl += authorizationUri + MessageFormat.format("?client_id={0}&response_type=code&scope=basic&display=popup&redirect_uri={1}"
            ,clientId,currentUrl);
        }else{
            //1. 经过Authorization Code获取Access Token
            AuthorizationResponse response = restTemplate.getForObject(accessTokenUri + "?client_id={1}&client_secret={2}&grant_type=authorization_code&code={3}&redirect_uri={4}"
                    ,AuthorizationResponse.class
                    , clientId, clientSecret, code,currentUrl);

            //2. 若是正常返回
            if(response != null && StringUtils.isNoneBlank(response.getAccess_token())){
                System.out.println(response);

                //2.1 将Access Token存到session
                session.setAttribute(Constants.SESSION_ACCESS_TOKEN,response.getAccess_token());

                //2.2 再次查询用户基础信息,并将用户ID存到session
                BaiduUser baiduUser = restTemplate.getForObject(userInfoUri + "?access_token={1}"
                        ,BaiduUser.class
                        ,response.getAccess_token());

                if(baiduUser != null &&  StringUtils.isNoneBlank(baiduUser.getUserid())){
                    System.out.println(baiduUser);

                    session.setAttribute(Constants.SESSION_USER_ID,baiduUser.getUserid());
                }
            }

            //3. 从session中获取回调URL,并返回
            redirectUrl = (String) session.getAttribute("redirectUrl");
            session.removeAttribute("redirectUrl");
            if(StringUtils.isNoneBlank(redirectUrl)){
                resultUrl += redirectUrl;
            }else{
                resultUrl += "/user/userIndex";
            }
        }

        return new ModelAndView(resultUrl);
    }

}
复制代码

上面代码里面的注释已经很详细了,这里我就很少作解释了,详细代码能够自行参考上面给出的示例源码。本篇文章到此结束,我将在下篇文章中介绍如何本身手动实现OAuth2.0受权服务端,敬请期待!

参考:

相关文章
相关标签/搜索