Bootstrap

JWT介绍、测试案例 以及实际开发中的使用

什么是JWT?

JWT,通过数字签名的方式,以json对象为载体,在不同的服务终端之间安全的传输信息,用来解决传统session的弊端。

JWT在前后端分离系统,通过JSON形式作为WEB应用中的令牌(token),用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中,还可以完成数据加密、签名等相关处理。

JWT能做什么?

1.授权:一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。

2.信息交换:jwt是在各方之间安全地传输信息的好方法,因为可以对JWT进行签名,所以可以确保发件人是他们所说的人,此外可以验证内容是否遭到篡改。

为什么会有JWT?

传统的session认证有如下的问题

1.每个用户经过我们的应用认证之后,将认证信息保存在session中,由于session服务器中对象,随着认证用户的增多,服务器内存开销会明显增大;

2.用户认证之后,服务端使用session保存认证信息,那么要取到认证信息,只能访问同一台服务器,才能拿到授权的资源。这样在分布式应用上,就需要实现session共享机制,不方便集群应用;

3.因为session是基于cookie来进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

基于JWT的认证流程:

  • 前端通过web表单将自己的用户和密码发送到后端接口(一般是http-post请求,建议使用SSL加密传输(https协议),以免敏感信息被嗅探)
  • 后端核对用户名和密码成功后,将用户的Id等其他信息作为JWT Payload(负载),将其与头部分别进行BASE64编码拼接后签名,形成一个JWT(Token),形成的JWT就是一个字符(head.payload.singueater)
  • 后端将JWT字符串作为登录成功的结果返回给前端。前端结果保存在localStorage(本地缓存)或sessionStorage上,退出登录时前端删除保存的JWT即可。
  • 前端在每次请求时将JWT放入http header中的Authorization位(解决XSS和XSRF问题)
  • 后端检查是否存在,如存在验证JWT的有效性,例如:检查签名是否正确,检查token是否过期,检查token的接收方是否是自己(可选)。

JWT的优势:

  • 简洁,可以通过URL、POST参数或Http header发送,因为数据量小,传输速度快;
  • 自包含,负载(属于JWT的一部分)中包含了用户所需要的信息,不需要在服务器端保存会话信息,不占服务器内存,也避免了多次查询数据库,特别适用于分布式微服务;
  • 因为token是以json加密的形式保存在客户端的,所以JWT可以跨语言使用,原则上任何WEB形式都支持。
  • 不需要再服务端保存会话信息,特别适用于分布式微服务。

JWT结构

JWT其实就是一段字符串,由标头(Header)有效载荷(Payload)签名(Signature)这三部分组成,用  .  拼接。在传输的时候,会将JWT的三部分分别进行Base64编码后用 . 进行连接形成最终传输的字符串

头部(Header): JWT的头部是一个JSON对象,用于描述JWT的元数据,例如令牌的类型(typ)和签名算法(alg)。通常情况下,头部会包含以下信息:

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

 - alg:指定签名算法,常见的有HMAC SHA256(HS256)和RSA SHA256(RS256)等。

   - typ:指定令牌的类型,一般为JWT。

头部需要经过Base64编码后作为JWT的第一部分

载荷(Payload):JWT的载荷是存储实际数据的部分,也是一个JSON对象。它包含了一些声明(claims),用于描述令牌的信息。常见的声明有:

{
  "sub": "1234567890",
   "name": "John Doe",
   "admin": true
}

        前面两部分都使用Base64进行编码,前端可以解开知道里面的信息, Signature需要使用编码后的header和payload以及我们提供的一密钥,然后使用header中指定的签名算法进行签名,以保证JWT没有被篡改过

        使用Signature签名可以防止内容被篡改。如果有人对头部及负载内容解码后进行修改,再进行编码,最后加上之前签名组成新的JWT。那么服务器会判断出新的头部和负载形成的签名和JWT附带的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。

JWT测试demo

package com.qcby.springbootdemo1;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.junit.jupiter.api.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class jwtTest {
    //JWT的生成:
    @Test
    public void testGen(){
        Map<String,Object> claims = new HashMap<>();
        claims.put("id",1);
        claims.put("username","张三");
        String token = JWT.create()
                .withClaim("user",claims) //t添加第二部分
                .withExpiresAt(new Date(System.currentTimeMillis()+1000*60*60*12)) //添加过期时间
                .sign(Algorithm.HMAC256("itheima"));//指定算法,配置 密钥
        System.out.println(token);
    }
    //JWT的验证:
    @Test
    public void testGen1(){
        //定义字符串,模拟前端传过来的token
        String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE3MzE2MjE4NjZ9.XKk_AiJ5njz7tXRG9xW5lWdrvq71LzIhiHKrvYsvvbI";
        //require:申请一个jwt的验证器
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("itheima")).build();
        DecodedJWT decodedJWT = jwtVerifier.verify(token);//验证token,生成一个解析后的JWT对象
        Map<String, Claim> claims = decodedJWT.getClaims();
        System.out.println(claims.get("user"));//拿到token中的信息
        //如果篡改了头部和载荷部分的数据,那么验证失败
        //如果密钥改了,验证失败
        //token过期
    }
}

封装的JwtUtil:

package com.qcby.gaokao.util;
/**
 * JWT工具类
 */
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;

public class JwtUtil {
    //有效期为
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 * 1000
    //设置秘钥明文
    public static final String JWT_KEY = "qcby";
    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        //设置签名算法:
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //获取当前时间,为设置过期时间准备
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        //如果调用方法时没有定义密钥,就是JwtUtil中定义的
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        //设置过期时间
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        //设置加密后的密钥
        SecretKey secretKey = generalKey();

        JwtBuilder builder = Jwts.builder()
                .setId(id)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("wd")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);// 设置过期时间
        return builder.compact();
    }
    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
    /**
     * 解析
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
    public static void main(String[] args) {
        String token = JwtUtil.createJWT(UUID.randomUUID().toString(),"qd",null );
        System.out.println(token);
    }
}

在项目中的应用:

我是在登录功能的时候使用的JWT技术

  • 在我第一次登录的时候就会生成token令牌
  • 我的令牌的校验的功能在拦截器中进行了实现

登录部分:

List<Student> students = studentMapper.findone(student);
if(students.size()== 1){
    //这些就是在进行登录拦截
    String token = JwtUtil.createJWT(UUID.randomUUID().toString(),String.valueOf(students.get(0).getId()),null);
    //将token信息设置如cookie当中
    Cookie cookie = new Cookie("token",token);
    cookie.setPath("/");   //设置浏览器的访问路径
    cookie.setMaxAge(36000); //设置cookie的过期时间
    response.addCookie(cookie);

    /**
     * 下面是用于传递账户邮箱信息的功能
     */
    //1、获取该账户的邮箱
    String email = students.get(0).getEmail();
    //2、将邮箱信息放入Cookie
    Cookie cookie1 = new Cookie("email",email);
    cookie1.setPath("/");
    cookie1.setMaxAge(36000);
    response.addCookie(cookie1);//到此逻辑结束

    //登录成功
        return new ResponseResult(200,"登录成功");
    }else {
    //登录失败
        return new ResponseResult(444,"登录失败");
    }

拦截器部分:

/*
* 配置拦截器
* */
@Component
public class LoginInterceptor implements HandlerInterceptor {
    //拦截器
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Cookie[] cookies = request.getCookies();
        if(cookies != null){
            for (Cookie cookie:cookies){
                if("token".equals(cookie.getName())){
                    String userToken = cookie.getValue();
                    if(!StringUtils.hasText(userToken)){
                        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                    }
                    //解析token看看是否成功
                    try {
                        Claims claims = JwtUtil.parseJWT(userToken);
                        claims.getSubject();
                    }catch (Exception e){
                        //e.printStackTrace();
                        System.out.println("token信息出错");
                        return false;
                    }
                    return true;  //放行
                }
            }
        }
        return false;
    }
}

;