Springboot-主线8-JWT

JWT

引言

jwt简介

​ Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

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

那它能干什么呢?

  • 授权,
  • 信息交换

传统的Session认证

我们知道,http协议本身是一种无状态的协议,而这就意味着用户向我们的应用提供了用户名和密码进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪一个用户发出的请求,我们只能在服务器存储一份用户登录信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户。

image-20201208175919909

⚠️暴露的问题:

  • 每个用户经过我们的应用认证,我们的应用都要在服务器做一次记录,以便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务器的开销会明显增大。
  • 用户认证之后,服务端做认证记录,如果认证的记录在内存中的话,就意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡的能力,限制了应用的额扩展的能力。
  • 如果用cookie来进行用户识别,cookie如果被屏蔽,用户就会很容易收到跨站请求伪造的攻击。
  • 在前后端分类模式中更为复杂。

JWT认证

认证流程

首先,前端通过web表单将自己的用户名和密码发送到后端的接口,这一个过程为一个POST提交,建议的方式通过SSL加密传输,从而避免敏感信息被泄露。

后端对用户名和密码成功后,将用户的id等其他信息作为JWT Payload,将其头部分别进行Base64编码拼接后签名,形成一个JWT(token).

后端将形成的token作为登录成功的返回结果递给前端,前端将信息保存在本地。退出时删除。

前端每次请求都要携带并放在Authorization中。

后端检查是否存在,如存在验证jwt的有效期。

jwt优势

  • 简洁:可以通过URL,POST参数或者再HTTP header发送,因为数据量小,传输速度也很快。
  • 自包含:负载中包含了所有用户所需要的信息,避免了多次查询数据库
  • 跨语言
  • 适用于分布式微服务,因为jwt不需要再服务端保存会话信息。

JWT结构

  1. 令牌组成

    标头(Header)

    有效载荷(PayLoad)

    签名(Signature)

结构
HeaderPayloadSingature
  1. header

表头有两部分组成:令牌的类型和所使用的签名算法。会使用Base64编码组成的JWT结构的第一部分。

{
    “alg”:"HS256",
    "typ":"JWT"
}
  1. Payload

    令牌的第二部分是有效负载,其中包含声明,声明是有关实体(通常是用户)和其他数据的声明,同样的,它会使用JWT结构的第二部分。

    {
    	“sub”:"1231424324",
    	"name":"wangyun",
    	"admin":true
    }
    
    1. signature

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

      签名的目的

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

      Demo

      导入依赖

      <dependency>
          <groupId>com.auth0</groupId>
          <artifactId>java-jwt</artifactId>
          <version>3.4.0</version>
      </dependency>
      

      生成令牌

      public static void main(String[] args) {
          HashMap<String, Object> map = new HashMap<>();
          Calendar instance = Calendar.getInstance();
          instance.add(Calendar.SECOND,20);
          String token=JWT.create()
                  .withHeader(map) // header
                  .withClaim("userid",21)  //payload
                  .withClaim("username","zhangsan")
                  .withExpiresAt(instance.getTime()) // 指定过期时间
                  .sign(Algorithm.HMAC256("!@#$%^"));// 签名
          System.out.println(token);
      }
      

      结果:

      eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDc1MjE3MDMsInVzZXJpZCI6MjEsInVzZXJuYW1lIjoiemhhbmdzYW4ifQ.UJNX7hQAjFbg2UGH6CWH_EoKny2RBceTat906Xbo84o

      验证令牌

     @Test
     void contextLoads() {
         JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("!@#$%^")).build();
         DecodedJWT jwt = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
                 "eyJleHAiOjE2MDc1Mjg0MzQsInVzZXJpZCI6MjEsInVzZXJuYW1lIjoiemhhbmdzYW4ifQ." +
                 "CbpAUlp5nHsQMO1yQMrcjtSR-aguNfEWZ_gJmw4Mmao");
         jwt.getHeader();
         System.out.println(jwt.getClaim("username").asString());
         System.out.println(jwt.getClaim("userid").asInt());
     }
    

    结果:

    zhangsan
    21

    --异常信息
    - SignatureVerificationException   签名不一致
    - TokenExpiredException			   令牌过期
    - AlgorithmMismatchException	   算法不匹配
    - InvalidClaimException			   失效的payload
    

封装工具类

package com.session.learning.springjwt.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

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

public class JWTUtils {
    private static final String SING="!@#$%^";

    /**
     * 生成token   header.payload.signature
     */
    public static String getToken(Map<String,String> map){
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE,7);// 默认七天过期
        JWTCreator.Builder builder= JWT.create();
        // Payload
        map.forEach((k,v)->{
            builder.withClaim(k,v);
        });
        String token=builder.withExpiresAt(instance.getTime()) // 指定过期时间
               .sign(Algorithm.HMAC256(SING));// 签名
        System.out.println(token);
        return token;
    }

    /**
     * 验证token
     * 如果有问题抛出异常
     **/
    public static void verify(String token){
        JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
    }

    /**
     * 获取token信息方法
     */
    public static DecodedJWT getTokenInfo(String token){
        DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
        return verify;
    }
}

集成SpringBoot

引入依赖

<!--JWT的依赖-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>
<!--引入mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<!--引入lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
</dependency>
<!--引入druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.19</version>
</dependency>
<!--引入mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.38</version>
</dependency>

配置文件

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/jwt?useSSL=false&useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root

mybatis.type-aliases-package=com.lxf.pojo
mybatis.mapper-locations=classpath:mapper/*.xml

logging.level.com.lxf.dao=debug

使用JWT

@RestController
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private JWTUtils jwtut;


    @GetMapping("/user/login")
    public Map<String,Object> login(User user){
        log.info("用户名:[{}]",user.getName());
        log.info("密码:[{}]",user.getPassword());
        HashMap<String, Object> map = new HashMap<>();

        try {
            //登录用户
            User userDB = userService.login(user);
            //存储载荷声明参数map
            HashMap<String, String> plMap = new HashMap<>();
            plMap.put("name", userDB.getName());
            //生成JWT令牌
            String token = jwtut.getToken(plMap);

            map.put("state", true);
            map.put("msg", "认证成功");
            map.put("token", token);
        } catch (Exception e) {
            map.put("state", false);
            map.put("msg", e.getMessage());
        }
        return map;
    }
}

但是你会发现这样很麻烦,所以我们需要每次都能自己在请求时验证token,在分布式环境中可以使用网关,单体应用推荐使用拦截器

public class JWTInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HashMap<String, Object> map = new HashMap<>();
        // 获取请求头的令牌
        String token = request.getHeader("token");
        try{
            DecodedJWT info = JWTUtils.getTokenInfo(token); // 验证令牌
            return true;
        }catch (SignatureVerificationException e){
            e.printStackTrace();
            map.put("msg","无效签名");
        }catch (TokenExpiredException e){
            e.printStackTrace();
            map.put("msg","Token过期");
        }catch (AlgorithmMismatchException e){
            e.printStackTrace();
            map.put("msg","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;

    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值