当前位置: 首页 > news >正文

你知道JWT是什么吗?它和Session的区别又在哪里?

听说微信搜索《Java鱼仔》会变更强哦!

本文收录于JavaStarter ,里面有我完整的Java系列文章,学习或面试都可以看看哦

(一)前言

在前面一篇讲分布式session的时候,有读者问了一句用JWT不香吗,连Redis服务器都省了,又可以实现分布式环境下的人员信息认证。正好我也还没写过关于JWT的文章,刚好在项目中又用过这项技术,今天就来分享一下。还是一样,先理论后实践,有问题评论一起头脑风暴。

(二)什么是JWT

JWT全程JSON Web Token,用户会话信息存储在客户端浏览器中,各方之间通过JSON对象安全传输信息,这些信息可以通过加密算法进行加密。

这样描述可能比较难懂,简单来讲,JWT就是在你登陆的时候生成一串加密字符串token,在每次请求的时候都带上这串token,服务器如果可以解密这段字符串,说明就是登陆状态的。

我们一般会将非敏感数据加密生成token,如果服务器端需要用到人员信息,就解密取人员信息。

你会发现,使用JWT直接就解决了分布式集群环境下的人员认证问题,因为生成的这串token在哪台服务器上都可以解析出来。如下图所示:

JWT流程图

了解了原理之后,来讲讲实际的,JWT的数据包含三个部分:

1、Header 2、Payload 3、Signature

三者通过"."组合在一起,经过加密后生成一段token:

Header.Payload.Signature

下面是我生成的一段token

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqYXZheXoiLCJjcmVhdGVkYXRlIjoxNjExNzU1MDIxNjk1LCJpZCI6MSwiZXhwIjoxNjEyMzU5ODIxLCJ1c2VyTGV2ZWxJZCI6bnVsbH0.ahFWQ_BJ1WNWp9GnlTrSNThVa3i3dydzcaNxLmPb7HI

Header用来描述JWT元数据,包含两个数据,其中alg表示签名的算法,默认HS256,typ属性表示令牌类型,这里就是JWT。上面生成的token中第一个“.”之前的数据base64解码后就是下面的json

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

Payload用来以JSON格式记录用户信息,这里的用户信息可以自定义,上面第一个“.”和第二个“.”之间的数据base64解码后就是下面的json

{
    "sub":"javayz",
    "createdate":1611755021695,
    "id":1,
    "exp":1612359821,
    "userLevelId":null
}

Signature存放加密串,通过指定算法对Header和Payload加盐加密,各部分通过“.”分割。组成了token。

(三)JWT身份认证流程

看完上面的内容,我相信你对JWT的认证已经有认识了,其身份认证的流程如下所示:

1、用户输入用户名和密码登陆

2、服务器端校验用户名和密码是否正确,如果正确就返回token给客户端

3、客户端将token存放在cookie或者local storage中

4、客户端后续的每次请求,都要带上这个token

5、服务器通过token判断是哪个用户

(四)代码实践

接下通过代码模拟JWT的认证实现,我写这段代码时的环境为springBoot2.4.2,引入JWT依赖:

<!--JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

在配置文件中配置一些接下来会用到的属性:

jwt:
  tokenHeader: Authorization
  secret: javayz
  expiration: 604800
  tokenHead: Bearer

写个类来读取这些参数:

@Data
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtProperties {
    private String tokenHeader;
    private String secret;
    private Long expiration;
    private String tokenHead;
}

编写JWT工具类,实现生成token和解码token的功能:

public class JwtKit {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 创建token
     * @param user
     * @return
     */
    public String generateJwtToken(User user){
        //所有的用户数据放在claims中
        Map<String,Object> claims=new HashMap<>();
        claims.put("sub",user.getUsername());
        claims.put("createdate",new Date());
        claims.put("id",user.getId());
        claims.put("userLevelId",user.getLevelId());

        return Jwts.builder()
                .setClaims(claims)
                .setHeaderParam("typ", "JWT")
                .setExpiration(new Date(System.currentTimeMillis()+jwtProperties.getExpiration()*1000))
                .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecret())
                .compact();
    }

    /**
     * 解码token
     * @param jwtToken
     * @return
     */
    public Claims parseJwtToken(String jwtToken){
        Claims claims=null;
        try {
            claims=Jwts.parser()
                    .setSigningKey(jwtProperties.getSecret())
                    .parseClaimsJws(jwtToken)
                    .getBody();
        }catch (ExpiredJwtException e){
            throw new MyException("JWTtoken过期");
        }catch (UnsupportedJwtException e){
            throw new MyException("JWTtoken格式不支持");
        }catch (MalformedJwtException e){
            throw new MyException("无效的token");
        }catch (SignatureException e){
            throw new MyException("无效的token");
        }catch (IllegalArgumentException e){
            throw new MyException("无效的token");
        }
        return claims;
    }
}

再将这个工具类注入到Bean容器中:

@Configuration
public class JwtConfiguration {

    @Bean
    public JwtKit jwtKit(){
        return new JwtKit();
    }
}

然后就可以愉快地使用了

@RestController
@RequestMapping("/sso")
public class UserController extends BaseController {

    @Autowired
    private UserService userService;

    @Autowired
    private JwtKit jwtKit;

    @Autowired
    private JwtProperties jwtProperties;

    @PostMapping("jwtlogin")
    public CommonResult jwtLogin(@RequestParam String username,@RequestParam String password){
        User user = userService.login(username, password);
        if (user!=null){
            Map<String,Object> map=new HashMap<>();
            String token=jwtKit.generateJwtToken(user);
            map.put("tokenHead",jwtProperties.getTokenHead());
            map.put("token",token);
            return new CommonResult(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMsg(),map);
        }
        return new CommonResult(ResponseCode.USER_NOT_EXISTS.getCode(),ResponseCode.USER_NOT_EXISTS.getMsg(),"");
    }
}

登陆成功的话将token的开头和实际token值返回给后端。

接着编写拦截器,拦截除/sso/*外的其他请求,这些请求是需要登陆才可以访问的。

@Configuration
public class IntercepterConfiguration implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        List list=new ArrayList();
        list.add("/sso/**");
        registry.addInterceptor(authInterceptorHandler())
                .addPathPatterns("/**")
                .excludePathPatterns(list);
    }

    @Bean
    public AuthInterceptorHandler authInterceptorHandler(){
        return new AuthInterceptorHandler();
    }
}

接着是对拦截的处理,首先判断header中是否有key为Authorization的数据,如果没有,说明未登陆,将未登录的结果返回出去。如果header中存在key为Authorization的数据,则先判断是否以Bearer开头(这个可以自定义),如果是的话说明登陆了,就直接返回true不拦截。

@Slf4j
public class AuthInterceptorHandler implements HandlerInterceptor {

    @Autowired
    private JwtKit jwtKit;

    @Autowired
    private JwtProperties jwtProperties;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("进入拦截器");
        String aa = request.getHeader("aa");
        String authorization =request.getHeader(jwtProperties.getTokenHeader());
        log.info("Authorization"+authorization);
        //如果不为空,说明head里存了数据,
        if(StringUtils.isNotEmpty(authorization) && authorization.startsWith(jwtProperties.getTokenHead())){
            String authToken = authorization.substring(jwtProperties.getTokenHead().length());
            Claims claims=null;
            try {
                claims=jwtKit.parseJwtToken(authToken);
                if (claims!=null){
                    return true;
                }
            }catch (MyException e){
                log.info(e.getMessage()+":"+authToken);
            }
        }

        response.setHeader("Content-Type","application/json");
        response.setCharacterEncoding("UTF-8");
        String result = new ObjectMapper().writeValueAsString(new CommonResult(ResponseCode.NEED_LOGIN.getCode(), ResponseCode.NEED_LOGIN.getMsg(), ""));
        response.getWriter().println(result);
        return false;
    }
}

最后写一个测试类:

@RestController
public class IndexController extends BaseController{

    @Autowired
    private JwtProperties jwtProperties;
    @Autowired
    private JwtKit jwtKit;

    @RequestMapping(value = "/index",method = RequestMethod.GET)
    public CommonResult index(){

        String authorization = getRequest().getHeader(jwtProperties.getTokenHeader());
        String authToken = authorization.substring(jwtProperties.getTokenHead().length());
        Claims claims=jwtKit.parseJwtToken(authToken);
        return new CommonResult(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMsg(),claims.get("sub"));
    }
}

(五)效果测试

首先我直接访问http://localhost:8189/index,返回结果为登陆失效,因为没有传header

于是先访问登陆接口:http://localhost:8189/sso/jwtlogin

在这里插入图片描述

返回了具体的tokenHead和token实际值。
将这个token放进header里,再次访问index接口:

在这里插入图片描述

操作成功,并且可以取到用户信息。

(六)JWT和Session的对比

前面一篇文章我用了Session实现了认证的功能,但是需要额外的Redis服务器才可以实现分布式的认证。而使用JWT不需要额外的服务器,它是把token放在header中的。

但是JWT同样存在缺点,最明显的就是这段token无法让他手动失效,生成token后,就算注销登陆,token依然是有效的。

第二点缺点是人员信息是base64后的数据,相当于只要拿到就可以被使用,因此JWT只能传输非敏感的人员数据

第三点缺点是由于每次请求都需要在header中携带token信息,增大了带宽的压力。别觉得一个请求的header就多占用了那么一点带宽,如果是一万个或者是十万个请求呢?带宽的资源是很珍贵的。

综上所述,JWT和Session各有优劣,如何使用就看你的系统情况了。好了,有什么问题评论区留言,我们下期再见!

相关文章:

  • hadoop家族成员
  • 项目经理最近感觉系统慢了,想知道整个系统每个方法的执行时间
  • 获得指定文件夹所有文件的路径
  • 面试官问我:Zookeeper实现分布式锁的原理是什么?
  • typedef与#define的区别
  • 一步步教你如何在SpringBoot项目中引入支付功能
  • OSChina 周三乱弹 ——你是有多寂寞啊,看光头强都……
  • 今天不聊技术,谈谈我眼中的程序员到底是个怎样的职业
  • 关于JVM调优,我理了一些工具和思路出来
  • 2016年4月20日***学习总结
  • 关于ThreadLocal的九个知识点,看完别再说不懂了
  • Java程序员需要知道的操作系统知识汇总(持续更新)
  • Tkinter之输入框操作
  • 平稳运行半年的系统宕机了,记录一次排错调优的全过程!
  • 服务发现、配置中心,Nacos帮我们都搞定了
  • IE9 : DOM Exception: INVALID_CHARACTER_ERR (5)
  • 《Java8实战》-第四章读书笔记(引入流Stream)
  • Codepen 每日精选(2018-3-25)
  • HTTP传输编码增加了传输量,只为解决这一个问题 | 实用 HTTP
  • Iterator 和 for...of 循环
  • Koa2 之文件上传下载
  • Laravel Telescope:优雅的应用调试工具
  • LeetCode算法系列_0891_子序列宽度之和
  • Mac转Windows的拯救指南
  • nginx 负载服务器优化
  • Quartz初级教程
  • React-生命周期杂记
  • Spring Cloud Alibaba迁移指南(一):一行代码从 Hystrix 迁移到 Sentinel
  • STAR法则
  • 聊聊redis的数据结构的应用
  • 每个JavaScript开发人员应阅读的书【1】 - JavaScript: The Good Parts
  • 微信小程序设置上一页数据
  • 小李飞刀:SQL题目刷起来!
  • ​VRRP 虚拟路由冗余协议(华为)
  • ​虚拟化系列介绍(十)
  • #AngularJS#$sce.trustAsResourceUrl
  • (C++20) consteval立即函数
  • (NO.00004)iOS实现打砖块游戏(九):游戏中小球与反弹棒的碰撞
  • (附源码)springboot码头作业管理系统 毕业设计 341654
  • (剑指Offer)面试题34:丑数
  • (七)MySQL是如何将LRU链表的使用性能优化到极致的?
  • (转)母版页和相对路径
  • (转载)深入super,看Python如何解决钻石继承难题
  • ./configure,make,make install的作用(转)
  • .gitattributes 文件
  • .NET MVC 验证码
  • .net 反编译_.net反编译的相关问题
  • .NET/C# 使用 #if 和 Conditional 特性来按条件编译代码的不同原理和适用场景
  • .php结尾的域名,【php】php正则截取url中域名后的内容
  • /etc/sudoer文件配置简析
  • ??如何把JavaScript脚本中的参数传到java代码段中
  • @软考考生,这份软考高分攻略你须知道
  • [2010-8-30]
  • [Android]竖直滑动选择器WheelView的实现
  • [BeginCTF]真龙之力