token验证技术的原理和简单实现

本文深入探讨了基于Token的身份验证机制,解释了为何Token验证成为WebAPI领域的主流选择。文章对比了传统的Session验证方式,详细阐述了Token验证的四大优势:无状态与可扩展性、移动设备友好、跨程序调用及安全性提升。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么使用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,数据和资源就能够在任何域上被请求到。

转载请注明出处,掌声送给社会人

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值