JWT官网: https://jwt.io/
JWT(Java版)的github地址:https://github.com/jwtk/jjwt

JWT请求流程

 

    

  1. 用户使用账号和面发出post请求;
  2.  服务器使用私钥创建一个jwt;
  3. 服务器返回这个jwt给浏览器;
  4. 浏览器将该jwt串在请求头中像服务器发送请求;
  5. 服务器验证该jwt;
  6. 返回响应的资源给浏览器。

JWT的主要应用场景

  1. 前后端分离
  2. 单点登录(sso)
  3. 分布式微服务

一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

优点

  1. 简洁(Compact): 可以通过URLPOST参数或者在HTTP header发送,因为数据量小,传输速度也很快
  2. 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
  3. 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
  4. 不需要在服务端保存会话信息,特别适用于分布式微服务。
  5. JWT只通过算法实现对Token合法性的验证,不依赖数据库,Memcached的等存储系统,因此可以做到跨服务器验证,只要密钥和算法相同,不同服务器程序生成的Token可以互相验证。

JWT的token机制

JWT标准的token包含三部分:

  1. header(头部),头部信息主要包括(参数的类型--JWT,签名的算法--HS256)
  2. poyload(负荷),负荷基本就是自己想要存放的信息(因为信息会暴露,不应该在载荷里面加入任何敏感的数据),有两个形式,下边会讲到
  3. sign(签名),签名的作用就是为了防止恶意篡改数据

为什么要使用签名

签名解决了数据传输过程中参数被篡改的风险
一般而言,加密算法对于不同的输入产生的输出总是不一样的,如果有人对Header以及Payload的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。


服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token

 

<!-- json web token  -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

package com.application.utils;

import java.util.Calendar;
import java.util.Date;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

public class TokenUtil {
	/**
	 * 加密
	 * @author Jason
	 * @date 2019-03-29
	 * @param id 用户id
	 * @param username 用户名称
	 * @param password 用户密码
	 * @param deviceId 设备号
	 * @param formatDate 过期日期
	 * @return 加密的token
	 */
	public static String createToken(String id, String username, String password, String deviceId, String formatDate) {
		SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

		long nowMillis = Calendar.getInstance().getTimeInMillis();
		Date now = new Date(nowMillis);

		// 加密秘钥
		byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary("123456789");
		SecretKeySpec signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

		JwtBuilder builder = Jwts.builder()
				.setIssuedAt(now)
				.setId(id)
				.setIssuer(username)
				.setSubject(password)
				.setAudience(deviceId)
				.signWith(signatureAlgorithm,signingKey);


		// 设置过期日期
		Date expirationDate = null;
		if (null != formatDate) {
			try {
				expirationDate = DateUtil.stringToDate(formatDate);
			} catch (Exception e) {
				expirationDate = getDefultExpirationDate();
			}
			if(null == expirationDate || expirationDate.before(new Date())){
				expirationDate = getDefultExpirationDate();
			}
		} else {
			expirationDate = getDefultExpirationDate();
		}
		builder.setExpiration(expirationDate);
		
		return builder.compact();
	}
	
	/**
	 * 获取默认的过期时间,默认是七天以后
	 * @author Arwen Liu
	 * @date 2018-10-29
	 * @return Date
	 */
	private static Date getDefultExpirationDate(){
		long timeInMillis = Calendar.getInstance().getTimeInMillis();
		Long time = 1 * 1000 * 60 * 60 * 24 * 7l;
		time += timeInMillis;
		return new Date(time);
	}

	/**
	 * 解密
	 * @author Arwen Liu
	 * @date 2018-10-29
	 * @param token
	 * @return Claims
	 */
	public static Claims parseToken(String token) {
		Claims claims = Jwts.parser()
				// s解密密钥,需要和 加密秘钥一致
				.setSigningKey(DatatypeConverter.parseBase64Binary("123456789"))
				.parseClaimsJws(token).getBody();

		return claims;
	}

	public static void main(String[] args) {
		String secretToken = createToken("1001", "staff", "123456","deviceId", "2018-12-28");
		System.out.println("加密后---->" + secretToken);
		// ss解密
		Claims claims = parseToken(secretToken);
		System.out.println("解密后---->");
		System.out.println("id: " + claims.getId());
		System.out.println("username: " + claims.getIssuer());
		System.out.println("password: " + claims.getSubject());
		System.out.println("deviceId: "+ claims.getAudience());
		System.out.println("expiration: " + DateUtil.dateToString(claims.getExpiration(), "yyyy-MM-dd"));

	}

}

验证token的策略

  1. 让客户端保存到 localStorage 每次请求带着token
  2. 放到cookie里面 然后定义一个拦截器 每次都从cookie里面查询并验证token
  3. 放到header里面 和 cookie类似

 

在退出登录时怎样实现JWT Token失效呢?
退出登录, 只要客户端端把Token丢弃就可以了,服务器端不需要废弃Token。

怎样保持客户端长时间保持登录状态?

服务器端提供刷新Token的接口, 客户端负责按一定的逻辑刷新服务器Token。

服务器端是否应该从JWT中取出userid用于业务查询?

REST API是无状态的,意味着服务器端每次请求都是独立的,即不依赖以前请求的结果,因此也不应该依赖JWT token做业务查询, 应该在请求报文中单独加个userid 字段。
为了做用户水平越权的检查,可以在业务层判断传入的userid和从JWT token中解析出的userid是否一致, 有些业务可能会允许查不同用户的数据。

如何防范Replay Attacks

重复攻击

所谓重复攻击就是攻击者发送一个后端服务器已接收过的包,来达到攻击系统的目的。

比如在浏览器端通过用户名/密码验证获得签名的Token被木马窃取。即使用户登出了系统,黑客还是可以利用窃取的Token模拟正常请求,而服务器端对此完全不知道,因为JWT机制是无状态的。

可以在Payload里增加时间戳并且前后端都参与来解决:

1.前端生成token时,在payload里增加当前时间戳
2.后端接收后,对解析出来的时间戳和当前时间进行判断,
3.如果相差特定时间内(比如2秒),允许请求否则判定为重复攻击

 

 

参考 https://www.jianshu.com/p/e88d3f8151db

https://www.jianshu.com/p/836df92c06eb

https://blog.csdn.net/why15732625998/article/details/78534711

https://www.jianshu.com/p/e88d3f8151db

http://www.cnblogs.com/xiekeli/p/5607107.html

 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐