[前端学java12-SpringSecurity] JWT

409 阅读8分钟

导航

[react] Hooks

[封装01-设计模式] 设计原则 和 工厂模式(简单抽象方法) 适配器模式 装饰器模式
[封装02-设计模式] 命令模式 享元模式 组合模式 代理模式

[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick
[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[数据结构和算法01] 二分查找和排序

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 数据结构和算法 - 二分查找和排序
[深入22] js和v8垃圾回收机制
[深入23] JS设计模式 - 代理,策略,单例

[前端学java01-SpringBoot实战] 环境配置和HelloWorld服务
[前端学java02-SpringBoot实战] mybatis + mysql 实现歌曲增删改查
[前端学java03-SpringBoot实战] lombok,日志,部署
[前端学java04-SpringBoot实战] 静态资源 + 拦截器 + 前后端文件上传
[前端学java05-SpringBoot实战] 常用注解 + redis实现统计功能
[前端学java06-SpringBoot实战] 注入 + Swagger2 3.0 + 单元测试JUnit5
[前端学java07-SpringBoot实战] IOC扫描器 + 事务 + Jackson
[前端学java08-SpringBoot实战总结1-7] 阶段性总结
[前端学java09-SpringBoot实战] 多模块配置 + Mybatis-plus + 单多模块打包部署
[前端学java10-SpringBoot实战] bean赋值转换 + 参数校验 + 全局异常处理
[前端学java11-SpringSecurity] 配置 + 内存 + 数据库 = 三种方式实现RBAC
[前端学java12-SpringSecurity] JWT

(一) 前置知识

(1) 一些单词

signature 签名
bearer 持票人

secure 安全的 // security 安全的
organize 组织

interval 间隔
poll 轮询 // polling interval 轮询间隔
brand 品牌 // brand color 品牌色

calendar 日历
algorithm 算法
amount 数量,总计,金额
verify 验证

(2) 传统的session认证

  • session的具体认证流程
    • 1.用户提交用户名,密码到服务器
    • 2.服务器认证通过后,在 session 中保存相关数据,比如用户名,角色,登陆时间,等
    • 3.服务器向客户端返回一个 session_id,并写入 cookie
    • 4.客户端以后的每一次请求,都会携带 cookie,即将 session_id 传回服务器
    • 5.服务器收到 session_id,找到之前保存的数据,由此得知用户的身份
  • sesstion其实就是一个 ( 对象 )
  • cookie <-> sessionId <-> session
  • 缺点
    • 集群服务
    • 跨域
    • csrf
    • 扩展性不好:如果是 ( 集群服务 ),或者需要 ( 跨域 ),则需要session共享,( 每台服务器都要能读取session )
    • 使用cookie携带信息,可能会遭到 csrf攻击
  • 代码测试
@RestController
public class TestSessionController {

    @GetMapping("/session")
    public String getSession(
            @RequestParam("username") String username,
            HttpServletRequest request
    ) {
        // 登陆成功后,将用户信息写入session
        // 用户信息写入session后,会将 ( session_id ) 存入 ( cookie ),以后每次请求都会携带cookie,cookie中有 ( JSESSIONID )
        request.getSession().setAttribute("username", username);
        return "服务端session保存成功";
    }
}

image.png

(3) Base64URL

  • jwt的header和payload ( 串型化 ) 的算法是 ( base64url )
  • jwt作为一个令牌,有些场合可能会放到 URL( 比如 api.example.com/?token=xxx
    • + / = 在url中有特殊含义所以要被替换掉,换成下面三种,就是base64url算法
    • = => 被省略
    • + => 换成 -
    • / => 换成 _

(4) cookie复习

  • 定义:cookie是 ( 服务器 ) 保存在浏览器的 ( 一小段文本信息 )
  • 大小:每个cookie的大小一般不能
  • cookie主要作用
    • 1.分辨两个请求是否来自于同意服务器
    • 2.保存一些状态信息
  • cookie包含的内容
    • cookie的名字
    • cookie的值,里面是 ( 真正的数字 )
    • 到期时间
    • 所属域名 - 默认是当前域名
    • 生效的路径 - 莫扔是当前网址
    • 浏览器可以设置不接受cookie,也可以设置不向服务器发送cookie
  • 如何查看浏览器是否打开cookie?
    • window.navigator.cookieEnabled
  • 如何返回当前网页的cookie?
    • document.cookie
  • 共享cookie的条件?
    • 只要 ( 域名 ) 和 ( 端口 ) 相同,不需要 协议 相同
    • 说明 a.com 设置的cookie,在 a.com 也可以共享
  • http回应 =====> cookie的生成【【set-cookie】】
    • 服务器在浏览器保存cookie,要在 ( http回应 )( 头信息 ) 里放置 ( set-cookie ) 字段
    • 如果包含多个 set-cookie 字段则表示设置了多个cookie保存在浏览器中
    • 除了cookie的值,还可以设置其他属性
  • http请求 =====> cookie的发送 【【cookie】】
    • ( cookie字段 ) 可以包含 ( 多个cookie ),用 ( ; ) 分割
  • 如何改变先前设置的cookie????????
    • 必须同时满足4个条件,即必须四个都匹配,只要一个值不一样就会重新生成cookie
      • key
      • domain
      • path
      • secure
  • 服务器收到cookie时,有两点是不知道的
    • cookie的各种属性,比如过期时间
    • 哪个域名设置的cookie,一级域名还是二级域名
  • cookie的属性
    • Expires Max-Age
    • Expires
      • Expires 指定一个具体的 到期时间UTC格式
      • 如果 ( 不设置Expires ) 或者 ( Expires设置为null ) 则相当于 ( session ),即相当于只要关闭浏览器窗口session结束,cookie就会被删除
      • 浏览器是根据 ( 本地时间 ) 决定cookie是否过期
    • Max-Age
      • 表示从现在开始,cookie存在的秒数
      • 如果 Set-Cookie 没有指定 Expires 或者 Max-Age,则是 session-cookie
    • Domain
      • Domain属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie
      • 如果没有指定该属性,浏览器会默认将其设为当前 URL 的一级域名
      • 如果服务器在Set-Cookie字段指定的域名,不属于当前域名,浏览器会拒绝这个 Cookie
    • Path
      • Path属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie
    • Secure // security 安全
      • Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器
    • HttpOnly
      • HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到
        • 主要是
        • Document.cookie属性
        • XMLHttpRequest对象
        • Request API
        • 都拿不到该属性

(5) Calendar 和 Date

(1) 实例化
Date date = new Date();
Calendar calendar = Calendar.getInstance();
log.info("calendar{}", calendar.toString());
log.info("date{}", date.toString());
(2) 实例化特定的时间
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Date date = dateFormat.parse("2020-02-02 02:20:20");
String format = dateFormat.format(date);
log.info("format{}", format);

String parse2 = dateFormat.format(new Date());
log.info("parse2{}", parse2);


Calendar instance = Calendar.getInstance();
instance.setTime(date);
Date time = instance.getTime();
log.info("time{}", time);

(6) ( jackson - ObjectMapper ) 实现 ( map ) 和 ( object ) 和 ( json ) 三者的相互转换

  • 使用 jackson 来实现
  • map object json
  • new ObjectMapper()
    • writeValueAsString => 将各种类型转成json
    • readValue => 将json转成各种值
    • convertValue => 各种值的相互转换
@SpringBootTest
@Slf4j
public class JwtTest {

    @Test
    public void object2map() throws JsonProcessingException {
        JwtUserBean jwtUserBean = new JwtUserBean(1, "admin", "admin", "admin"); // bean
        HashMap<String, Object> stringObjectHashMap = new HashMap<>();
        stringObjectHashMap.put("a", "a");
        stringObjectHashMap.put("b", "b");

        // object => json
        ObjectMapper objectMapper = new ObjectMapper(); // jackson => ObjectMapper
        String s = objectMapper.writeValueAsString(jwtUserBean); // writeValueAsString()
        log.info("object => json: {}", s);

        // map => json
        ObjectMapper objectMapper1 = new ObjectMapper(); // jackson => ObjectMapper
        String s1 = objectMapper1.writeValueAsString(stringObjectHashMap);  // writeValueAsString()
        log.info("map => json: {}", s1);

        // json => object
        ObjectMapper objectMapper3 = new ObjectMapper(); // jackson => ObjectMapper
        JwtUserBean jwtUserBean1 = objectMapper3.readValue(s, JwtUserBean.class);
        log.info("json => object: {}", jwtUserBean1);

        // json => map
        ObjectMapper objectMapper4 = new ObjectMapper(); // jackson => ObjectMapper
        Map map = objectMapper4.readValue(s1, Map.class);
        log.info("json => map: {}", map);

        // object => map !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ( object和map的相互转化 )
        ObjectMapper objectMapper5 = new ObjectMapper(); // jackson => ObjectMapper
        Map map1 = objectMapper5.convertValue(jwtUserBean, Map.class);
        log.info("object => map: {}", map);
    }
}

(二) JWT

  • JWT
    • JWT是 json web tokens 的缩写
    • 服务端不在保存session,即服务端变成无状态的了

(1) JWT的数据结构

  • JWT表现上是一个很长的字符串,用 ( . ) 分成三段
    • header 头部
    • payload 负载
    • signature 签名
    • header.payload.signature
  • header
    • header是一个json对象
    • header对象中有两个属性 alg typ
    • alg 表示签名算法 => 默认是 hs256
    • typ 表示令牌的类型 => 一般都是 JWT
       {
          "alg": "HS256",
          "typ": "JWT"
       }
    
  • payload
    • payload也是一个json对象,用来存放实际需要传递的数据
    • 注意:jwt默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分
  • signature
    • signature是对前两部分的签名,防止数据串改

(2) JWT的存储

  • 可以存储在 ( cookie ) 中,也可以存储在 ( localStorage ) 中
  • 发送请求时,最好是放在 header 头中,也可以放在cookie中但是不能跨域
    • Authorization: Bearer <token>

(3) JWT在springboot中的使用

(3.1) 安装依赖

<!-- JWT  -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.15.0</version>
</dependency>

(3.2) 生成jwt

@RestController
@Slf4j
public class JwtTest {
    @GetMapping("/jwt-test")
    public void testJwt() throws ParseException {
        HashMap<String, Object> HeaderMap = new HashMap<>();
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.SECOND, 2000); // 2000s 过期

        String token = JWT.create() // 生成jwt
                .withHeader(HeaderMap) // header
                .withClaim("username", "woow_wu7") // ------ payload
                .withClaim("age", 20) // ------------------- payload
                .withExpiresAt(instance.getTime()) // 过期时间
                .sign(Algorithm.HMAC256("secretXx"));// ---------------- signature
        log.info("token: {}", token);
}

image.png

(3.3) 验证jwt

根据 ( 令牌 ) 和 ( 签名 ) 解析数据


@RestController
@Slf4j
public class JwtTest {
    @GetMapping("/jwt-test")
    public void testJwt() throws ParseException {
    
        // 1. JWT加密
        HashMap<String, Object> HeaderMap = new HashMap<>();
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.SECOND, 2000); // 2000s 过期
        String token = JWT.create() // 生成jwt
                .withHeader(HeaderMap) // header
                .withClaim("username", "woow_wu7") // ------ payload
                .withClaim("age", 20) // ------------------- payload
                .withExpiresAt(instance.getTime()) // 过期时间
                .sign(Algorithm.HMAC256("secretXx"));// ---------------- signature 签名    (algorithm是算法的意思)
        log.info("token: {}", token);


        // 2. JWT验证
        // secretXx 生成验证对象
        // 注意:Algorithm.HMAC256() 的参数就是上面生成jwt时 ( 签名 ) 时传入的签名字符串
        JWTVerifier secretXx = JWT.require(Algorithm.HMAC256("secretXx")).build();
        DecodedJWT verify = secretXx.verify(token);
        String username = verify.getClaim("username").asString(); // -- 通过token,获取username,注意过期时间,过期后是拿不到username的
        Integer age = verify.getClaim("age").asInt(); // -------------- 通过token,获取age
        log.info("传入token,验证token中的username:{}", username);
        log.info("传入token,验证token中的age:{}", age);

        Date expiresAt = verify.getExpiresAt(); // -------------------- 通过token,获取过期时间
        log.info("token的过期时间:{}", expiresAt);
    }
}

image.png

(3.4) JWT常见的异常信息

- SignatureVerificationException ------ 签名不一致异常
- TokenExpiredException --------------- token过期异常
- AlgorithmMismatchException ---------- 算法不匹配异常,algorithm是算法的意思
- InvalidClaimException --------------- 失效的payload异常

(3.5) JWT的封装

public class JwtUtil {
    
    // private 表示只能自己访问,子类和其他类都不能访问
    // static 静态类,表示可以通过类本身来访问
    // final 表示非继承类,不能被其他类继承
    private static final String SIGNATURE = "randomString!";

    // JWT 生成
    // token => header.payload.signature
    public static String getToken(Map<String, Object> map) {
        // 过期时间设置
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE, 7); // ------------ 过期时间,这里是天为单位,即7天后过期
        JWTCreator.Builder builder = JWT.create(); // ----------------- jwt
        map.forEach((k, v) -> {
            builder.withClaim(String.valueOf(k), String.valueOf(v)); // jwt payload
        });
        String token = builder
                .withExpiresAt(instance.getTime()) // ----------------- jwt 过期时间
                .sign(Algorithm.HMAC256(SIGNATURE));// ---------------- jwt signature
        return token;
    }
    
    // JWT 验证 + 获取信息
    // 验证token的合法性,并获取token中的信息
    public static DecodedJWT verify(String token) {
        DecodedJWT verify = JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
        return verify; // 返回 verify 则可以通过 verify.getClaim() 获取到 ( token ) 中存入的 ( payload )
    }
}

(3.6) JWT在springboot中的使用

(1) 登陆成功返回token
@RestController
@Slf4j
public class JwtLoginTestController {

    // 如果service是通过实现类实现的接口,这里还是注入interface
    @Autowired
    JwtLoginTestServiceInterface jwtLoginTestServiceInterface;

    @GetMapping("/jwt-login")
    public Object getJwtUser(
            @RequestParam String username,
            @RequestParam String password
    ) {
        HashMap<Object, Object> stringObjectHashMap = new HashMap<>(); // ------------------- 结果map

        Object jwtUser = jwtLoginTestServiceInterface.getJwtUser(username, password); // ---- object,查询数据库返回值
        Map map = new ObjectMapper().convertValue(jwtUser, Map.class); //-------------------- object => map
        String token = JwtUtil.getToken(map); // 生成token
        try {
            stringObjectHashMap.put("msg", "请求成功");
            stringObjectHashMap.put("token", token);
        } catch (Exception e) {
            stringObjectHashMap.put("msg", "请求失败");
            stringObjectHashMap.put("data", e.getMessage());

        }
        return stringObjectHashMap;
    }
}
(2) 访问接口,做token验证,是否过期expire,捕捉异常exception等

@Test
    private  Map<String, Object> testToken2() {
        HashMap<String, Object> stringObjectHashMap = new HashMap<>();
        try {
            JwtUtil.verify("\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6ImFkbWluIiwicm9sZXMiOiJhZG1pbiIsImlkIjoiMSIsImV4cCI6MTYyMzU3MTU5MywidXNlcm5hbWUiOiJhZG1pbiJ9.JNME8wbaedF1EYyr9agbTs9pmTxkQ8Iwxh0WJB1Zwig\"");
            // 验证成功
            stringObjectHashMap.put("msg", "请求成功");
            return stringObjectHashMap;
        } catch (SignatureVerificationException e) { // --------- 签名错误
            e.printStackTrace();
        } catch (TokenExpiredException e) { // ------------------ 过期
            e.printStackTrace();
        } catch (AlgorithmMismatchException e) { // ------------- 算法不匹配
            e.printStackTrace();
        } catch (InvalidClaimException e) { // ------------------ 无效payload
            e.printStackTrace();
        }
        
        stringObjectHashMap.put("msg", "请求失败");
        return stringObjectHashMap;
    }

image.png