基于动态token加解密验证的实现方案

4,035 阅读5分钟

笔者多年从事移动客户端开发领域,同时对于服务端开发技术也在持续关注和研究。前后端分离技术我们已经讨论过多年,也有很多实践。其实移动开发自诞生起就天然是前后端分离的架构,因为涉及到前后台数据的通讯,所以我们会借助一种中间数据格式来完成前后端数据对接,对接过程主要包含数据的包装,传递和解析。在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;
	}
}