06--JWT技术分析及应用实践

JWT简介

背景

在传统的有状态服务应用中,服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如:Tomcat中的Session。例如:登录,用户登录后,我们把用户的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session,然后下次请求,用户携带cookie值(这一步由浏览器自动完成),我们就能识别到对应的session,从而找到用户的信息。这种方式目前看来是最方便的,但是在分布式应用中,由服务端保存用户状态不是一种很好的选择,因此JWT由此而生。

JWT概述

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

JWT数据结构

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

格式如下:xxxxx.yyyyy.zzzzz

eyJhbGciOiJIUzI1NiJ9.eyJwZXJtaXNzaW9ucyI6InN5czpyZXM6Y3JlYXRlLHN5czpyZXM6cmV0cmlldmUiLCJleHAiOjE2MjY5MzIyNTksImlhdCI6MTYyNjkzMDQ1OSwidXNlcm5hbWUiOiJqYWNrIn0.SQrRS5nuID1Xv5GMvUgnr7xrVzB7GcRFrkNak-x16Mw

Header部分

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

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

上面代码中,alg属性表示签名的算法(algorithm),默认是HMAC SHA256(简写HS256);typ属性表示这个令牌(token)的类型,JWT令牌统一写为JWT。

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":"XiongQiang",
	"admin":true
}

注意,JWT默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分,最后json对象也要使用Base64URL算法转成字符串。

Signature部分

Signature部分是对前两部分的签名,其目的是为了防止数据被篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用Header里面指定的签名算法(默认是HMAC SHA256),按照下面的公式产生签名。

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

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

JWT快速入门

环境准备

第一步:创建maven项目;
第二步:添加依赖,其中pom.xml文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.3.2.RELEASE</version>
    </parent>

    <groupId>com.cy</groupId>
    <artifactId>03-jt-security-jwt</artifactId>
    <version>1.0-SNAPSHOT</version>

    <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>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

第三步:创建配置文件application.yml。
第四步:定义启动类,代码如下:

package com.cy.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);
    }
}

第四步:运行启动类,检测是否可以成功启动。

创建和解析token

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


@SpringBootTest
public class JwtTest {


    /*测试创建和解析token的过程*/
    @Test
    public  void testCreateAndParseToken(){
        //1.创建一个token令牌(包含三部分:头信息、负载信息、签名信息)
            //定义payload负载信息
          Map<String,Object> map = new HashMap<>();
          map.put("username", "xiongqiang");
          map.put("permissions", "sys:res:create");

          //定义密钥
          String secret="AAABBBCCCDD";
          //生成令牌token
          String token = Jwts.builder()
                .setSubject("延安大学")
                .setClaims(map) //负载信息(存储登录用户的信息)
                .setExpiration(new Date(System.currentTimeMillis()+3600*10000))//失效时间
                .setIssuedAt(new Date()) //签发时间
                .signWith(SignatureAlgorithm.HS256,secret)//签名加密算法以及密钥盐
                .compact();//生成token
        System.out.println("token:"+token);
        
          //2.解析token内容
           Claims body = Jwts.parser() //获取解析器对象 Claims可以理解为后续存储用户认证,授权信息的的对象
                .setSigningKey(secret) //设置解析时的密钥
                .parseClaimsJws(token) //获取token中的负载
                .getBody(); //获取具体负载内容
        System.out.println(body);
    }
}

创建JWT工具类

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

public class JwtUtils {
    //密钥
     private static  String secret = "AABBCCDD";

    /*1.基于负载和算法创建token令牌*/
    public static String generatorToken(Map<String,Object> map){
        String token = Jwts.builder()
                .setClaims(map)
                .setExpiration(new Date(System.currentTimeMillis()+3600))
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS256,secret)
                .compact();
        return token;
    }

    /*2.解析token获取数据*/
    public static Claims getClaimsFromToken(String token){
        return Jwts.parser()
                .setSigningKey(secret.getBytes())
                .parseClaimsJwt(token)
                .getBody();

    }

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

    /*4.为指定用户生成token令牌*/
    public static String generateToken(Map<String,Object> claims){
        Date createdTime = new Date();
        Date expirationTime = new Date(System.currentTimeMillis()+3600*1000);
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(createdTime)
                .setExpiration(expirationTime)
                .signWith(SignatureAlgorithm.HS256,secret)
                .compact();
    }
}

JWT在项目中的应用

AuthenticationController认证服务

@RestController
public class AuthenticationController {
    @RequestMapping("/login")
    public Map<String,Object> doLogin(String username,String password){
        Map<String,Object> map = new HashMap<>();
        if("xiongqiang".equals(username)&&"123456".equals(password)){
            map.put("state","200");
            map.put("message","登录成功!");
            Map<String,Object> claims = new HashMap<>();
            claims.put("username", username);
            map.put("Authntication", JwtUtils.generatorToken(claims));
            return map;
        }else {
            map.put("state", "500");
            map.put("message", "登录失败!");
            return map;
        }
    }
}

ResourceController资源服务

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

@RestController
public class ExeController {
    //用户必须在登录之后才能执行相关操作(需要检查)

    //1.添加操作
    @RequestMapping("/doCreate")
    public String doCreate() {
        return "insert resource  data Ok!";
    }

    //2.查询操作
    @RequestMapping("/doRetrieve")
    public String doRetrieve() {
        return "select resource  data Ok!";
    }
}

TokenInterceptor拦截器及配置

假如在每个方法中都需要去检验用户身份是否认证,代码冗余性会比较大,我们可以写一个Spring MVC拦截器。在拦截器中进行用户身份验证,例如:

public class TokenInterceptor implements HandlerInterceptor {
    /*
    令牌(token:ticker-通票)拦截器
    其中,HandlerInterceptor为Spring Mvc中的拦截器,可以在Controller方法执行之前或者之后完成一些规定的动作。
     preHandle方法是在目标Controller方法之前执行规定的动作,例如检查用户是否登录
     */
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {

        System.out.println("---------站住,你需要接受我们的检查!-------------");

        String token = request.getParameter("Authentication");
        //判断请求中是否有令牌
        if(token==null||"".equals(token))throw new RuntimeException("请先登录");
        return true;//false表示拦截到请求以后,不再继续传递;true表示放行;
    }
}

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

**
 * 定义Spring Web MVC配置类
 */
@Configuration
public class SpringWebConfig implements WebMvcConfigurer {

    /**
     *  将拦截器添加到spring mvc的执行链中
     * @param registry 此对象提供了一个list集合,可以将拦截器添加到集合中
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TokenInterceptor())
                .addPathPatterns("/doCreate","/doRetrieve");//设置拦截的目标(URL)
    }
}

总结(Summary)

本章节重点讲解了JWT产生的背景,它的构成和项目中的基本应用,需要在实践中进行分析和理解.

重难点分析

JWT 诞生的背景?(分布式架构应用平台下无状态会话时,规范令牌(通票)数据格式)
JWT 规范定义的数据格式?(头,负载-详细内容,签名,思考一篇文章的构成,)
JWT 规范下JAVA相关API的应用?(jjwt依赖-Jwts)
基于JWT规范下JAVA API 创建令牌,解析令牌

FAQ分析

JWT 是什么?(一种规范的数据格式)
JWT规范中的数据格式有几部分构成?(3部分,前两部分会进行Base64编码,最会基于签名算法加密)
JWT的负载(Payload-存储实际用户信息的部分)部分可以自定义吗?(Claims)
JWT令牌对象一般是在哪里创建?(服务端,可以创建令牌以后,响应到客户端)
JWT令牌假如要存储在客户端你会存储在哪里?(Cookie,localStorage,sessionStorage)
JWT令牌以怎样的方式有客户端传递到服务端?(请求参数,请求头)

Bug分析

创建token和解析token时一定要相同的密钥
Token过期了

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值