为什么使用Token验证:
在Web领域基于Token的身份验证随处可见。在大多数使用Web API的互联网公司中,token是多用户下处理认证的最佳方式。
以下几点特性会让你在程序中使用基于Token的身份验证
1.无状态、可扩展
2.支持移动设备
3.跨程序调用
4.安全
那些使用基于Token的身份验证的大佬们
大部分你见到过的API和Web应用都使用tokens。例如Facebook, Twitter, Google+, GitHub等。
Token的起源
在介绍基于Token的身份验证的原理与优势之前,不妨先看看之前的认证都是怎么做的。
基于服务器的验证(移动端的兴起,Session辨别已经不满足于现状)
我们都是知道HTTP协议是无状态的,这种无状态意味着程序需要验证每一次请求,从而辨别客户端的身份。在这之前,程序都是通过在服务端存储的登录信息来辨别请求的。这种方式一般都是通过存储Session来完成。
随着Web,应用程序,已经移动端的兴起,这种验证的方式逐渐暴露出了问题。尤其是在可扩展性方面。
基于服务器验证方式暴露的一些问题(目前的技术满足不了需求)
1.Seesion:每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加。
2.可扩展性:在服务端的内存中使用Seesion存储登录信息,伴随而来的是可扩展性问题。
3.CORS(跨域资源共享):当我们需要让数据跨多台移动设备上使用时,跨域资源的共享会是一个让人头疼的问题。在使用Ajax抓取另一个域的资源,就可以会出现禁止请求的情况。
4.CSRF(跨站请求伪造):用户在访问银行网站时,他们很容易受到跨站请求伪造的攻击,并且能够被利用其访问其他的网站。
在这些问题中,可扩展行是最突出的。因此我们有必要去寻求一种更有行之有效的方法。
基于Token的验证原理
基于Token的身份验证是无状态的,我们不将用户信息存在服务器或Session中。
这种概念解决了在服务端存储信息时的许多问题,NoSession意味着你的程序可以根据需要去增减机器,而不用去担心用户是否登录。基于Token的身份验证的过程如下:
1.用户通过用户名和密码发送请求。
2.程序验证。
3.程序返回一个签名的token 给客户端。
4.客户端储存token,并且每次用于每次发送请求。
5.服务端验证token并返回数据。
每一次请求都需要token。token应该在HTTP的头部发送从而保证了Http请求无状态。我们同样通过设置服务器属性Access-Control-Allow-Origin:* ,让服务器能接受到来自所有域的请求。需要主要的是,在ACAO头部标明(designating)*时,不得带有像HTTP认证,客户端SSL证书和cookies的证书。
如何去使用token:
注:(笔者使用的是SSM框架,使用了redis来做缓存)
一、我们需要去生成一个token
token组成:统一的前缀,判断登录的设备,用户的名称,登录的日期,加盐
private static final String tokenPrefix = "toekn"; //统一前缀表示符 @Resource //private RedisAPI redisAPI; //private int expire = SESSION_TIMEOUT; private Logger logger = Logger.getLogger(this.getClass()); @Override //生成token的方法 public String generateToken(String agent, ItripUser user) { try { //UserAgentUtil可检测客户端类型,因为要根据客户端类型生成不同的Token UserAgentInfo userAgentInfo = UserAgentUtil.getUasParser().parse(agent); StringBuilder sb = new StringBuilder(); sb.append(tokenPrefix); //统一前缀 if (userAgentInfo.getDeviceType().equals(UserAgentInfo.UNKNOWN)) { //如果是未知客户类型 if (UserAgentUtil.CheckAgent(agent)) { sb.append("MOBILE-"); //如果包含移动设备的关键字,拼接移动设备的Token前缀 } else { sb.append("PC-"); //除了移动设备就是PC端 } } else if (userAgentInfo.getDeviceType().equals("Personal computer")) { //如果很明显是PC设备 sb.append("PC-"); //很明显是PC端添加这个Token前缀 } else { sb.append("MOBILE-"); //拼接移动设备的Token前缀 } sb.append(MD5.getMd5(user.getUserCode(), 32)); //拼接加密用户名称 sb.append(user.getId() + "-"); //拼接user id和日期 sb.append(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + "-"); sb.append(MD5.getMd5(agent, 6)); //再拼接6个字符的密码 return sb.toString(); } catch (IOException e) { e.printStackTrace(); } return null; }
二、判断token是否需要置换,移动端不置换,PC端置换,置换时间自定义
判断使用的设备
public void save(String token, ItripUser user) { if (token.startsWith(tokenPrefix + "PC-")) { //如果客户端是pc端,则设置Token过期时间 redisAPI.set(token, expire, JSON.toJSONString(user)); } else { //如果是客户端,则永不过期 redisAPI.set(token, JSON.toJSONString(user)); } }
验证token是否有效
/** *验证token是否有效: * 1、token在redis是否存在, * token是否过期 *返回false */ public boolean validate(String agent, String token) { boolean tokenValid = false; //该变量表示token是否有效 if (!redisAPI.exist(token)) {//2、如果token在redis中不存在 return tokenValid; } String[] tokenDetails = token.split("-"); //按"-"拆分toekn字符串 SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss"); try { Date TokenGentime = formatter.parse(tokenDetails[3]); //还原token生成时间 long passed = Calendar.getInstance().getTimeInMillis() - TokenGentime.getTime(); if (passed > this.SESSION_TIMEOUT * 1000) //如果TOKEN过期 return tokenValid; //返回false; String agentMD5 = tokenDetails[4]; if (MD5.getMd5(agent, 6).equals(agentMD5)) { //验证tokend的6位密码 tokenValid = true; //若果token中的密码一致,token有效 } } catch (ParseException e) { e.printStackTrace(); } return tokenValid; }
置换toekn
public String ReplaceToken(String agent, String token) throws TokenValidationFailedException { //验证旧Token是否有效 if (!redisAPI.exist(token)) { //token不存在 throw new TokenValidationFailedException("未知的token或者token已过期");//终止置换 } Date TokenGenTime; //Token生成时间 String[] tokenDeatils = token.split("-"); SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss"); try { TokenGenTime = formatter.parse(tokenDeatils[3]); } catch (ParseException e) { logger.error(e); throw new TokenValidationFailedException("token格式错误" + token); } long passed = Calendar.getInstance().getTimeInMillis() - TokenGenTime.getTime(); //计算token生成了多久(毫秒) if (passed < REPLACEMENT_PROTECTION_TIMEOUT * 1000) { //置换保护时间 throw new TokenValidationFailedException("token处于置换保护期间,禁止置换,剩余" + (REPLACEMENT_PROTECTION_TIMEOUT * 1000 - passed) / 1000 + "秒"); } //置换token String newToken = ""; ItripUser user = this.load(token);//根据token加载用户信息 long ttl = redisAPI.ttl(token); //从redis中获取token有效期(剩余秒数) if (ttl > 0 || ttl == -1) { //兼容手机与PC端的token有效期 newToken = this.generateToken(agent, user); //生成新的Token //2分钟后旧Token过期,注意手机端由永久变为2分钟(REPLACEMENT_DELY默认值)后失效 redisAPI.set(token, this.REPLACEMENT_DELAY, JSON.toJSONString(user)); } else { //其他未考虑情况不给予置换 throw new TokenValidationFailedException("当前Token的过期时间异常,禁止置换"); } return newToken; }
删除以及加载token
public ItripUser load(String token) { return JSON.parseObject(redisAPI.get(token), ItripUser.class); }
//从redis中删除token(key) 和用户信息(value) public void delete(String token) { if (redisAPI.exist(token)) { redisAPI.delete(token); } }
附上token interface 以供参考
public interface TokenService { //Token过期时间 Integer SESSION_TIMEOUT=2*60*60; //置换保护时间,Token生成时间至少一个小时才允许置换,防止万一置换攻击服务器 int REPLACEMENT_PROTECTION_TIMEOUT=60*60; //旧Token延迟过期时间 int REPLACEMENT_DELAY=2*60; //默认2min String generateToken(String agent, ItripUser user); void save(String token, ItripUser user); boolean validate(String agent,String token); //验证token是否有效 void delete(String token); String ReplaceToken(String agent,String token)throws TokenValidationFailedException; //置换Token的方法 ItripUser load(String token); }
Controller :
ps:token保存在header中
@Controller @RequestMapping(value = "/api") public class TokenController { @Resource private TokenService tokenService; @RequestMapping(value = "/retoken",method = RequestMethod.POST, produces = "application/json") @ResponseBody public Dto Replace(HttpServletRequest request){ String agent=request.getHeader("user-agent"); String token=request.getHeader("token"); //从请求中获取token try { String newToken=tokenService.ReplaceToken(agent,token);//置换一个新的Token //返回ItripTokenVO ItripTokenVO tokenVO=new ItripTokenVO(newToken,Calendar.getInstance().getTimeInMillis()+ TokenService.SESSION_TIMEOUT*1000, //2小时有效期 Calendar.getInstance().getTimeInMillis() ); return DtoUtil.returnDataSuccess(tokenVO); //返回新的token } catch (TokenValidationFailedException e) { e.printStackTrace(); return DtoUtil.returnFail(e.getMessage(),ErrorCode.AUTH_ACTIVATE_FAILED); } } }
Token的优势
一、无状态、可扩展
在客户端存储的Tokens是无状态的,并且能够被扩展。基于这种无状态和不存储Session信息,负载负载均衡器能够将用户信息从一个服务传到其他服务器上。
如果我们将已验证的用户的信息保存在Session中,则每次请求都需要用户向已验证的服务器发送验证信息(称为Session亲和性)。用户量大时,可能会造成 一些拥堵。
但是不要着急。使用tokens之后这些问题都迎刃而解,因为tokens自己hold住了用户的验证信息。
二、安全性
请求中发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)。即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。
token是有时效的,一段时间之后用户需要重新验证。我们也不一定需要等到token自动失效,token有撤回的操作,通过token revocataion可以使一个特定的token或是一组有相同认证的token无效。
三、可扩展性
Tokens能够创建与其它程序共享权限的程序。例如,能将一个随便的社交帐号和自己的大号(Fackbook或是Twitter)联系起来。当通过服务登录Twitter(我们将这个过程Buffer)时,我们可以将这些Buffer附到Twitter的数据流上(we are allowing Buffer to post to our Twitter stream)。
使用tokens时,可以提供可选的权限给第三方应用程序。当用户想让另一个应用程序访问它们的数据,我们可以通过建立自己的API,得出特殊权限的tokens。
四、多平台跨域
我们提前先来谈论一下CORS(跨域资源共享),对应用程序和服务进行扩展的时候,需要介入各种各种的设备和应用程序。
Having our API just serve data, we can also make the design choice to serve assets from a CDN. This eliminates the issues that CORS brings up after we set a quick header configuration for our application.
只要用户有一个通过了验证的token,数据和资源就能够在任何域上被请求到。
转载请注明出处,掌声送给社会人