欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

网关、开放平台如何设计appKey,appSecret,accessToken的生成和校验机制

程序员文章站 2022-03-26 10:36:47
...

总述

在开放平台或者网关中,经常会见到appKey,appSecret和accessToken,这是用来对openApi访问的一种授权机制。一般分为调用方应用和发布方API,发布了API以后,是用来调用的。如果想调用API的话,需要创建一个调用方应用,同时会颁发一对appKey以及appSecret,前者是公开的,这就是你的唯一身份认证的,后者是**,一般不会公开,后续会用于加签,而且一般情况下也会支持重置。同时你这个应用可能需要购买申请想调用的API,当然也有免费的,其实就是说这个应用需要先得到某些API的授权才能够调用。真正调用的时候,一般需要两步:

  1. 使用appKey等参数按照平台的要求调用获取token的接口,获取的token一般会有有效期。
  2. 带着token然后去访问具体的openApi。然后平台通过token就能够识别到你的应用信息,去判断你的授权情况等等。

从上面就可以看出平台里面需要管理token,负责token的生成和验证。并且在一些情况下,可能会同时存在多种的token类型,比如通过账号密码登录获取token,然后再去调用API,当然这个可能需要用户的API权限管理功能才行。
本文分享一下这方面的设计以及具体实现。

需求

本文的所有都建立在如下的需求中,可能和大家所见到的会有所出入,本文只是举例子,不过思想应该可以借鉴。

  1. 关键参数只有appKey,appSecret和accessToken。(有的平台可能有appId等参数)。
  2. 获取token的时候,需要提供appKey和一个随机字符串random(可以限制长度),还有一个是以appKey+random,通过appSecret使用SHA256签名算法得到的一个签名sign。总共3个参数。
  3. 获取的token是有有效期的,但如果在有效期内同一个appKey再获取的话,会直接返回已经存在的token,并且不会刷新它的有效时间。当它失效并且再次获取的时候会生成新的。
  4. 调用API的时候仅传一个token参数,也就是仅通过token要能够查询到调用方的身份(token本身没有含义,但它映射了一些身份参数,比如appKey),然后通过appKey再去进行授权验证等等。

本文后面的讲解都是按照如上的需求去设计和实现,但实际情况中,特别是第2个和第3个,可能会稍微不太一样,同时我分享的时候也对模型进行了一个简化,但设计思想是存在的。里面一些参数等可以根据实际情况进行调整。

整体设计

token管理分为3层,服务层、管理层和存储层。如下图:
网关、开放平台如何设计appKey,appSecret,accessToken的生成和校验机制

  1. 服务层:对外提供服务的,这个比较简单。
  2. token管理:它负责token的整个生命周期,从获取token的时候参数校验,延签,然后生成以及调用API的时候对token的校验。它的重点职责是要规划token的分配原则,比如一个appKey对应一个token,还是一个userId对应一个token,同时生产的这个token需要映射什么参数,它也负责提供。而且每一种token都有这么一种管理对象,但管理对象本身不负责存储,交给下一级。
  3. token存储:它仅负责对token进行存储和查询。用什么来存储token,它本身不关心,由token管理层负责。同时它也不关心token映射了什么业务参数,由管理层提供。它只负责如何存储。

这种职责的设计原因如下:

  1. 可能会有多种token,每一种token的映射关系都是不确定的,比如就是appKey->token,token->appKey(甚至更多的参数),又或者是userId->token,token->userId。但是把他们抽象一下,就可以得到:xxx->token,token->xxx这样一个关系。
  2. 虽然有多种token,但是其实他们的存储方式可能是一模一样的,只不过里面存储的映射关系可能会有所不同,但是这一层可以完全不关心,只负责存储即可。而且如果有多种存储方式的话,抽出这一层也可以解决,虽然我没有遇见过。

基于这种设计,可以看一下整体的一个类图:
网关、开放平台如何设计appKey,appSecret,accessToken的生成和校验机制
很简单,token管理和token存储两个接口,分别可以有多种实现。token管理的不同实现代表了不同的token,而token存储的不同实现代表了不同的存储方式,比如上面描述的需求和“每次获取都是新的token,并且之前的也不过期”,又或者是用redis实现或者数据库以及内存等等。这其实是两个维度的事情了。
看一下这两个的接口的具体定义吧,上面有注释,应该比较清晰:

/**
 * @Description token存储
 **/
public interface TokenDao {
    /**
     * 生成或查询token
     * @param key   唯一用来识别token的key,
     * @param values token对应的业务参数。
     * @return 返回token对象。
     */
    Token buildOrQuery(String key, Map<String,String> values);

    /**
     *查询token,查询token对应的业务参数。
     * @param token
     * @return
     */
    Map<String,String> tokenQuery(String token);
}
/**
 * @Description token管理对象,负责管理token的生命周期,负责验签,构建以及校验。
 **/
public interface TokenManage<T,R> {
    /**
     * 构建token。
     * @param getTokenParam 这个是用于构建token的参数,对应不同类型的token,会有不同的参数,具体的由实现类进行指定。
     * @return 返回构建的token。
     * @throws Exception 验签失败的话会抛出异常。
     */
    public Token build(T getTokenParam) throws Exception;

    /**
     *token校验。同时会返回这个token对应的业务参数。
     * @param token 用户输入的token值。
     * @return  返回值是token对应的业务参数。
     */
    public R validate(String token);
}

再看一下token对象

@Setter
@Getter
@NoArgsConstructor
public class Token {
    //生成时间,ms时间戳
    private long generateTime;
    //过期时间,ms时间戳
    private long expireTime;
    //有效期,ms
    private long expireIn;
    //令牌
    private String accessToken;

    public static Token createToken(long expireIn){
        Token token=new Token();
        token.setAccessToken(UUID.randomUUID().toString().replace("-",""));
        token.setGenerateTime(System.currentTimeMillis());
        token.setExpireIn(expireIn);
        token.setExpireTime(token.getGenerateTime()+expireIn);
        return token;
    }

干货要来了。如果忘了我要做什么,可以看看前面的需求。

appKey的token管理

直接来一个完整的类图吧
网关、开放平台如何设计appKey,appSecret,accessToken的生成和校验机制
类图应该还是比较清晰了,管理和存储分别实现了一种。同时管理本身也依赖了存储。
贴一下代码,先是RedisTokenDao,

public class RedisTokenDao implements TokenDao {
    private RedisUtil redisUtil=RedisUtil.getInstance();
    private long expireIn=2*3600*1000L;
    @Override
    public Token buildOrQuery(String key, Map<String, String> values) {
        Token token;
        Map<String,String> tokenMap=redisUtil.hgetall(key);
        if (tokenMap==null||tokenMap.size()==0){
            token=Token.createToken(expireIn);
            tokenMap=new HashMap<>();
            tokenMap.put("generateTime",String.valueOf(token.getGenerateTime()));
            tokenMap.put("expireTime",String.valueOf(token.getExpireTime()));
            tokenMap.put("expireIn",String.valueOf(token.getExpireIn()));
            tokenMap.put("accessToken",String.valueOf(token.getAccessToken()));
            redisUtil.hmsetWithTime(key,tokenMap,expireIn);
            redisUtil.hmsetWithTime(getTokenKey(token.getAccessToken()),values,expireIn);
        }else{
            token=JSONObject.parseObject(JSONObject.toJSONString(tokenMap),Token.class);
        }
        return token;
    }

    @Override
    public Map<String, String> tokenQuery(String token) {
        return redisUtil.hgetall(getTokenKey(token));
    }

    private String getTokenKey(String token){
        return "token:"+token;
    }

里面的redisUtil类就不贴了,也比较简单。
这里面重点是存储逻辑。

接下来是:AppTokenManage

/**
 * @Description 应用token管理对象
 **/
public class AppTokenManage implements TokenManage<AppTokenGetParam,AppParam> {
    private TokenDao tokenDao;

    public AppTokenManage() {
        this.tokenDao=new RedisTokenDao();
    }

    @Override
    public Token build(AppTokenGetParam getTokenParam) throws Exception {
        signValidate(getTokenParam);
        String key=getSaveKey(getTokenParam);
        AppParam param=new AppParam();
        param.setAppKey(getTokenParam.getAppKey());
        return tokenDao.buildOrQuery(key,(Map<String,String>)JSONObject.toJSON(param));
    }

    @Override
    public AppParam validate(String token) {
        Map<String,String> saveValues=tokenDao.tokenQuery(token);
        if (saveValues!=null&&saveValues.size()>0){
            return JSONObject.parseObject(JSONObject.toJSONString(saveValues),AppParam.class);
        }
        return null;
    }

    private void signValidate(AppTokenGetParam getTokenParam) throws Exception {
        String sign=SHA256Util.sign(getTokenParam.getAppKey()+getTokenParam.getRandom(),getTokenParam.getAppSecret());
        if (!sign.equals(getTokenParam.getSign())){
            throw new Exception("签名验证失败");
        }
    }

    private String getSaveKey(AppTokenGetParam getTokenParam){
        return "appKey-token:"+getTokenParam.getAppKey();
    }
}

这个里面对获取token的参数进行了校验,并且提供了用什么key去存储token,并且是token映射了什么参数。
顺便再看一下两个参数:

public class AppTokenGetParam {
    private String appKey;
    private String random;
    private String sign;
    private String appSecret;
}

public class AppParam {
    private String appKey;
}

还是比较简单的。

最后看一下如何来用的:

public class AppTokenService {
    private AppTokenManage appTokenManage;
    private RedisUtil redisUtil;

    public AppTokenService() {
        this.appTokenManage=new AppTokenManage();
        this.redisUtil=RedisUtil.getInstance();
    }

    public Token buildToken(String appKey, String random, String sign) throws Exception {
        //校验参数不能为空。
        String appSecret=validateApp(appKey);
        AppTokenGetParam appTokenGetParam=new AppTokenGetParam();
        appTokenGetParam.setAppKey(appKey);
        appTokenGetParam.setAppSecret(appSecret);
        appTokenGetParam.setRandom(random);
        appTokenGetParam.setSign(sign);
        Token token=appTokenManage.build(appTokenGetParam);
        return token;
    }

    /**
     * 校验appKey的合法性,同时返回这个app相关的一些属性,比如appSecret。
     * @param appKey
     * @return
     */
    private String validateApp(String appKey) throws Exception {
        Map<String,String> appInfo=redisUtil.hgetall("appKey:"+appKey);
        if (appInfo!=null&&appInfo.size()>0){
            return appInfo.get("appSecret");
        }else{
            throw new Exception("appKey不存在");
        }
    }

这个里面有一部分校验appKey的逻辑,必须是合法的才行。

跑一跑,验证一下

最后写了测试代码,做了一下验证,这段代码只是简单测试,正常应该是调用API的逻辑。

    /**
     * 调用方去获取token。
     */
    @Test
    public void getToken() throws Exception {
        String appKey="1a748b70d0cb4a8a8e37e959f4a4f1e6";
        String appSecret="0c128efdc2ef45c1a7be39462701e65b";
        String random=UUID.randomUUID().toString();
        String sign=SHA256Util.sign(appKey+random,appSecret);

        //这边应该是API的调用,而不是方法调用,简单模拟一下就行了。
        AppTokenService appTokenService=new AppTokenService();
        Token token=appTokenService.buildToken(appKey,random,sign);
        System.out.println(JSONObject.toJSONString(token));
    }


    /**
     * 模拟API调用
     */
    @Test
    public void businssCall() throws Exception {
        String accessToken="9a07e90961a94e11b5ab110b9d1b7905";
        AppTokenManage appTokenManage=new AppTokenManage();
        AppParam appParam=appTokenManage.validate(accessToken);
        System.out.println(JSONObject.toJSONString(appParam));

        //拿到token对应的参数,接下来就可以进一步处理了。TODO
    }

注意做这个测试的时候,肯定得有个redis,同时也得有数据,就先手动插一条吧,比如:
网关、开放平台如何设计appKey,appSecret,accessToken的生成和校验机制
我自己比较喜欢用hash,用string或许也行吧。
最后贴一下测试成功的截图吧
网关、开放平台如何设计appKey,appSecret,accessToken的生成和校验机制
这是获取token成功的截图。
网关、开放平台如何设计appKey,appSecret,accessToken的生成和校验机制
这是通过token查到了对应的参数。
网关、开放平台如何设计appKey,appSecret,accessToken的生成和校验机制
这是最终redis里面的数据。

结尾

虽然有很多代码,但只是分享一下设计思想吧,毕竟实际情况中可能会有很多差异的。