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

总述

在开放平台或者网关中,经常会见到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层,服务层、管理层和存储层。如下图:
在这里插入图片描述

  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,但是其实他们的存储方式可能是一模一样的,只不过里面存储的映射关系可能会有所不同,但是这一层可以完全不关心,只负责存储即可。而且如果有多种存储方式的话,抽出这一层也可以解决,虽然我没有遇见过。

基于这种设计,可以看一下整体的一个类图:
在这里插入图片描述
很简单,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管理

直接来一个完整的类图吧
在这里插入图片描述
类图应该还是比较清晰了,管理和存储分别实现了一种。同时管理本身也依赖了存储。
贴一下代码,先是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,同时也得有数据,就先手动插一条吧,比如:
在这里插入图片描述
我自己比较喜欢用hash,用string或许也行吧。
最后贴一下测试成功的截图吧
在这里插入图片描述
这是获取token成功的截图。
在这里插入图片描述
这是通过token查到了对应的参数。
在这里插入图片描述
这是最终redis里面的数据。

结尾

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

猜你喜欢

转载自blog.csdn.net/ywg_1994/article/details/103518806