【JAVA】基于Token的用户验证

背景


传统的用户验证是基于session自身的特性实现,当用户提交登陆请求,后台验证通过后,会在session中留下用户的信息,用于识别当前用户在客户端登陆了。通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。因为认证的记录是保存在内存中,意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

一、基于Token的用户验证


基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

常见验证流程:

用户提交用户名、密码到服务器后台
后台验证用户信息的正确性
若用户验证通过,服务器端生成Token,返回到客户端
客户端保存Token,再下一次请求资源时,附带上Token信息
服务器端(一般在拦截器中进行拦截)验证Token是否由服务器签发的
若Token验证通过,则返回需要的资源


二、JSON Web Token(JWT)


JWT是一种通用的规范,它定义了Token的生成方式。

2.1 JWT的格式
一个完整的Token的形式:

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1bmlzaW1zIiwicGVyaW9kIjo2MDAwMCwiZXhwIjoxNTMxODI5NzI5LCJ1c2VySWQiOiIwZjg3LTQxYzI2ZGExMzUxNjA2MDhkNTYtIiwiaWF0IjoxNTMxODI5NjA5fQ.RZCUmxfaDgPxhocCkomSDcUOwLNYUW3Hgu-ufi0mJZNlurGSQHex0CokiUqRTfhQo0G8VJuYDzjeUklHN2pAdA

由“.”符号拼接,共有三部分信息组成。

JWT由三部分组成:Header、Payload、Signature。

JWT:{
    header:{ // 头部
        alg:'HS256' // 算法声明
    },
    payload:{ // 数据
        exp:'1532180906', // 过期时间
        userId:'xxxx',
        iss:'xxx'
    },
    signature:'' // 签名
}


2.2 Header(头部)
jwt的头部承载两部分信息:

声明类型,这里是jwt
声明加密的算法 通常直接使用 HMAC SHA256
2.3 Payload(数据)
这部分包含了一些通用的信息,以及用户自定义的信息。通常在做用户验证时,会放置userId等信息。 
通用信息组成(不强制全部使用):

iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
2.4 Signature(签证)
这部分信息是一个签证信息,由算法进行加密,它由三部分信息组成:

Header(base64编码后的字符串)
Payload(base64编码后的字符串)
Secret
签证信息的生成方式:

将编码后的Header、Payload由“.”符号进行拼接,将拼接后的字符串使用加密算法进行加密

加密算法的选择:尽量选择哈希散列解密方式,避免被容易的暴力破解。 
备注:密钥需要妥善保存,这是验证Token有效性的唯一标识。

2.5 Token的生成方式
{{base64Encode(Header)}}.{{base64Encode(Payload)}}.{{Signature}}

将编码后的Header、编码后的Payload以及Signature由符号“.”进行拼接,形成最终的Token。

三、JWT的实现


JWT提供了Token的生成规范,没有现成的JAR包可以引用。可以自己手动实现,也可以使用开源项目。推荐一个开源的JWT实现,使用起来挺方便的。Java版本的JWT实现3.1 生成一个Token

private Key getKey(){
        if(ObjectUtils.isEmpty(key)){
            key = MacProvider.generateKey();
        }
        return key;
    }
@Override
    public String generatorToken(String userId){
        Key key = getKey();

        Long currentTime = System.currentTimeMillis();
        Long activeTime = 30 * 60000L; //

        Map<String, Object> claims = new HashMap<>();
        claims.put(USERID, userId);
        claims.put(PERIOD, activeTime / 2); // 有效时间

        String compactJws = Jwts.builder()
                .setClaims(claims)  // 自定义数据
                .setSubject("unisims")  //  
                .setIssuedAt(new Date(currentTime)) // 签发时间
                .setExpiration(new Date(currentTime + activeTime)) // 超时时间
                .signWith(SignatureAlgorithm.HS512, key) // 签名
                .compact();

        return compactJws;
    }   


3.2 验证Token

@Override
    public JWTCheckResult validateToken(String token) {
        JWTCheckResult result = new JWTCheckResult();

        if(StringUtils.isEmpty(token)){
            result.setStatus(JWTEnum.EMPTY.getStatus());
            result.setMessage(JWTEnum.EMPTY.getMessage());
            return result;
        }

        try {
            Key key = getKey();
            Jws<Claims> jwt = Jwts.parser().setSigningKey(key).parseClaimsJws(token);

        } catch (SignatureException e) {
            result.setStatus(JWTEnum.ERROR.getStatus());
            result.setMessage(JWTEnum.ERROR.getMessage());

            System.out.println(e);
        }catch(ExpiredJwtException e){
            result.setStatus(JWTEnum.TIMEOUT.getStatus());
            result.setMessage(JWTEnum.TIMEOUT.getMessage());

            System.out.println(e);
        }catch(Exception e){
            result.setStatus(JWTEnum.ERROR.getStatus());
            result.setMessage(JWTEnum.ERROR.getMessage());
            System.out.println(e);
        }
        return result;
    }


四、Token在用户验证中真正的使用


第三节中讲了Token的生成与简单验证,但是,在项目中真正真正使用时,仅有这些是不够的。

4.1 Token的第一次签发
当服务端收到登陆请求,需要在后台验证账户信息。若是验证通过,则生成Token,除了固定的信息外,还会放置一些与用户相关的信息,通常就是放置用户的ID。
将生成的Token返回前端,一般是将Token放置在response的header中。同时,浏览器在下一次请求也是将Token放置在请求头中。
4.2 Token的验证
客户端请求资源时,需要对客户端的用户进行验证。有些请求是不需要的验证用户(比如登陆请求,不需要验证用户,此时本就没有用户登陆),有些资源是需要验证的,所以需要对URL进行区分。

Teken的验证我选择在拦截器中实现(HandlerInterceptor),在这里对URL进行拦截。首先判断是否是需要验证的URL,然后对Token进行验证。对于Token的验证分为成功验证、无效验证、超时验证、刷新处理、主动失效处理。

Token验证方式的选择:

第一种验证方式:利用session 
在服务器端,生成Token的同时将Token存入session。当收到客户端上传的Token时,将它与session中的进行比对,一致则合法,验证通过。否则,返回验证失败,前端跳转到登陆页面。

第二种验证方式:算法验证 
当收到客户端上传的Token时,对Token的前两部分进行加密,比对加密结果是否与第三部分相同,相同则验证通过。否则,返回验证失败,前端跳转到登陆页面。

备注:为了项目的扩展性考虑,采用第二种方式进行验证。第一种方式依赖于Session,做负载均衡时,当请求被转发到另一台服务器时,由于Session中没有Token信息,会造成成验证不通过。

成功验证: 
保证Token是正确的有两个因素:第一,这个Token能够正确被Token识别,即这个Token的确是由自身的后台签发的;第二,这个Toen没有过期。

一般的,只要这个能够被后台使用自身的密钥+算法正确解密,即可认为这个Token是由自身签发的。

Token的失效:

一般都会为Token添加有效时间区间,即在某个时间区间这个Token是有效的,过了这个时间点后,即认为这是一个不可信任的Token。

上文讲述到的那个开源项目中,已经做好了无效、超时Token验证的接口,不需要我们手动去实现。

无效验证: 
不能被正确解析的Token、以及超时的Token,既是验证不通过。

超时验证: 
默认每一个Token都会为它添加有效时间,当超过生效时间,则认为此Token验不通过。 
一般的,在生成Token时,会在Payload中设置过期时间,在拦截器中,根据这个时间验证是否超时。

上面提到 开源项目中已经自动处理的超时的处理,不需要认为的再次处理,仅仅只需要设置一个过期时间即可。

刷新处理: 
因为业务的需要,不能要求每次Token超时后,用户再次登陆。所以,需要在用户无感知情况下自动刷新Token,避免用户再次发起登陆请求。

第一种方式:通过算法计算 
将过期周期认为是2T,当在T-2T时间区间时,认为此时需要刷新Token。当此Token验证通过,则在后台生成新的Token,随着header返回到前端,前端自己去刷新Token。

这种方式有个缺点,即只要不是超时的Token_A,依然可以正常的请求后台资源。即,假如在T-2T时间区间刷新Token,产生了新的Token_B,此时Token_A依然是有效的Token。

第二种方式:配合内存、数据库刷新Token 
预先将生成的Token存储在内存、或数据库中,在拦截器中验证时,多加一个验证,即前端上报的Token必须与内存或数据库中Token保持一致,否则验证不通过。

备注:第一种方式虽然会造成旧的Token可以一直使用,但是有利于分布式部署。另外存在数据库可是也可以,对分布式没有影响(使用同一个数据库的话)。但是当存储在内存当中(比如Session)时,对分布式就不太友好了。

主动失效: 
一般管理系统都会有用户注销功能,这时,需要主动让Token失效。这时需要配合内存、数据库实现,即在内存、数据库中保存 一份最新的、有效的Token,当注销时,将存储的Token清空。同时在拦截器验证时,发现内存、数据库的Token是空值时,则验证不通过。
--------------------- 
作者:萌太隆 
来源:CSDN 
原文:https://blog.csdn.net/swl979623074/article/details/81150184 
版权声明:本文为博主原创文章,转载请附上博文链接!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章