JWT-认证原理

JWT 能做什么

  • 授权

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

  • 信息交换

    JSON Web Token 是在各方之间安全地传输信息地好方法,因为可以对JWT 进行签名(例如: 使用公钥/私钥对),此外,由于签名是使用标头和有效负载计算的,因此可以验证内容是否遭到篡改

为什么是JWT

基于传统的Session 认证
  • Session

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

    • 认证流程

      认证流程
      认证流程
    • 发送请求将携带cookie

      cookie
      cookie
    • 产生问题

      1. 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session 都是保存在内存中,而随着认证用户的增多,服务器的开销会明显增大

      2. 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的化,这意味着用户下次请求还必须要请求,这样才能拿到授权的资源,这样在分布的应用上,相应的限制了负载均衡的能力,这也意味着限制了应用的扩展能力

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

      4. 在前后端分离系统中更加痛苦:

        也就是说前后端分离在应用解耦后增加了部署的复杂性,通常用户一次请求就要转发多次,如果用sessionid 到服务器,服务器还要查询用户信息,同时,如果用户很多,这些信息存储在服务器内存中,给服务器增加了负担,还有就是CSRF(跨站伪造请求攻击),session 是基于cookie 进行用户识别的,cookie 如果被捕获了,用户就很容易受到跨站伪造攻击,还有就是sessionid 就是一个特征值,表达的信息不够丰富,不容易扩展,而且如果你后端应用是多节点部署,那么就需要实现session 共享机制,不方便应用集群

基于JWT认证
  • 认证流程图

    JWT-认证流程
    JWT-认证流程
  • 认证流程

    • 首先,前端通过web 表单将自己的用户名和密码发送到后端接口,这一过程一般是HTTP POST 请求,建议的方式是用过SSL 加密的传输(https 协议),从而避免敏感的信息被嗅探

    • 后端核对用户名和密码成功后,将用户的id 等其他信息作为JWT Payload(负载),将其与头部分别进行Base64 编码拼接后签名,形成一个JWT,形成的JWT 是一个形同xxx.xxx.xxx 的字符串

    • 后端将JWT 字符串作为登录成功的返回结果返回给前端,前端可以将返回结果保存在localStoragesessionStorage 上,退出登录时前端删除保存的JWT 即可

    • 前端在每次请求时将JWT 放入HTP Header 中的Authorization(解决XSSXSRF问题)

    • 验证通过后后端使用JWT 中包含的用户信息进行其他逻辑操作,返回响应的结果

  • JWT 的优势

    • 简洁: 可以通过URL、POST 参数或者在HTTP Header 发送,因为数据量小,传输速度也很快
    • 自包含:负载中包含了所有用户所需的信息,避免了多次查询数据库
    • 因为Token 是以JSON 加密的形式保存在客户端的,所以JWT 是跨语言的,原则上任何web 形式都支持
    • 不需要在服务端保存会话信息,特别适用于分布式微服务

JWT 结构

  • 令牌组成

    1. 标头(Header)
    2. 有效载荷(Payload)
    3. 签名(Signature)
  • Header

    标头通常由两部分组成: 令牌的类型(即JWT)和所使用的签名算法,例如MAC、SHA256、RSA 他会使用Base64 编码组成JWT 结构的第一部分

    Base64 是一种编码,也就是说,它是可以被翻译回原来的样子的,它并不是一种加密过程

    1
    2
    3
    4
    {
    "alg": "HS256",
    "typ": "JWT"
    }
  • Payload

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

  • Signature

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

    • 签名目的

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

    • 信息安全问题

      Base64 是可逆的,那么信息不久被暴露了吗?

      因此,JWT 中,不应该在负载里面加入任何敏感的数据

第一次使用JWT

  • 添加依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.19.2</version>
    </dependency>
  • 创建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    * 获取token
    */
    @Test
    void getToken() {
    HashMap<String, Object> map = new HashMap<>();
    Calendar instance = Calendar.getInstance();
    // 20
    instance.add(Calendar.SECOND, 20);
    String token = JWT.create()
    .withHeader(map) // header(可以省略)
    .withClaim("userId", "xiaohong") // payload: 数据
    .withExpiresAt(instance.getTime()) // 指定令牌的过期时间
    .sign(Algorithm.HMAC256("coder-itl")); // 签名
    System.out.println(token);
    }

  • 验证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * 签名验证
    */
    @Test
    void signatureToken() {
    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("coder-itl")).build();
    DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NTc4NjM5ODIsInVzZXJJZCI6InhpYW9ob25nIn0.e7-0F99BvA-q9GhH1DZB7ze9b35aaQ8iLkQ3JeQAVEo");
    String userId = verify.getClaim("userId").asString();
    System.out.println("userId = " + userId);
    }
  • 常见异常

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

封装工具类

  • 工具类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    package com.coderitl.jwt.utils;

    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTCreator;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.interfaces.DecodedJWT;
    import jdk.nashorn.internal.parser.Token;

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

    public class JWTUtils {
    /* 密钥数据 */
    private static final String SIGN = "coder-itl";

    /**
    * 生成 token:
    * header.payload.sing
    */
    public static String getToken(Map<String, String> map) {
    Calendar instance = Calendar.getInstance();
    /* 过期时间: 默认7天 */
    instance.add(Calendar.DATE, 7);
    /* 创建 JWT Builder */
    JWTCreator.Builder builder = JWT.create();
    /* payload */
    map.forEach((k, v) -> {
    builder.withClaim(k, v);
    });
    /* 生成 token 令牌,指定令牌过期时间 */
    String token = builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SIGN));
    return token;
    }

    /**
    * 验证 token 合法性,异常抛出信息,正确返回结果
    */
    public static DecodedJWT verify(String token) {
    return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
    }

    }

  • 拦截器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    package com.coderitl.intercepter;

    import com.auth0.jwt.exceptions.AlgorithmMismatchException;
    import com.auth0.jwt.exceptions.SignatureVerificationException;
    import com.auth0.jwt.exceptions.TokenExpiredException;
    import com.auth0.jwt.interfaces.DecodedJWT;
    import com.coderitl.jwt.utils.JWTUtils;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.web.servlet.HandlerInterceptor;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.HashMap;
    import java.util.Map;

    public class JWTInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    Map<String, Object> map = new HashMap<>();
    // 获取请求头中的 map
    String token = request.getHeader("token");
    try {
    // 验证令牌
    JWTUtils.verify(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();
    // TODO: 消息未响应在页面
    map.put("msg", "token无效!");
    }
    // 设置状态
    map.put("status", false);
    // 将 map 转换为 json jackson(@ResponseBody)
    String json = new ObjectMapper().writeValueAsString(map);
    response.setContentType("application/json;charset=UTF-8");
    response.getWriter().write(json);
    return false;
    }
    }

  • 配置拦截器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.coderitl.config;

    import com.coderitl.intercepter.JWTInterceptor;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    @Configuration
    public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new JWTInterceptor())
    // 其他所有请求都需要携带
    .addPathPatterns("/**")
    // 生成令牌(排除此路径)
    .excludePathPatterns("/user/login");
    }
    }

  • 控制器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    package com.coderitl.controller;


    import com.auth0.jwt.exceptions.AlgorithmMismatchException;
    import com.auth0.jwt.exceptions.SignatureVerificationException;
    import com.auth0.jwt.exceptions.TokenExpiredException;
    import com.auth0.jwt.interfaces.DecodedJWT;
    import com.coderitl.entity.User;
    import com.coderitl.jwt.utils.JWTUtils;
    import com.coderitl.service.UserService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestController;

    import java.security.SignatureException;
    import java.util.HashMap;
    import java.util.Map;

    @RestController
    @Slf4j
    @ResponseBody
    public class UserController {
    @Autowired
    private UserService service;

    @GetMapping("/user/login")
    public Map<String, Object> login(User user) {
    log.info("用户名: [{}]", user.getUsername());
    log.info("密码: [{}]", user.getPassword());
    Map<String, Object> map = new HashMap<>();
    try {
    User userDB = service.login(user);
    Map<String, String> payload = new HashMap<>();
    payload.put("username", userDB.getUsername());
    payload.put("userId", String.valueOf(userDB.getId()));
    String token = JWTUtils.getToken(payload);
    // 响应 token
    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;
    }

    @PostMapping("/user/carrytoken")
    public Map<String, Object> carrytoken() {
    Map<String, Object> map = new HashMap<>();
    // 处理自己的业务逻辑
    map.put("status", true);
    map.put("msg", "请求成功!");
    return map;
    }
    }

  • 测试

    • 生成token

      生成token
      生成token
    • 未携带token 测试

      未携带token 测试
      未携带token测试
    • 携带token 测试

      携带token
      携带token

单体项目认证流程

  • 单体项目session

    单体项目认证流程
    单体项目认证流程
  • 单体项目特点

    在单体项目中,视图资源和接口都在同一台服务器,用户的多次请求都是基于用一个会话session,因此可以借助session 来进行用户认证判断

前后端分离项目认证流程

  • 前端后分离项目

    前端后分离项目
    前端后分离项目

默认方法调用(不需要创建工具类)

  • 添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.19.2</version>
    </dependency>
    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
    </dependency>
  • 创建拦截器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    package com.coderitl.intercepter;

    import com.fasterxml.jackson.databind.ObjectMapper;
    import io.jsonwebtoken.*;
    import org.springframework.web.servlet.HandlerInterceptor;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.PrintWriter;

    public class JWTInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

    String token = request.getParameter("token");
    if (token == null) {
    // 提示请先登录
    R r = new R();
    r.error("请先登录");
    doResponse(response, r);
    } else {
    try {
    // 验证 token
    JwtParser parser = Jwts.parser();
    // 解析 token 的 setSigningKey 必须和生成的 token时设置密码一致
    parser.setSigningKey("签名数据");
    // 如果 token 正确(签名正确,有效时间内)则正常执行,否则抛出异常
    Jws<Claims> claimsJws = parser.parseClaimsJws(token);
    // 放行请求
    return true;
    } catch (ExpiredJwtException e) {
    e.printStackTrace();
    R r = new R();
    r.error("登录过期,请重新登录");
    doResponse(response, r);
    } catch (UnsupportedJwtException e) {
    e.printStackTrace();
    R r = new R();
    r.error("token不合法,请自重!");
    doResponse(response, r);
    } catch (Exception e) {
    e.printStackTrace();
    R r = new R();
    r.error("请登录!");
    doResponse(response, r);
    }
    }
    return false;
    }

    private void doResponse(HttpServletResponse response, R<T> r) throws Exception {
    response.setContentType("application/json");
    response.setCharacterEncoding("utf-8");
    PrintWriter out = response.getWriter();
    String s = new ObjectMapper().writeValueAsString(r);
    out.println(s);
    out.flush();
    out.close();
    }
    }

  • 配置拦截器

    1
    ...