欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

【Shiro 自学笔记七】 什么是 JWT?Shiro 整合JWT

程序员文章站 2022-03-26 20:23:30
文章目录什么是 JWTSession 的缺陷JWT 的结构JWT 的生成和解析导入依赖编写工具类测试基于 JWT 的用户认证编写拦截器配置拦截规则编写控制器测试主流的双 token 认证方案主流的双 token 认证方案JWT 全称 JSON Web Token,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。本期内容与 Shiro 无关,不过在下一期我们就会整合 Shiro 和 JWT。什么是 JWTJWT 是一种可以携带信息的加密串,加密时可以将各...

JWT 全称 JSON Web Token,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。本期内容与 Shiro 无关,不过在下一期我们就会整合 Shiro 和 JWT。

什么是 JWT

JWT 是一种可以携带信息的加密串,加密时可以将各种信息,如用户、作者、过期时间等,并设定签名(密钥)。

解密时,只要提供签名(密钥),token 就可以被解析得到信息,从而实现一种相对安全的前后端交互方式。

Session 的缺陷

传统的认证采用 Session 的形式,用户登录成功后,就将用户信息以 Session 形式存入服务器内存,并为用户发送 Cookie 保存登录信息。下次用户登录时,通过检验 Cookie 和 Session 信息,判断认证是否有效。

  • 浪费资源,Session 保存在服务器内存中,开销很大
  • 因为是基于 Cookie 来进行用户识别的, Cookie 如果被截获,用户就会很容易受到跨站请求伪造的攻击
  • 由于 Session 存在内存中,如果是分布式应用,服务器之间共享内存,不利于应用扩展

JWT 的结构

一个生成的 token 如下:eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJrb29yeWUiLCJzdWIiOiJzaGlyb19kZW1vIiwiYXVkIjoia29vcnllIiwiaWF0IjoxNTk1NjYwNTQxLCJleHAiOjE1OTU2NjQxNDF9.EMq7pVog37X3Un0FVgx2qP8sULpd5haXvdU1qvzKZYo

token 分为3部分,用 . 分割:

  • 第一部分 头部信息
  • 第二部分 载荷信息
  • 第三部分 签名信息

JWT 官网提供了清晰的例子,头部包含加密算法等信息,中间包含我们传入的信息,尾部则包含密钥、用于验证:

【Shiro 自学笔记七】 什么是 JWT?Shiro 整合JWT

JWT 的生成和解析

一个标准的 JWT 应该具有的信息:

  • iss: jwt 签发者
  • sub: jwt 所面向的用户
  • aud: 接收 jwt 的一方
  • exp: jwt 的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该 jwt 都是不可用的
  • iat: jwt 的签发时间
  • jti: jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击

导入依赖

导入 jjwt 包:

    <!-- JWT -->
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.1</version>
    </dependency>

编写工具类

笔者此处将一些固定的信息,如签发者、主题、签名等以常量形式存储。

生成 token 时,需要调用 Jwts.builder 存入信息,然后使用 compact 得到 token 字符串。

解析token 时,调用 Jwts.parser,传入签名即可。

例:

package org.koorye.util;

import io.jsonwebtoken.*;

import java.util.Date;

public class JwtUtil {
    public static final String ISSUER = "koorye";
    public static final String SUBJECT = "shiro_demo";
    public static final String SIGN = "koorye_love_jwt";

    public static String getToken(String username, int expireTime) {
        Date currentTime = new Date();

        JwtBuilder jwtBuilder = Jwts.builder()
                .setIssuer(ISSUER)
                .setSubject(SUBJECT)
                .setAudience(username)
                .setIssuedAt(currentTime)
                .setExpiration(new Date(System.currentTimeMillis() + expireTime * 1000))
                .signWith(SignatureAlgorithm.HS256, SIGN);

        return jwtBuilder.compact();
    }

    public static Claims parseToken(String token) {
        return Jwts.parser().setSigningKey(SIGN).parseClaimsJws(token).getBody();
    }
}

测试

第二个参数是过期时间(单位秒):

    @Test
    public void testToken() {
        String token = JwtUtil.getToken("koorye", 60 * 60);
        System.out.println(token);
        System.out.println("=======================");
        Claims claims = JwtUtil.parseToken(token);
        System.out.println("Audience: " + claims.getAudience());
        System.out.println("Subject: " + claims.getSubject());
    }

运行:

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJrb29yeWUiLCJzdWIiOiJzaGlyb19kZW1vIiwiYXVkIjoia29vcnllIiwiaWF0IjoxNTk1NjYwNTQxLCJleHAiOjE1OTU2NjQxNDF9.EMq7pVog37X3Un0FVgx2qP8sULpd5haXvdU1qvzKZYo
=======================
Audience: koorye
Subject: shiro_demo

基于 JWT 的用户认证

编写拦截器

使用原生 JWT 完成认证,我们可以自定义拦截器来实现:

  • returnJson 用于返回信息
  • preHandle 重写,定义拦截规则

在拦截器中,我们检测 Header 中是否包含 access_token 信息,如果不包含,说明未登录。

如果包含:

  • 如果 token 解析失败,说明 token 错误
  • 如果 token 解析成功,但是过期异常,说明 token 过期

都通过则说明 token 可用,返回 true:

@Component
public class JwtInterceptor extends HandlerInterceptorAdapter {

    private void returnJson(HttpServletResponse response, String json) throws Exception {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");
        try (PrintWriter writer = response.getWriter()) {
            writer.print(json);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("access_token");
        if (token == null) {
            System.out.println("[ ERROR ] Token is NULL.");

            Map<String, String> map = new HashMap<>();
            map.put("ret_code", "401");
            map.put("err_msg", "Token is NULL.");
            response.setStatus(401);

            returnJson(response, JSON.toJSONString(map));
            return false;
        } else {
            try {
                Claims claims = JwtUtil.parseToken(token);
            } catch (MalformedJwtException e) {
                System.out.println("[ ERROR ] Token is ERROR.");

                Map<String, String> map = new HashMap<>();
                map.put("ret_code", "402");
                map.put("err_msg", "Token is ERROR.");
                response.setStatus(403);
                returnJson(response, JSON.toJSONString(map));

                return false;
            } catch (ExpiredJwtException e) {
                System.out.println("[ ERROR ] Token is EXPIRED.");

                Map<String, String> map = new HashMap<>();
                map.put("ret_code", "403");
                map.put("err_msg", "Token is EXPIRED.");
                response.setStatus(403);
                returnJson(response, JSON.toJSONString(map));

                return false;
            }
        }
        return true;
    }
}

配置拦截规则

在 Spring Boot 中,我们可以使用配置类的形式实现。

除了登录和注册,其他请求都需要拦截:

@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
    @Autowired
    private JwtInterceptor jwtInterceptor;

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/login")
                .excludePathPatterns("/api/register");
    }
}

编写控制器

主要关注认证成功的部分 isAuthenticated,认证成功时,生成一个 token 传入返回体:

    @RequestMapping("/api/login")
    public ResponseEntity<Map<String, String>> login(String username, String password) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);
        } catch (UnknownAccountException e) {
            Map<String, String> map = new HashMap<>();
            map.put("ret_code", "401");
            map.put("err_msg", "Username is not EXISTED.");
            return new ResponseEntity<>(map, HttpStatus.BAD_REQUEST);
        } catch (AuthenticationException e) {
            Map<String, String> map = new HashMap<>();
            map.put("ret_code", "402");
            map.put("err_msg", "Password is ERROR.");
            return new ResponseEntity<>(map, HttpStatus.BAD_REQUEST);
        }

        if (subject.isAuthenticated()) {
            String access_token = JwtUtil.getToken(username, 30 * 60);  // 设置 30 分钟过期
            Map<String, String> map = new HashMap<>();
            map.put("ret_code", "201");
            map.put("access_token", access_token);
            return new ResponseEntity<>(map, HttpStatus.OK);
        } else {
            Map<String, String> map = new HashMap<>();
            map.put("ret_code", "403");
            map.put("err_msg", "Login failed.");
            return new ResponseEntity<>(map, HttpStatus.BAD_REQUEST);
        }
    }

测试

我们使用 postman 测试一下。

模拟登录:

【Shiro 自学笔记七】 什么是 JWT?Shiro 整合JWT

返回体:

{
    "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJrb29yeWUiLCJzdWIiOiJzaGlyb19kZW1vIiwiYXVkIjoia29vcnllIiwiaWF0IjoxNTk1NjYxNzgyLCJleHAiOjE1OTU2NjM1ODJ9.AqhHVRw7FiOMv3y79XAelkVLgfeQzrmCmqYYPg1ouOY",
    "ret_code": "201"
}

模拟访问页面,访问时提供 access_token:

【Shiro 自学笔记七】 什么是 JWT?Shiro 整合JWT

返回体:

You are an admin.

如果 token 被修改:

{"err_msg":"Token is ERROR.","ret_code":"402"}

如果 token 过期:

{"err_msg":"Token is EXPIRED.","ret_code":"403"}

主流的双 token 认证方案

目前的主流方案是使用双 token 认证。

用户登录成功后,服务端给用户传递两个 token:

  • access_token 认证用 token,存储主体信息,过期时间较短(如 30 分钟)
  • refresh_token 刷新用 token,过期时间较长(如一星期)

用户登录时,首先发送 access_token,如果请求成功,则放行。

如果请求失败,提供 access_token 过期,则发送 refresh_token:

  • 如果 refresh_token 没过期,则给用户传递一个新的 access_token 和 refresh_token
    ``

主流的双 token 认证方案

目前的主流方案是使用双 token 认证。

用户登录成功后,服务端给用户传递两个 token:

  • access_token 认证用 token,存储主体信息,过期时间较短(如 30 分钟)
  • refresh_token 刷新用 token,过期时间较长(如一星期)

用户登录时,首先发送 access_token,如果请求成功,则放行。

如果请求失败,提供 access_token 过期,则发送 refresh_token:

  • 如果 refresh_token 没过期,则给用户传递一个新的 access_token 和 refresh_token
  • 如果 refresh_token 过期,则不放行

本文地址:https://blog.csdn.net/weixin_45901207/article/details/107582409