在描述鉴权和流控之前,可能需要先描述为什么需要搭建开放平台。 

开放平台最先由FB推出,而后在2012年左右,国内比较大型的互联网公司都开始搭建自己的开放平台。

 

搭建属于自己的开放平台的原因,一般是以下几点:

1. 借助第三方满足用户的零碎需求

2. 借助第三方提升自己的影响力

3. 作为渠道获得第三方应用带来的红利

4. 加深与用户的粘连

 

OAuth2.0协议是一个协议,不是具体的框架或者实现,这个协议定义了一个流程,保证这个流程的实现相应的保证权限管理的成功。 

OAuth2.0中主要规定了三方。

1. 第三方程序

2. 用户及客户端

3. 保存用户信息的服务器

 

OAuth2.0协议出现是为了解决第三方程序可以获取保存在服务器上的用户的信息但用户又能不将自己的账号密码告知第三方程序。 

这个过程通过3个流程来实现。

1. 第三方程序向平台注册应用,获取AppKey,Appsecret。

2. 第三方程序向平台提供的接口1发起请求,平台向用户索要账号密码,验证通过后返回给第三方程序一个code(临时授权码)。

3. 第三方程序拿着这个code再次向平台提供的接口2发起请求,并需一同提供AppKey,Appsecret,平台校验通过后返回accessToken,第三方程序就可以拿着这个accessToken开始真正的业务操作了。

code的出现,是为了防止中间人攻击,即,平台需要第三方程序证明是其需要获取用户的信息,而不是拦截下信息的其他程序。 

除了accessToken,一些开放平台,比如微信,会一同返回refreshToken,其用来刷新accessToken。

 

以新浪开放平台和微信开放平台为例,描述鉴权的实现流程. 

新浪API: 

微信API: 

所以,我们可以定义三个接口:

public String generateRequestToken(String appKey) throws AuthorizeException;

此接口用于获取code

public OpenRefreshToken generateAccessToken(String appKey, final String appSecret, String requestToken) throws AuthorizeException; 

此接口用于获取accessToken。 

 

如果参见新浪api和微信api的授权返回值,其分别如下:

新浪API
{
  "accesstoken":"ACCESSTOKEN",
  "expires_in":"1234",
  "uid":"123456"
}

微信API
{
  "accessToken":"ACCESS_TOKEN",
  "expires_in":"7200",
  "refresh_token":"REFRESHTOKEN",
  "open_id":"OPEN_ID",
  "scope":"SCOPE"
}

不同的平台根据其业务的不同其返回的值也不同。 

设计的接口的返回对象将在上层逻辑中被处理再以JSON格式返回给第三方应用。

public OpenRefreshToken refreshAccessToken(String appSecret, String refreshToken) throws AuthorizeException;

此接口用于刷新accesstoken,除了需要传入refreshtoken外,还需要传入appkey和appsecret以验证第三方程序的身份,其与refreshtoken是否匹配,

注:这一点上不同平台可能有不同。 

以微信的刷新授权令牌流程为例(AS-REDIS为个人猜测): 

最后,根据业务的不同提供常规的业务接口,但是这个接口中需要带上accessToken。

按照OAuth2协议,接口的设计就这样定下来了。

对于权限判断的实现,其实可以看出来appkey,code,accesstoken,refreshtoken之间的映射关系,根据协议的实现流程,我们能看出:

appkey -> code
code -> accesstoken
accesstoken <-> refreshtoken

1. 通过appkey能获得code 

2. 通过code能获得accesstoken 

3. 通过accesstoken能获得refreshtoken,否则任何一个refreshtoken都可用于刷新 

4. 通过refreshtoken能获得accesstoken,要让refreshtoken知道该刷新谁。

但在接口的实现中,如果保存这种映射关系。我们需要验证信息在这OAuth2协议中交互的3步中被传递。 

用户可访问的接口,应该在返回code时就被确定下来,并被缓存下来用于后续的第三方应用接口鉴权。 

应用的appkey,appsecret也应在返回code前被缓存下来,用于code的验证,accesstoken的验证。 

accesstoken的超时时间,refreshtoken的超时时间,也应被缓存下来用于请求到达时的超时判断。

1.每个appkey对应一个code对象,每个code对象生成一个code,将code返回给第三方应用。这是交互的第一步。 

2.第三方应用带上appkey,appsecret,code请求获取accesstoken的接口,通过code找到code对象,比对code对象中缓存的appkey,appsecret以确认该code是用户授权给该应用获得的,验证成功后生成accesstoken对象和refreshtoken对象,让accesstoken对象成为refreshtoken对象的一个属性,设置超时时间,返回accestoken对象/refreshtoken对象生成的accesstoken码/refreshtoken码,这是交互的第二步。 

3.刷新accesstoken,用户带上refrehtoken码/第三方应用调用业务接口,带上accesstoken。

映射关系可以通过K-V内存数据库完成,由于这些验证数据都是有状态的,将有状态的数据集中放入缓存或数据库,以保证应用的无状态,便于横向扩展。 

那么映射关系如下,以redis作为缓存数据库:

appkey,用户授权 -> 应用相关信息类A(比如appkey,UID,可访问接口)
code码 -> code对象,包括类A
accesstoken -> accesstoke对象,包括类A,超时时间,生成时间
refreshtoken -> refreshtoken对象,包括类A,包括accesstoken对象,超时时间,生成时间

最近在负责公司开发平台授权和流控部分,授权按照OAuth2协议实现,

以下是code,accesstoken,refreshtoken的主要流程,方便对以上内容的理解,上述内容可能会显得有些抽象。

/**
 * api开放平台授权、鉴权实现
 *
 * @author zhenghao:
 * @version 1.0 2016/5/27
 */
@Service("asAuthorizeService")
public class AsAuthorizeServiceImpl implements AsAuthorizeService {

    @Autowired
    private ValidatorService validatorService;

    @Autowired
    private CacheService cacheService;

    @Autowired
    private OpenTokenService openTokenService;

    @Autowired
    private OpenSecretService openSecretService;

    /**
     *
     * @param appKey
     * @param appSecret
     * @return
     * @throws AuthorizeException
     * @desription 后期的用户验证将在此逻辑中完成
     *               通过AppSecret获取OpenConsumer对象
     *               通过获取结果以及验证AppKey与OpenConsumer对象中appkey是否一致完成验证
     *               验证通过后创建OpenRequestToken对象并返回code(临时授权码)
     *               code与generateAccessToken函数中参数:requestToken是一个含义
     */
    @Override
    public String generateRequestToken(String appKey, String appSecret) throws AuthorizeException {
        OpenConsumer openConsumer = validatorService.validateAppKeyAndAppSecret(appKey, appSecret);
        OpenRequestToken openRequestToken = new OpenRequestToken();
        openRequestToken.setOpenConsumer(openConsumer);
        openRequestToken.setTimeStamp(System.currentTimeMillis());
        String code = openRequestToken.generate();
        cacheService.setRequestToken(code, openRequestToken);
        return code;
    }

    /**
     *
     * @param appKey
     * @param appSecret
     * @param requestToken 临时令牌
     * @return
     * @throws AuthorizeException
     * @Description 验证RequestToken是否为向用户请求授权的客户端发出
     *                验证成功后创建AccessToken和RefreshToken
     *                将AccessToken,RefreshToken存于Reis,将AccessToken另存于数据库
     *                出于实时性的考虑,存Redis未采用异步操作
     *                返回的OpenRefreshToken将在API层做处理
     */
    @Override
    public OpenRefreshToken generateAccessToken(String appKey, final String appSecret, String requestToken) throws AuthorizeException {
        OpenRequestToken openRequestToken = validatorService.validateRequestToken(appKey, appSecret,requestToken);
        final OpenAccessToken openAccessToken = new OpenAccessToken();
        openAccessToken.setOpenConsumer(openRequestToken.getOpenConsumer());
        openAccessToken.setTimeStamp(System.currentTimeMillis());
        final String accessToken = openAccessToken.generate();
        OpenRefreshToken openRefreshToken = new OpenRefreshToken();
        openRefreshToken.setAccessToken(openAccessToken);
        openRefreshToken.setTimeStamp(openAccessToken.getTimeStamp());
        openRefreshToken.setOpenConsumer(openAccessToken.getOpenConsumer());
        String refreshToken = openRefreshToken.generate();
        cacheService.setOpenRefreshToken(refreshToken, openRefreshToken);
        cacheService.setOpenAccessToken(accessToken, openAccessToken);

        new Thread(new Runnable()
        {
           @Override
           public void run() {
               OpenTokenPo openTokenPo = openTokenService.getOpenTokenByAppSecret(appSecret);
               if(openTokenPo == null){
                   openTokenPo = new OpenTokenPo();
                   openTokenPo.setCreateTime(new Date(openAccessToken.getTimeStamp()));
                   OpenSecretPo openSecretPo = openSecretService.getOpenSecretByAppSecret(appSecret);
                   openTokenPo.setSecret(openSecretPo);
               }
               openTokenPo.setAccessToken(accessToken);
               openTokenPo.setExpiredTime(new Date(openAccessToken.getTimeStamp() + openAccessToken.getExpiredTime()));
               openTokenPo.setUpdateTime(openTokenPo.getCreateTime());
               openTokenService.saveOrUpdateOpenToken(openTokenPo);
           }
        }).start();

        return openRefreshToken;
    }

    /**
     *
     * @param appKey
     * @param appSecret
     * @param refreshToken 刷新令牌
     * @return
     * @throws AuthorizeException
     * @Desription 以下:AccessToken表示服务器端OpenAccessToken对象,accesstoken表示返回给客户端的令牌
     *               刷新accesstoken,通过重置TimeStamp,重新生成accesstoken完成
     *               未使用如果accesstoken未超时则续期,accsstoken值不变,超时则重新生成accesstoken的逻辑
     *               对于新生成的accesstoken,需要立即保存到redis等待用户请求
     *               对于accesstoken刷新带来的redis中删除旧的OpenAccessToken,更新redis中OpenRefreshToken(因为嵌套),
     *               更新DB中OpenToken记录,可以异步执行。
     */
    @Override
    public OpenRefreshToken refreshAccessToken(String appKey, final String appSecret, final String refreshToken) throws AuthorizeException {

        OpenRefreshToken openRefreshToken = validatorService.validateRefreshToken(appKey, appSecret, refreshToken);
        final OpenAccessToken requiredRefreshOpenAccessToken = (OpenAccessToken) openRefreshToken.getAccessToken();
        final String oldAccessToken = requiredRefreshOpenAccessToken.getToken();
        requiredRefreshOpenAccessToken.setTimeStamp(System.currentTimeMillis());
        final String newAccessToken = requiredRefreshOpenAccessToken.generate();
        openRefreshToken.setAccessToken(requiredRefreshOpenAccessToken);
        cacheService.removeOpenAccessToken(oldAccessToken);
        cacheService.setOpenAccessToken(newAccessToken, requiredRefreshOpenAccessToken);
        cacheService.setOpenRefreshToken(refreshToken, openRefreshToken);

        new Thread(new Runnable()
        {
            @Override
            public void run() {
                OpenTokenPo openTokenPo = openTokenService.getOpenTokenByAppSecret(appSecret);
                openTokenPo.setAccessToken(newAccessToken);
                openTokenPo.setUpdateTime(new Date(requiredRefreshOpenAccessToken.getTimeStamp()));
                openTokenPo.setExpiredTime(new Date(requiredRefreshOpenAccessToken.getTimeStamp() + requiredRefreshOpenAccessToken.getExpiredTime()));
                openTokenService.saveOrUpdateOpenToken(openTokenPo);
            }
        }).start();

        return openRefreshToken;
    }
}

 

 

App开放接口API 安全性:Token签名sign的设计与实现

在App开放接口api的设计中,避免不了的就是安全性问题,因为大多数接口涉及到用户的个人信息以及一些敏感的数据,所以对这些接口需要进行身份的认证,那么这就需要用户提供一些信息,比如用户名密码等,但是为了安全起见让用户暴露的明文密码次数越少越好,我们一般在web项目中,大多数采用保存的session中,然后在存一份到cookie中,来保持用户的回话有效性。但是在app提供的开放接口中,后端服务器在用户登录后如何去验证和维护用户的登陆有效性呢,以下是参考项目中设计的解决方案,其原理和大多数开放接口安全验证一样,如淘宝的开放接口token验证,微信开发平台token验证都是同理。

 

一、签名设计

对于敏感的api接口,需使用https协议

https是在http超文本传输协议加入SSL层,它在网络间通信是加密的,所以需要加密证书。

https协议需要ca证书,一般需要交费。

原理:用户登录后向服务器提供用户认证信息(如账户和密码),服务器认证完后给客户端返回一个Token令牌,用户再次获取信息时,带上此令牌,如果令牌正取,则返回数据。对于获取Token信息后,访问用户相关接口,客户端请求的url需要带上如下参数:

时间戳:timestamp

Token令牌:token

然后将所有用户请求的参数按照字母排序(包括timestamp,token),然后更具MD5加密(可以加点盐),全部大写,生成sign签名,这就是所说的url签名算法。然后登陆后每次调用用户信息时,带上sign,timestamp,token参数。

例如:原请求https://www.andy.cn/api/user/update/info.shtml?city=北京 (post和get都一样,对所有参数排序加密)

加上时间戳和token

 https://www.andy.cn/api/user/update/info.shtml?city=北京&timestamp=12445323134&token=wefkfjdskfjewfjkjfdfnc

然后更具url参数生成sign

最终的请求如

https://www.andy.cn/api/user/update/info.shtml?city=北京×tamp=12445323134&token=wefkfjdskfjewfjkjfdfnc&sign=FDK2434JKJFD334FDF2

 

二、具体实现

其最终的原理是减小明文的暴露次数;保证数据安全的访问。

1. api请求客户端想服务器端一次发送用用户认证信息(用户名和密码),服务器端请求到改请求后,验证用户信息是否正确。

如果正确:则返回一个唯一不重复的字符串(一般为UUID),然后在Redis(任意缓存服务器)中维护Token----Uid的用户信息关系,以便其他api对token的校验。

如果错误:则返回错误码。

 

2.服务器设计一个url请求拦截规则

(1)判断是否包含timestamp,token,sign参数,如果不含有返回错误码。

(2)判断服务器接到请求的时间和参数中的时间戳是否相差很长一段时间(时间自定义如半个小时),如果超过则说明该    url已经过期(如果url被盗,他改变了时间戳,但是会导致sign签名不相等)。

(3)判断token是否有效,根据请求过来的token,查询redis缓存中的uid,如果获取不到这说明该token已过期。

(4)根据用户请求的url参数,服务器端按照同样的规则生成sign签名,对比签名看是否相等,相等则放行。(自然url签名  也无法100%保证其安全,也可以通过公钥AES对数据和url加密,但这样如果无法确保公钥丢失,所以签名只是很大程  度上保证安全)。

(5)此url拦截只需对获取身份认证的url放行(如登陆url),剩余所有的url都需拦截。

 

3.Token和Uid关系维护

对于用户登录我们需要创建token--uid的关系,用户退出时需要需删除token--uid的关系。

 

三、签名实现

获取全部请求参数

String sign = request.getParameter("sign");
Enumeration<?> pNames =  request.getParameterNames();
Map<String, Object> params = new HashMap<String, Object>();

while (pNames.hasMoreElements()) {
  String pName = (String) pNames.nextElement();
  if("sign".equals(pName))continue;
  Object pValue = request.getParameter(pName);
  params.put(pName, pValue);
}   

 

生成签名

public static String createSign(Map<String, String> params, boolean encode) throws UnsupportedEncodingException {
    Set<String> keysSet = params.keySet();
    Object[] keys = keysSet.toArray();
    Arrays.sort(keys);
    StringBuffer temp = new StringBuffer();
    boolean first = true;
    for (Object key : keys) {
      if (first) {
        first = false;
      } else {
        temp.append("&");
      }
      temp.append(key).append("=");
      Object value = params.get(key);
      String valueString = "";
      if (null != value) {
        valueString = String.valueOf(value);
      }
      if (encode) {
        temp.append(URLEncoder.encode(valueString, "UTF-8"));
      } else {
        temp.append(valueString);
      }
    }
 
    return MD5Utils.getMD5(temp.toString()).toUpperCase();
}

 

 

App开放接口api安全性:防腾讯签名sign的设计与实现

接口API 裸奔时代已结束,企业级高可用api设防腾讯sign设计与实现。。。 
 

设计起来就是头冷。。。

有三个关键点:接口参数加密+时效性验证+私钥

首先定义几个参数:

int age = 0;
String app_id = "123";
String name = "撒旦法";
String nonce_str = _stringUtil.getCharAndNumr(16, 3);
String sign = "";
String time_stamp = _timeUtil.getTimeStamp();
String app_key = "123456";

就是一个姓名为XX年龄为Y的appid用户在time_stamp这个时间拿着app_key钥匙nonce_str 随机打开sign 门,然后偷窃。。。

偷窃未遂,这很安全吧。。。

首先要确定偷哪个房屋,sign门需求确定。

sign = YhSignSort.getSignature(map);
// System.out.println("生成的sign参数:" + sign);

怎么确定?偷了穷的人家怎么办,我们是劫富济贫的,所以需要调研。。。 
算法实现

签名算法采用MD5摘要方式实现,步骤如下:

1、将参数对按key进行字典升序排序,得到有序的参数对列表N

2、将列表N中的参数对按URL键值对的格式拼接成字符串,得到字符串T(如:key1=val1&key2=val2),值使用URL编码

3、将应用密钥以app_key为键名,组成URL键值拼接到字符串T末尾,得到字符串S(如:key1=val1&key2=val2&app_key=密钥)

4、对字符串S进行MD5摘要计算,将得到的md5值转换成大写,最终得到接口请求签名

 

代码实现:

String secret = "123456";  //把secret加入进行加密 MD5盐值加密app_id 这里很多加密算法 现在假设加密结果是123456

boolean a = validateTimeStamp(time_stamp);
if (a) {
    System.out.println("time_stamp参数无效"); //请检查time_stamp距离当前时间是否超过5分钟
}

//      map.put("nonce_str", "");
//      map.put("age", "");
//      map.put("app_id", "123");
if("".equals(map.get("app_id"))){
    System.out.println("缺少app_id参数");
}
else if(!"123".equals(map.get("app_id"))){
    System.out.println("appid应用不存在");
}
else if("".equals(map.get("time_stamp"))){
    System.out.println("缺少time_stamp参数");
}
else if("".equals(map.get("nonce_str"))){
    System.out.println("缺少nonce_str参数");
}
else if(validateSign(map,sign))
{
     if("123456".equals(secret)){
            System.out.println("验证成功");
        }else{
            System.out.println("缺失API权限");
        }
} else {
    System.out.println("请求签名无效");
}

validateTimeStamp方法是验证失效性

validateSign方法是验证签名正确性

一个app_id对应多个应用,只要app_key正确,可以访问,否则访问失败。app_key是根据app_id 
加密加密生成,可以怎么生成也行,只要不被破解,或告诉别人,也可以重新生成。

 

 

参考推荐

前后端 API接口常见的几种鉴权方式

认证(authentication)和授权(authorization)的几种方式总结

Nginx 配置文件添加 http 授权

10多款支持二次验证的网络服务

LastPass 跨平台密码管理工具

Linux expect 命令无需输入密码登陆

ssh + sshpass 自动输入密码登录服务器