笔者多年从事移动客户端开发领域,同时对于服务端开发技术也在持续关注和研究。前后端分离技术我们已经讨论过多年,也有很多实践。其实移动开发自诞生起就天然是前后端分离的架构,因为涉及到前后台数据的通讯,所以我们会借助一种中间数据格式来完成前后端数据对接,对接过程主要包含数据的包装,传递和解析。在Json之前,我们通常会采用xml作为数据封装格式。随之restful架构风格的兴起,json的运用也越来越广泛,目前企业级开发中已难觅xml和基于soap服务的webservice身影。但是无论是restful/json风格还是soap/xml架构,其数据传递都是无状态的,一起数据请求是不需要依赖于cookie或session这种客户端或服务端存储技术的。
动态token可以解决哪些问题
- 可以解决服务调用用户调用的权限问题
我们都知道,服务端api的发布的无状态性决定了其任意接口方法可以任意被某客户端调用,没有顺序而言。如果某一客户端是恶意调用用来窃取敏感数据,通常我们会在调用接口的传递参数里加上用户身份信息,例如用户名和密码。如果每个api接口请求带上用户名和密码,且不说会承担用户账号信息泄露的风险,从设计角度来看,这对于服务api接口来说也是业务无关的,违背了程序设计中关注点分离原则。如果将来api的权限验证机制更改那么维护修改的成本还是非常高的。
- 可以解决服务调用的时效性问题
时效性问题其实也包含安全性问题,通常我们设计token时是会同时赋予这个token有效期的。过了这个有效期,这个token就已经失效了,这样夹带这个token来获取api数据也是不成功的。有人说,那我们是否可以将token设置成没有有效期呢?我们想一下,如果一个token永久有效,时间持续越长这个token泄露的风险就越高,这是一个线性增长。如果将来有人拿到了这个token,那他就获得了api访问的永久权限,这显然是不合适的。例如,我们如果将一个token设置为2小时失效,即使在两小时内泄露了这个token,那么调用api的权限也仅在两小时之内。
- 可以解决用户身份传递问题
传统的api请求,获取用户数据一般依赖于用户身份的传递,如在参数中加上用户的id字段。如果我们在参数中移除这个字段,也能获取用户的数据是不是也很不错。借助token,我们同样可以做到。一般会将用户的id封装到token里加密。在使用时,解析出token拿到用户数据。在传递给api方法,在实现层面途径有很多,会在下面的章节中介绍到。
token的设计
需要思考几个问题,分别是token何时产生,token的基本构成和token的加解密。
- token何时生成
将系统的登录入口作为token产生的关键点。用户登录一般会采用用户名/密码,手机号/验证码的方式来登录,这个一般能够确认用户身份的合法性。
- token的构成
一般包含用户身份信息,有效期,生产日期。
- token的加解密
采用AES非对称加密token,这里token的加解密只会在服务端进行,在私钥不被泄露的情况下破解token的难度还是非常大的。
- token的验证
首先验证token的合法性,如何token解析有问题直接判断非法。通过私钥对token进行解密,将产生时间戳和有效期相加与当前时间戳对比,比当前时间大则判定合法。这个过程,一般我们通过过滤器来对调用api的request进行检查,所以我们需要定义好哪些api路径或方法需要被执行token的有效期验证。
- token的传递
在客户端请求api服务时,将token添加到http请求头中作为authration进行传递。
关键代码
Token.java
/**
* @author liyc
* @date 2017年12月8日 下午3:17:02
* @description token对象
*/
public class Token {
String userid;
Long timestamp;
Long period;
public Token(String userid, Long timestamp, Long period) {
super();
this.userid = userid;
this.timestamp = timestamp;
this.period = period;
}
public String getUserid() {
return userid;
}
public void setUserid(String userid) {
this.userid = userid;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
public Long getPeriod() {
return period;
}
public void setPeriod(Long period) {
this.period = period;
}
@Override
public String toString() {
return this.userid+";"+timestamp+";"+period;
}
}
TokenMaker.java
/**
* @author liyc
* @date 2017年12月8日 下午3:18:21
* @description 负责token的生成,合法性验证,解析
*/
public class TokenMaker {
/**
* 产生一个新的token
*
* @param token
* @param secretKey
* @return
* @throws TokenException
*/
public static String newToken(Token token, String secretKey) throws TokenException {
try {
byte[] b = AEScrypto.encrypt(token.toString(), secretKey);
String result = Base64.encodeBase64String(b);
return result;
} catch (Exception e) {
throw new TokenException();
}
}
/**
* 解析token
*
* @param tokenStr
* @param password
* @return
* @throws TokenException
*/
public static Token resolve(String tokenStr, String password) throws TokenException {
try {
byte[] origionResult = Base64.decodeBase64(tokenStr);
byte[] decryResult = AEScrypto.decrypt(origionResult, password);
String origionStr = new String(decryResult);
String[] ss = origionStr.split(";");
Token token = new Token(ss[0], Long.parseLong(ss[1]), Long.parseLong(ss[2]));
return token;
} catch (Exception e) {
throw new TokenException();
}
}
/**
* 判断token是否合法
*
* @param tokenStr
* @param password
* @return
* @throws TokenException
*/
public static boolean isLegal(String tokenStr, String password) throws TokenException {
Token token = resolve(tokenStr, password);
long nowTimeStamp = new Date().getTime();
long endTimeStamp = token.getTimestamp() + token.getPeriod() * 1000;
return endTimeStamp > nowTimeStamp;
}
}