JWT技术分析及应用

1. 系统会话状态

1.1 会话状态

客户端与服务端通讯过程中产生的状态信息(类似会议记录),称之为会话状态.

1.2 会话状态存储方式

客户端浏览器与服务器端通讯时使用的是http协议,这个协议本身是无状态协议,也就是说通过此协议,无法存储会话状态,此时在服务端与客户端就采用一种Cookie与Session方式记录会话状态.

1.3 有状态的会话技术分析

  • Cookie 技术
    Cookie是由服务端创建但在客户端存储会话状态的一个对象,此对象分为两种类型,一种为会话Cookie,一种为持久Cookie,浏览器在访问具体的某个域名时会携带这个域的有效Cookie到服务端.
  1. 会话Cookie: 浏览器关闭Cookie生命周期结束(一般默认都是会话Cookie)
  2. 持久Cookie: 持久Cookie是在Cookie对象创建时指定了生命周期,例如一周时间,即便浏览器关闭,持久Cookie依旧有效.
  • Session技术
    Session技术由服务端创建,并在服务端存储会话状态的一个对象,当Session对象创建时,还会创建一个会话Cookie对象,并且通过这个会话Cookie将SessionId写到客户端,客户端下次访问服务端会携带这个会话Cookie,并且通过JsessionId找到Session对象,进而获取Session对象中存储的数据.Cookie默认的生命周期为30分钟.

在SpringSecurity中获取用户的认证信息,就可以通过如下方式进行实现:

 Authentication authentication =
                SecurityContextHolder.getContext().getAuthentication();

1.4 无状态的会话技术分析

有状态的会话实现,在分布式架构中可能会存在很多问题,例如浏览器默认不支持携带其他域的Cookie信息进行资源访问,同时服务端的Session默认不能共享.这种问题的处理方法有两种

  • 将session持久化到到一些数据库,例如Redis,下次请求到其它服务器(例如tomcat)时,可以直接从redis中获取登录信息,但是假如并发比较大,数据库的访问压力就会剧增,压力太大有可能会导致系统宕机.
  • 将用户的登录状态信息都存储在客户端,服务端不记录任何状态,服务端只负责对客户端传递过来的状态信息进行解析,基于此方式进行用户登录状态的判断,这样的会话过程称之为无状态会话.因此JWT诞生.

2. JWT简介

2.1 JWT概述

JWT(JSON WEB Token)是一个标准,借助JSON格式数据作为WEB应用请求中的令牌,进行数据的自包含设计,实现各方安全的信息传输,在数据传输过程中还可以对数据进行加密,签名等相关处理。同时JWT也是目前最流行的跨域身份验证解决方案(其官方网址为:https://jwt.io/)。可以非常方便的在分布式系统中实现用户身份认证。

2.2 JWT数据结构

2.2.1 JWT通常由三部分构成,分别为Header(头部),Payload(负载),Signature(签名),其格式如下:

xxxxxxx.yyyyyyy.zzzzzzz
 头部	 负载	  签名

2.2.2 Header部分

Header部分是一个JSON对象,描述JWT的元数据,通常是下面的样子

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

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(简写HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。最后,将这个 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。

2.2.3 Payload部分

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT规范中规定了7个官方字段,供选用。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号
{
	"sub": "123456",
	"name": "jack",
	"admin": true
}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

这个 JSON 对象也要使用 Base64URL 算法转成字符串。

2.2.3 Signature部分

Signature 部分是对前两部分的签名,其目的是防止数据被篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

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

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

3. JWT快速入门

3.1 环境准备

第一步:创建项目,例如
在这里插入图片描述

第二步:添加相关依赖,其pom.xml文件添加内容如下

    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.3.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--添加jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
    </dependencies>

第三步:定义启动类代码如下

package com.djh.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecurityJwtApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityJwtApplication.class, args);
    }
}

3.2 创建和解析token

编写单元测试,实践Token对象的创建与解析,例如:

	@Test
    void testCreateAndParseToken() {
        //1.创建令牌
        //1.1定义负载信息
        Map<String,Object> map = new HashMap<>();
        map.put("username", "jack");
        map.put("permissions", "sys:res:create,sys:res:retrieve");
        //1.2定义过期实践
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, 30);
        Date expirationTime = calendar.getTime();
        //1.3定义秘钥
        String secret = "djhdjhdjh";
        //1.4生成令牌
        String token = Jwts.builder()
                .setClaims(map)
                .setIssuedAt(new Date())
                .setExpiration(expirationTime)
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
        System.out.println(token);

        //2.解析令牌
        Claims body = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
        System.out.println(body);
	}

3.3 创建JWT工具类

为了简化JWT在项目中的应用,我们通常会构建一个工具类,对token的创建和解析进行封装,例如:

package com.djh.jt.security.util;

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

import java.util.Date;
import java.util.Map;

public class JwtUtils {

   //密钥
   private static String secret="djhdjh";

   /**
    * 为用户生成token
    */
   public static String generateToken(Map<String,Object> claims){

       return Jwts.builder()
               .setClaims(claims)
               .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 30))
               .setIssuedAt(new Date())
               .signWith(SignatureAlgorithm.HS256, secret)
               .compact();
   }

   /**
    * 解析token
    */
   public static Claims getClaimsFromToken(String token) {
       return  Jwts.parser()
               .setSigningKey(secret.getBytes())
               .parseClaimsJws(token)
               .getBody();
   }

   /**
    * 判断token是否过期
    */
   public static Boolean isTokenExpired(String token) {
       Date expiration = getClaimsFromToken(token).getExpiration();
       return expiration.before(new Date());
   }
}

4. JWT在项目中的应用

4.1 AuthController认证服务

定义AuthController用于处理登录认证业务,代码如下:

package com.djh.jt.security.controller;

import com.djh.jt.security.util.JwtUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class AuthController {

   @RequestMapping("/login")
   public Map<String,Object> doLogin(String username,String password) {
       Map<String,Object> map = new HashMap<>();
       if ("jack".equals(username) && "123456".equals(password)){
           map.put("state", "200");
           map.put("message", "login ok");
           Map<String,Object> claims = new HashMap<>();
           claims.put("username",username);
           map.put("Authentication", JwtUtils.generateToken(claims));
           return map;
       }else {
           map.put("state", "500");
           map.put("message", "username or password errpr");
           return map;
       }
   }
}

4.2 ResourceController 资源服务

定义一个资源服务对象,登录成功以后可以访问此对象中的方法,例如:

package com.djh.jt.security.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ResourceController {

    @RequestMapping("/retrieve")
    public String doRetrieve(){
        //检查用户有没有登录
        //执行业务查询操作
        return "do retrieve resource success";
    }
    @RequestMapping("/update")
    public String doUpdate(){
        //检查用户有没有登录
        //执行业务查询操作
        return "do update resource success";
    }
}

4.3 TokenInterceptor 拦截器及配置

假如在每个方法中都去校验用户身份的合法性,代码冗余会比较大,我们可以写一个SpringMVC拦截器,在拦截器中进行用户身份检测,例如:

package com.djh.jt.security.interceptor;

import com.djh.jt.security.util.JwtUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TokenInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler) throws Exception {
        String token = request.getHeader("Authentication");
        //判断请求中是否有令牌
        if(token == null || "".equals(token)){
            throw new RuntimeException("请先登录再执行该操作");
        }
        //判断令牌是否过期
        if(JwtUtils.isTokenExpired(token)){
            throw new RuntimeException("用户信息已过期,请先登录再执行该操作");
        }
        return true;
    }
}

拦截器编写好以后,需要将拦截器添加到SpringMVC执行链中并设置要拦截的请求,可通过配置类完成这个过程,代码如下:

package com.djh.jt.security.config;

import com.djh.jt.security.interceptor.TokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class SpringWebConfig implements WebMvcConfigurer {

    /**
     * 将拦截器添加到Spring MVC的执行链中
     * @param registry 此对象提供了一个list集合,可以将拦截器添加到集合中
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TokenInterceptor())
                .addPathPatterns("/retrieve","/update");
    }
}

4.4 Postman访问测试

第一步:登录访问测试,例如
在这里插入图片描述
第二步:资源访问测试,例如
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

卑微前端汪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值