【JAVA】基于Token的用户验证


🤩🤩🤩🤩🤩🤩🤩🤩🤩🤩🤩🤩🤩🤩

To 个人主页 关注不迷路 😙😙😙


背景

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

一、基于Token的用户验证

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

常见验证流程:

  1. 用户提交用户名、密码到服务器后台
  2. 后台验证用户信息的正确性
  3. 若用户验证通过,服务器端生成Token,返回到客户端
  4. 客户端保存Token,再下一次请求资源时,附带上Token信息
  5. 服务器端(一般在拦截器中进行拦截)验证Token是否由服务器签发的
  6. 若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的第一次签发

  1. 当服务端收到登陆请求,需要在后台验证账户信息。若是验证通过,则生成Token,除了固定的信息外,还会放置一些与用户相关的信息,通常就是放置用户的ID。
  2. 将生成的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是空值时,则验证不通过。

(做个记号,还没写完,需要补充)

  • 8
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逛街的猫啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值