JWT详解及使用(Springboot整合)

什么是JWT

JWT(JSON Web Token)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,可以在各方之间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。

也就是通过JSON形式作为 Web应用中的令牌,用于在各方之间安全的将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。

通俗的讲就是一个json形式的web应用令牌。

什么时候应该用JWT

下列场景中使用JSON Web Token是很有用的:

  • Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。

  • Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。

为什么使用JWT?

我们先看一下传统的认证方式

session 认证:

用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
​
请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
​
浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
​
当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

 

根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

session认证暴露的问题

1、每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大。 
2、用户认证之后,服务端做认证记录,如果认证的记录保存在内存中,这意味着用户下次请求还需请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应限制了负载均衡的能力。这也意味着限制了应用的扩展能力。 
3、因为是基于cookie来进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击
cookie+session这种模式通常是保存在内存中,而且服务从单服务到多服务会面临的session共享问题,随着用户量的增多,开销就会越大。而JWT只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析即可。

基于token认证

认证流程:

前端将用户的登录信息发送给服务器;

服务器接受请求后为用户生成独一无二的认证信息--token,传给客户端浏览器;

客户端将token保存在cookie或者storage中;

在之后访问客户端都携带这个token请求服务器;

服务器验证token的值,如果验证成功则给客户端返回数据 

JWT的组成

JWT的组成分为三部分,它们之间用圆点(.)连接

Header 头部 
Payload 载荷 
Signature 签名

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0b2tlbl9kZW1vIiwiaXNzIjoiU0VSVklDRSIsIm5hbWUiOiJ4eXgiLCJleHAiOjE2Mjk1Mzk0MzMsImlhdCI6MTYyOTQ1MzAzMywidXNlcklkIjoyMX0.91uTZ1O7Pt_st_UKBlTseMJe3ZlSu027mrv4Admo1u8

Header(头部)

header部分,即头信息,包含两部分,alg:算法名称、typ:token的类型

{
    'alg':"HS256",
    'typ':"JWT"
}

将头部使用base64编码加密,构成第一部分

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload(载荷)

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

标准中注册的声明 (建议但不强制使用) :

iss: jwt签发者 
sub: jwt所面向的用户
aud: 接收jwt的一方 
exp: jwt的过期时间,这个过期时间必须要大于签发时间 
nbf: 定义在什么时间之前,该jwt都是不可用的. 
iat: jwt的签发时间 
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

存放传输的数据

{
        'name':"name" 
        'state':true 
}

对于签名令牌,此信息虽然可以防止篡改,但任何人都可以读取。除非加密,否则不要将敏感信息放入到Payload或Header元素中。

Payload中放传输的信息,用户信息等用于传输的信息。Playload部分的JSON使用base64编码加密,形成JWT的第二部分。

eyJzdWIiOiJ0b2tlbl9kZW1vIiwiaXNzIjoiU0VSVklDRSIsIm5hbWUiOiJ4eXgiLCJleHAiOjE2Mjk1Mzk0MzMsImlhdCI6MTYyOTQ1MzAzMywidXNlcklkIjoyMX0

Signature(签名)

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行secret组合加密,然后就构成了jwt的第三部分

如:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

将这三部分用.连接,组成了一个JWT

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0b2tlbl9kZW1vIiwiaXNzIjoiU0VSVklDRSIsIm5hbWUiOiJ4eXgiLCJleHAiOjE2Mjk1Mzk0MzMsImlhdCI6MTYyOTQ1MzAzMywidXNlcklkIjoyMX0.91uTZ1O7Pt_st_UKBlTseMJe3ZlSu027mrv4Admo1u8

JWT优缺点

优点:

1. 可扩展性好
应用程序分布式部署的情况下,Session需要做多机数据共享,通常可以存在数据库或者Redis里面。而JWT不需要。
​
2. 无状态
JWT不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外JWT的载荷中可以存储一些常用信息,用于交换信息,有效地使用 JWT,可以降低服务器查询数据库的次数。

缺点:

1 安全性:由于JWT的payload是使用Base64编码的,并没有加密,因此JWT中不能存储敏感数据。而Session的信息是存在服务端的,相对来说更安全。
​
2 性能:JWT太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致JWT非常长,Cookie的限制大小一般是4k,cookie很可能放不下,所以JWT一般放在LocalStorage里面。并且用户在系统中的每一次Http请求都会把JWT携带在Header里面,Http请求的Header可能比Body还要大。而SessionId只是很短的一个字符串,因此使用JWT的Http请求比使用Session的开销大得多。
​
3 一次性:无状态是JWT的特点,但也导致了这个问题,JWT是一次性的。想修改里面的内容,就必须签发一个新的JWT。即缺陷是一旦下发,服务后台无法拒绝携带该jwt的请求(如踢除用户)
​
(1)无法废弃:通过JWT的验证机制可以看出来,一旦签发一个JWT,在到期之前就会始终有效,无法中途废弃。例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个JWT,但是由于旧的jwt还没过期,拿着这个旧的JWT依旧可以登录,那登录后服务端从JWT中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的JWT,那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。
​
(2)续签:如果你使用jwt做会话管理,传统的Cookie续签方案一般都是框架自带的,Session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变JWT的有效时间,就要签发新的JWT。最简单的一种方式是每次请求刷新JWT,即每个HTTP请求都返回一个新的JWT。这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。另一种方法是在Redis中单独为每个JWT设置过期时间,每次访问时刷新JWT的过期时间。

可以看出想要破解JWT一次性的特性,就需要在服务端存储jwt的状态。但是引入 redis 之后,就把无状态的jwt硬生生变成了有状态了,违背了JWT的初衷。而且这个方案和Session都差不多了。

jwt续签思路

使用 Refresh Token

还有另一种方案,使用 Refresh Token,它可以避免频繁的读写操作。这种方案中,服务端不需要刷新 Token 的过期时间,一旦 Token 过期,就反馈给前端,前端使用 Refresh Token 申请一个全新 Token 继续使用。

这种方案中,服务端只需要在客户端请求更新 Token 的时候对 Refresh Token 的有效性进行一次检查,大大减少了更新有效期的操作,也就避免了频繁读写。

当然 Refresh Token 也是有有效期的,但是这个有效期就可以长一点了,比如,以天为单位的时间。

服务端保存token状态

在服务器端保存 Token 状态,用户每次操作都会自动刷新(推迟) Token 的过期时间

但每秒种可能发起很多次请求,每次都去刷新过期时间会产生非常大的代价。

如果 Token 的过期时间被持久化到数据库或文件,代价就更大了。所以通常为了提升效率,减少消耗,会把 Token 的过期时间保存在缓存或者内存中

这里我们使用第二种方式实现续签功能

整合JWT 

所需依赖

<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.4.1</version>
</dependency>
<!--redis依赖 存储jwt状态-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

拦截请求验证jwt 

@Component
public class JWTInterceptor implements HandlerInterceptor{

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取请求头中token
        String token = request.getHeader("token");
        HashMap<String, Object> map = new HashMap<>();
        DecodedJWT verify = null;
        try{
            //验证token
            verify = JWTUtil.verify(token);
        }catch (Exception e){
            //验证失败,抛出异常
            e.printStackTrace();
            map.put("msg","无效token");
            map.put("state",false);
            String json = new ObjectMapper().writeValueAsString(map);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(json);
            return false;
        }

        //判断redis缓存中有没有这条token
        if (redisUtil.sGet("token"+verify.getClaim("id").asString())==null
                || !redisUtil.sGet("token"+verify.getClaim("id").asString()).equals(token)){
            map.put("msg","305 无效token");
            map.put("state",false);
            String json = new ObjectMapper().writeValueAsString(map);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(json);
            return false;
        }

        //每一次发送请求,重新保存token,刷新过期时间
        redisUtil.sSet("token"+verify.getClaim("id").asString(),token);
        logger.info("已续签:{}",redisUtil.getExpireTime("token"+verify.getClaim("id").asString()));
        return true;

    }

}

 jwt 工具类

public class JWTUtil {

    private static final String SECRET = "w!erf^&gdAd#rf$G^H^yfnS";

    /**
     * 获取token
     * @param map
     * @return
     */
    public static String getJWT(Map<String,String> map){
        HashMap<String, Object> JWTMap = new HashMap<>();
        JWTMap.put("typ","JWT");
        JWTMap.put("alg","HS256");

        JWTCreator.Builder builder = JWT.create().withHeader(JWTMap)
                .withIssuer("xyx")
                .withIssuedAt(new Date());
//                .withExpiresAt()

        map.forEach((a,v)->{
            builder.withClaim(a,v);
        });

        String token = builder.sign(Algorithm.HMAC256(SECRET));

        return token;
    }

    /**
     *  验证token
     * @param token
     * @return
     */
    public static DecodedJWT verify(String token){
        Verification require = JWT.require(Algorithm.HMAC256(SECRET));
        DecodedJWT verify = require.build().verify(token);
        return verify;
    }

}

Redis相关工具类 

@Component
public class RedisUtil {

    //60秒做测试
    private final long time = 1000*60;

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    public boolean sSet(String key,String value){
        try{
            redisTemplate.opsForValue().set(key,value,time, TimeUnit.MILLISECONDS);
            return true;
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

    public String sGet(String key){
        return String.valueOf(redisTemplate.opsForValue().get(key));
    }

    public long getExpireTime(String key){
        return redisTemplate.opsForValue().getOperations().getExpire(key);
    }
}

如您在阅读中发现不足,欢迎留言!!!

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值