网关、开放平台如何设计appKey,appSecret,accessToken的生成和校验机制
总述
在开放平台或者网关中,经常会见到appKey,appSecret和accessToken,这是用来对openApi访问的一种授权机制。一般分为调用方应用和发布方API,发布了API以后,是用来调用的。如果想调用API的话,需要创建一个调用方应用,同时会颁发一对appKey以及appSecret,前者是公开的,这就是你的唯一身份认证的,后者是**,一般不会公开,后续会用于加签,而且一般情况下也会支持重置。同时你这个应用可能需要购买申请想调用的API,当然也有免费的,其实就是说这个应用需要先得到某些API的授权才能够调用。真正调用的时候,一般需要两步:
- 使用appKey等参数按照平台的要求调用获取token的接口,获取的token一般会有有效期。
- 带着token然后去访问具体的openApi。然后平台通过token就能够识别到你的应用信息,去判断你的授权情况等等。
从上面就可以看出平台里面需要管理token,负责token的生成和验证。并且在一些情况下,可能会同时存在多种的token类型,比如通过账号密码登录获取token,然后再去调用API,当然这个可能需要用户的API权限管理功能才行。
本文分享一下这方面的设计以及具体实现。
需求
本文的所有都建立在如下的需求中,可能和大家所见到的会有所出入,本文只是举例子,不过思想应该可以借鉴。
- 关键参数只有appKey,appSecret和accessToken。(有的平台可能有appId等参数)。
- 获取token的时候,需要提供appKey和一个随机字符串random(可以限制长度),还有一个是以appKey+random,通过appSecret使用SHA256签名算法得到的一个签名sign。总共3个参数。
- 获取的token是有有效期的,但如果在有效期内同一个appKey再获取的话,会直接返回已经存在的token,并且不会刷新它的有效时间。当它失效并且再次获取的时候会生成新的。
- 调用API的时候仅传一个token参数,也就是仅通过token要能够查询到调用方的身份(token本身没有含义,但它映射了一些身份参数,比如appKey),然后通过appKey再去进行授权验证等等。
本文后面的讲解都是按照如上的需求去设计和实现,但实际情况中,特别是第2个和第3个,可能会稍微不太一样,同时我分享的时候也对模型进行了一个简化,但设计思想是存在的。里面一些参数等可以根据实际情况进行调整。
整体设计
token管理分为3层,服务层、管理层和存储层。如下图:
- 服务层:对外提供服务的,这个比较简单。
- token管理:它负责token的整个生命周期,从获取token的时候参数校验,延签,然后生成以及调用API的时候对token的校验。它的重点职责是要规划token的分配原则,比如一个appKey对应一个token,还是一个userId对应一个token,同时生产的这个token需要映射什么参数,它也负责提供。而且每一种token都有这么一种管理对象,但管理对象本身不负责存储,交给下一级。
- token存储:它仅负责对token进行存储和查询。用什么来存储token,它本身不关心,由token管理层负责。同时它也不关心token映射了什么业务参数,由管理层提供。它只负责如何存储。
这种职责的设计原因如下:
- 可能会有多种token,每一种token的映射关系都是不确定的,比如就是appKey->token,token->appKey(甚至更多的参数),又或者是userId->token,token->userId。但是把他们抽象一下,就可以得到:xxx->token,token->xxx这样一个关系。
- 虽然有多种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里面的数据。
结尾
虽然有很多代码,但只是分享一下设计思想吧,毕竟实际情况中可能会有很多差异的。
上一篇: 图的遍历 - 邻接矩阵 - 深搜与广搜
下一篇: 在Unity中使用四叉树算法绘制地形