基于oauth2.0的授权更新时间:2017-09-13 

一、引言

如果您的应用和BMOP对接后,为保证接口交互的安全性与隐私性,您的应用需要取得的授权才能访问接口。

力方帐号授权,采用国际通用的OAuth2.0标准协议,作为用户身份验证与授权协议,支持网站、手机客户端、桌面客户端。 若要了解更多关于 OAuth2.0 的技术说明文档,可参看官方网站(http://oauth.net/2/) 。

目前力方OAuth2.0服务仅支持Server-side flow获取Access Token(授权令牌)的方式。 此流程要求ISV应用有Web Server应用,能够保存应用本身的密钥以及状态,可以通过http直接访问力方的授权服务器。

二、授权获取流程

此流程需要您有自己的web服务器,能够保存应用本身的密钥以及状态,可以通过http直接访问力方的授权服务器。

1、授权用户绑定

为了保护开发者利益,防止用户越权使用,所有用户在使用应用之前,必须进行授权编号绑定,在开放平台“控制台 > 应用管理 > 授权管理”页面进行设置,将需要授权的用户的A编号添加到应用授权用户列表,该设置将直接影响应用的安全等级。

2、请求入口地址

业务方式 入口地址(HTTP) 入口地址(HTTPS)
获取授权码(code) http://oauth.bm001.com/authorize https://oauth.bm001.com/authorize
获取接口访问令牌(accessToken) http://oauth.bm001.com/token https://oauth.bm001.com/token
下图为整个授权方式流程图,以下按流程图逐步说明:

3、拼接授权URL

  • 拼接用户授权需要访问的url,示例及参数说明如下:
    https://oauth.bm001.com/authorize?client_id=10000013&response_type=code&redirect_uri=http://www.bm001.com&state=123&view=web
    参数说明
    参数名字 参数选项 参数值 参数释义
    client_id 必选 即应用的appKey
    response_type 必选 code
    redirect_uri 必选 参见redirect_uri的定义
    state 可选 维持应用的状态,传入值与返回值保持一致。
    view 可选 默认为web 授权页面样式,不传默认web,可选值:web、app

4、引导用户授权登录

引导用户通过浏览器访问以上授权url,将弹出以下登录页面,用户输入账号,密码以后,点击"登录"按钮,即可进入授权页面。

5、获取授权码code

登录成功,进入授权界面,如果用户点击"授权"按钮,力方开放平台会将授权码code返回到回调地址上面,应用接下来就可以用该code去换取accessToken。

回调URL示例:http://XXXXXXXXXX/callBack?code=90b9f0a1175e2e9afb5b9864c8f8c185&state=123
如果用户未点击授权而是点了"取消"按钮,则返回error,其中error为错误码,具体见下面返回值说明:
  • 返回值说明
    名称 说明
    code 授权码(只能使用一次,10分钟有效期)
    state 与请求授权时传入的一样
    error 错误信息

6、换取accessToken

  • 根据上一步获取的授权码code获取accessToken令牌,请求方法为POST请求。
    请求地址:https://oauth.bm001.com/token
    换取accessToken请求参数说明
    参数名称 参数选项 参数值 参数释义
    client_id 必选 appKey
    grant_type 必选 authorization_code 用code请求token时传authorization_code
    code 必选 每个授权码只能使用1次,10分钟内有效
    state 可选 维持应用的状态,传入值与返回值保持一致。
    sign 必选 请求签名,具体签名方法详见本文档最下方签名方法
  • 请求成功返回:
    {
        status: 1,
        errorCode: 0,
        errorMessage: null,
        data: {
            access_token: "ca79e16d7363682d1bd41b6d99140115",
            expires_in: 86400,
            refresh_token: "84c97358d110fa81c5d89f496c49913e",
            re_expires_in: 86400,
            token_type: "Bearer",
            parent_id: "A00000",
            user_id: "A854800",
            user_nick: "qmopen",
            sub_user_id: "E183727",
            sub_user_nick: "maomao"
        }
    }
    
    请求失败返回:
    {
        status: 0,
        errorCode: 104,
        errorMessage: "code不存在或已失效!",
        data: null
    }
    
  • 换取access_token返回参数说明
    名称 类型 示例 说明
    token_type String Bearer 目前accessToken类型只支持(Bearer)
    access_token String ca79e16d7363682d1bd41b6d99140115 Access Token即授权访问令牌
    expires_in number 86400(表示86400秒后过期) Access Token过期时间(单位:秒)
    refresh_token String 84c97358d110fa81c5d89f496c49913e Refresh Token 即刷新令牌
    re_expires_in number 86400(表示86400秒后过期) Refresh Token过期时间(单位:秒)
    parent_id String A00000 授权用户的上级编号
    user_id String A854800 力方账号对应的ID
    user_nick String qmopen 力方登录账号
    sub_user_id String E183727 商家员工账号对应的ID
    sub_user_nick String maomao 商家员工登录账号

注:授权成功获取到accessToken以后,就可以调用相关API接口,在有效期内(expires_in,单位秒),accessToken是可以一直使用的,只需要在accessToken过期之前,用refreshToken刷新换取新的accessToken,新的accessToken颁发会导致旧的accessToken和refreshToken立即失效,刷新accessToken的方法见下方“令牌刷新”。

7、授权辅助工具

点击AccessToken辅助获取工具,您可以直接使用开放平台提供的授权工具自助获取accessToken。见下图所示:

三、令牌刷新

    access_token是有一定的有效期的,请在access_token过期之前刷新,请求方法使用POST请求。通过授权获取的refresh_token,可以用来刷新access_token的时长,操作方法类似换取access_token,仅仅请求参数有区别,说明如下:

    请求地址:https://oauth.bm001.com/token
    刷新access_token参数说明
    参数名称 参数选项 参数值 参数释义
    client_id 必选 即应用的appKey
    grant_type 必选 refresh_token 刷新token时传refresh_token
    state 可选 维持应用的状态,传入值与返回值保持一致。
    sign 必选 请求签名,签名方法具体见下方签名方法
    refresh_token 必选 refreshToken值 刷新令牌,请在refreshToken有效期内刷新accesstoken,每次刷新后,accessToken和refreshToken都会变更
注1:一个refreshToken只能使用一次,每次刷新accessToken的同时,refreshToken也会一起被刷新,每个应用下面单个accessToken一天内最多可刷新60次。
注2:refreshToken刷新方式的返回值与code方式换取accessToken的返回值格式是一致的。
注3:开放平台建议:无论accessToken有效时长多久,最好一天刷新accessToken一到两次,以提高系统的安全性。

四、授权时长说明

为了更加灵活的对力方开放平台开放的接口和数据进行安全有效的管控,从用户的安全角度出发,减少用户数据泄露或者被恶意更改的可能,力方开放平台对开发者的应用进行分级处理。

如何提高自己应用的安全等级,点击安全等级说明,查看应用的安全等级说明文档。

为了减少数据泄露或者被恶意更改的可能,授权时长根据应用等级、应用类型分别给予不同的有效时长。

授权时长与应用安全等级关系
应用安全等级 accessToken有效时长 refreshToken失效时长
2级 固定时长90天 同accessToken时长
1级 固定时长30天 同accessToken时长
0级 固定时长1天 同accessToken时长

最近有一些开发者反馈accessToken的授权和获取太过于麻烦,并且开发者承诺能保证自身的accessToken安全,希望能够得到一个长久的accessToken。 为了解决满足这个需求,现在有获取长期accessToken的方法, 登录“直销商系统 > 数据中心 > 开发工具”会看到下面有一个accessToken,点击更新,将会获取新的accessToken,有效期30年

五、异常说明

授权常见错误码说明
errorCode errorMessage
100 系统繁忙,请稍后再试!
101 client_id不存在或已删除!
102 此应用尚未审核通过或者已被暂时关闭!
103 签名不正确!
104 code不存在或已失效!
105 code和client_id不匹配!
106 refresh_token不能为空!
107 refresh_token不存在或已过期!
108 code不能为空!
109 redirect_uri和callback不在同一根域名
110 您的用户编号还未被绑定到该应用,请联系应用所有者设置
111 刷新次数超过上限,每个accessToken一天最多可刷新60次
112 您还没有签订合同或者合同已到期
113 您还没有订购该应用或者订购的应用已到期
114 您不是应用所属用户的下级,不能授权该应用
115 该用户已被冻结,不能登录授权或刷新

六、名词解释

  • 1、redirect_uri

    redirect_uri指的是应用发起请求时所传的回调地址参数,在用户登录授权后,力方授权服务器会回调redirect_uri地址,将code查询码一并返回,可在接受到code以后发起post请求,换取最终的accessToken令牌。

    举例:假如您的应用回调地址为redirect_uri=http://www.XXXXX.com/qianmi/auth/callBack,授权成功后,力方授权服务器会回调该地址,将code返回,您的应用在接受到code以后就可以进行下一步换取accessToken操作。

  • 2、view

    view参数可选值:web、app。web对应普通pc端的浏览器页面样式,app对应手机等无线客户端的浏览器页面样式。没有任何特殊说明的情况下,默认为web样式,如果通过手机端访问授权,必须指定为app。

  • 3、state

    client端的状态值。用于第三方应用防止CSRF攻击,成功授权后回调时会原样带回。请务必严格按照流程检查用户与state参数状态的绑定。

  • 4、Access Token

    Access Token是用户授权后颁发的session key,即接口访问令牌,同一应用、同一授权用户的Access Token是唯一的。

  • 5、Refresh Token

    Refresh Token是随accessToken一起颁发的刷新令牌,RefreshToken用来在accessToken过期之前重新刷新accessToken

  • 6、授权账号

    E生活应用不能直接用BOSS账号登录授权,必须使用一个直销商账号来登录授权。

  • 注:新授权颁发的Access Token会导致已有的授权Access Token失效。

七、代码示例

这里仅展示如何利用SDK获取accessToken的示例代码(需要使用SDK),请求成功以后,将responsejson串转化成对象,然后从里面可以解析各相关字段

import com.qianmi.open.api.ApiException;
import com.qianmi.open.api.response.TokenResponse;
import com.qianmi.open.api.tool.util.OAuthUtils;
import com.qianmi.open.api.tool.util.QianmiContext;

public class AuthorizeDemo {

    /**
     * 根据授权码获取token
     * @param appKey
     * @param appSecret
     * @param code
     * @return
     * @throws ApiException
     */
    public String getToken(String appKey, String appSecret, String code) throws ApiException {
        QianmiContext context = OAuthUtils.getToken(appKey, appSecret, code);
        TokenResponse response = context.getTokenResponse();
        String accessToken = response.getAccessToken();
        return accessToken;
    }

    /**
     * 根据refreshToken刷新token
     * @param appKey
     * @param appSecret
     * @param refreshToken
     * @return
     * @throws ApiException
     */
    public String refreshToken(String appKey, String appSecret, String refreshToken) throws ApiException {
        QianmiContext context = OAuthUtils.refreshToken(appKey, appSecret, refreshToken);
        TokenResponse response = context.getTokenResponse();
        String accessToken = response.getAccessToken();
        return accessToken;
    }

}
<?php
    $url = "http://oauth.bm001.com/token";
    $client_id = "100000";
    $appSecret = "pi8YzLlPtBTJjEiwS4f4G74EfmSBIV2m";
    $grant_type = "authorization_code";
    $code = "2918e3cae67108d3151eb6fad6888b1a";
    $data = Array (
           'client_id'  => $client_id,
           'code' => $code,
           'grant_type'  => $grant_type
    );
    ksort($data);
    $plain_text="";
    foreach($data as  $key => $value) {
        $plain_text .= $key.$value;
    }
    $plain_text  = $appSecret.$plain_text.$appSecret;
    $sign = strtoupper(sha1($plain_text));
    $data['sign'] = $sign;
    ksort($data);
    $url_params = "";
	foreach ($data as $key => $value) {
		$url_params .= "&".$key."=".$value;
	}
	$url_params = ltrim($url_params,"&");
	//curl初始化
	$ch = curl_init();
	curl_setopt ( $ch, CURLOPT_URL, $url );
	curl_setopt ( $ch, CURLOPT_POST, 1 );
	curl_setopt ( $ch, CURLOPT_HEADER, 0 );
	curl_setopt ( $ch, CURLOPT_RETURNTRANSFER, 1 );
	curl_setopt ( $ch, CURLOPT_POSTFIELDS, $url_params );
	$return = curl_exec ( $ch );
	//出错检测
	if(curl_errno($ch)){
		echo "curl error:".curl_errno($ch);
	}else{
		print_r($return);
	}
	curl_close ( $ch );
?>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Api;
using System.Api.Util;



namespace ConsoleApplication
    {
     class  OAuthDemo
      {
      /// <summary>
      ///   根据授权码获取token
      /// <summary>
      /// <param name="appKey">应用appkey</param>
      /// <param name="appSecret">应用密钥</param>
      /// <param name="code">授权码</param>
      /// <returns>AccessToken</returns>

    string getToken(string appKey, string appSecret, string code)
    {
        QianmiContext context = OAuthUtils.getToken(appKey, appSecret, code);
        return context.Token.AccessToken;
    }

      /// <summary>
      ///   根据授权码获取token
      /// <summary>
      /// <param name="appKey">应用appkey</param>
      /// <param name="appSecret">应用密钥</param>
      /// <param name="refreshToken">RefreshToken</param>
      /// <returns>AccessToken</returns>
    string refreshToken(string appKey, string appSecret, string refreshToken)
    {
        QianmiContext context = OAuthUtils.refreshToken(appKey, appSecret, refreshToken);
        return context.Token.AccessToken;
    }
  }
}

八、数字签名算法

为了防止在获取accessToken或者refreshToken过程中被黑客恶意篡改,需要对请求参数进行签名验证,签名不合法的请求将会被拒绝,力方开放平台目前支持的签名算法为SHA1,签名的大致过程如下:

  • 对所有授权请求参数(但除去sign参数本身),根据参数名称的ASCII码表的顺序排序。如:bad=2,bac=1,cba=3,排序后的顺序是bac=1,bad=2,cba=3。
  • 将排序好的参数排在一起,根据上面的示例得到的排序后的结果:bac1bad2cba3。
  • 把拼装好的字符串用UTF-8编码,需要在拼装后的字符串前后都加上应用的appSecret(应用证书中查看)在进行SHA1摘要,例如应用的appSecret为Banma,进行签名计算:SHA1(Banmabac1bad2cba3Banma)
  • 将摘要得到的字节流转化成16进制,例如将上面的签名摘要转换后的结果为:8AC30853E229E19EB7C8BCA9782D3079CC7399E8

说明:SHA1为安全哈希算法,主要用于数字签名标准(DSS)中的签名算法(DSA),SHA1会产生一个160位的消息摘要,用16进制表示,一个16进制的字符能表示4个位,所以签名后的长度固定为40个16进制字符。

JAVA签名示例代码
package com.qianmi.util;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.*;

/**
 * Created by QianMi
 */
public class SignUtil {
    public static String sha1(String str) throws IOException {
        return byte2hex(getSHA1Digest(str));
    }

    private static byte[] getSHA1Digest(String data) throws IOException {
        byte[] bytes;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            bytes = md.digest(data.getBytes("utf-8"));
        } catch (GeneralSecurityException gse) {
            throw new IOException(gse);
        }
        return bytes;
    }

    private static String byte2hex(byte[] bytes) {
        StringBuilder sign = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if (hex.length() == 1) {
                sign.append("0");
            }
            sign.append(hex.toUpperCase());
        }
        return sign.toString();
    }

    public static String sign(Map<String, String> param, String secret) throws IOException {
        if (param == null) {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        List<String> paramNames = new ArrayList<>(param.size());
        paramNames.addAll(param.keySet());
        Collections.sort(paramNames);
        sb.append(secret);
        for (String paramName : paramNames) {
            sb.append(paramName).append(param.get(paramName));
        }
        sb.append(secret);
        return sha1(sb.toString());
    }

    public static void main(String[] args) throws IOException {
        Map<String, String> param = new HashMap<>();
        param.put("access_token", "7466bdfc5f79a7fe1defd9a5880a4b84");
        param.put("appKey", "10000");
        param.put("format", "json");
        param.put("v", "1.1");
        param.put("method", "bm.elife.recharge.mobile.getItemInfo");
        param.put("timestamp", "1428488009985");
        param.put("mobileNo", "13888888888");
        param.put("rechargeAmount", "100");
        System.out.println(sign(param, "test"));
    }
}

九、常见问题

1、为什么一定要通过浏览器授权,不能提供登录接口?

之所以不提供登录接口,是从安全角度考虑的,防止可能的第三方系统获取您的用户名、密码等资料。力方开放平台采用业内普遍使用的oAuth2.0协议进行登录授权认证,统一跳转力方授权服务器进行账号登录授权,授权完毕回调code至各ISV应用以便完成接口访问令牌(access_token)的换取。

2、我注册的是E生活商家后台应用,我们的会员需要进行授权吗?

E生活商家后台应用仅需要绑定的下级直销商账号授权即可,无需第三方会员账号在力方授权。此问题更多描述请参考“接入指南”中的常见问题回答。