JWT 概念原理及应用学习笔记

简介

JSON Web Token(JWT)是一种开放标准(RFC 7519),它使用一种紧凑且自包含的方式在各方之间作为JSON对象安全地传输信息。此信息经过数字签名,可以被验证和信任。JWT可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。

使用场景

JWT的主要使用场景:

  • Authorization (授权) : 这是使用 JWT 的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在JWT广泛使用的场景,因为它的开销很小,并且可以轻松地跨域使用。

  • Information Exchange (信息交换) : 由于JWT中包含签名,因此可以在传输信息中进行安全控制,主要是进行身份确认和防止内容被篡改。

组成结构

JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:

  • Header
  • Payload
  • Signature

JWT看起来格式如下:

xxxxx.yyyyy.zzzzz

例如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

Header

JWT 的 Header 包含两部分信息:

  • 声明类型,统一是 JWT。
  • 声明加密算法,可以使用HMAC SHA256或者RSA等。

例如:

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

后,用Base64对这个JSON编码就得到JWT的第一部分。

Payload

Payload 是存放有效信息的地方。这些有效信息包含三个部分

  • 标准中注册的声明(Registered claims)。这些是一组预定义的声明,它们不是强制性的,而是推荐的,以提供一组有用的、可互操作的声明。包括:

    • iss: JWT签发者。
    • sub: JWT所面向的用户。
    • aud: 接收JWT的一方。
    • exp: JWT的过期时间,这个过期时间必须要大于签发时间。
    • nbf: 定义在什么时间之前,该JWT都是不可用的。
    • iat: JWT的签发时间。
    • jti: JWT的唯一身份标识,主要用来作为一次性Token,从而回避重放攻击。
  • 公共的声明(Public claims)。公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。为了避免冲突,建议参考 IANA JSON Web Token Registry 进行定义,或者将它们定义为包含防冲突命名空间的URI。

  • 私有的声明(Private claims)。私有声明是使用各方共同定义的声明。

Payload示例:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

有效负载经过 Base64 编码,形成 JWT 的第二部分。

注意,不要将未经加密的敏感信息放在JWT的Payload或Header中。

Signature

JWT的第三部分是一个签证信息。这个部分需要base64加密后的Header和base64加密后的Payload使用 . 连接组成的字符串,然后通过Header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分。

密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。

工作方式

一般是在请求头里加入Authorization,并加上Bearer标注(只要各方约定好即可,不一定非要按此方式):

fetch('api/user/1', {
  headers: {
    'Authorization': 'Bearer ' + token
  }
})

JWT的认证流程如下:

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

  2. 后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token。

  3. 后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可。

  4. 前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)。

  5. 后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等。

  6. 验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果

jwt工作流程.png

优势

传统基于 Session 方式的弊端

在使用 JWT 之前,一般使用基于Session的认证,即我们在用户首次登录成功后,在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户。

这种方式的弊端是:

  • 每个用户的登录信息都会保存到服务器的Session中,随着用户的增多,服务器开销会明显增大。

  • 由于Session是存在与服务器的物理内存中,所以在分布式系统中,这种方式将会失效。虽然可以将Session统一保存到Redis中,但是这样做无疑增加了系统的复杂性,对于不需要Redis的应用也会白白多引入一个缓存中间件。

  • 对于非浏览器的客户端、手机移动端等不适用,因为Session依赖于Cookie,而移动端经常没有Cookie。

  • 因为Session认证本质基于Cookie,所以如果Cookie被截获,用户很容易收到跨站请求伪造攻击。并且如果浏览器禁用了Cookie,这种方式也会失效。

  • 前后端分离系统中更加不适用,后端部署复杂,前端发送的请求往往经过多个中间件到达后端,Cookie中关于Session的信息会转发多次。

  • 由于基于Cookie,而Cookie无法跨域,所以Session的认证也无法跨域,对单点登录不适用。

JWT的优势

对比传统的session认证方式,JWT的优势是:

  • 简洁:JWT Token数据量小,传输速度也很快。

  • 因为JWT Token是以JSON加密形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。

  • 不需要在服务端保存会话信息,也就是说不依赖于Cookie和Session,所以没有了传统Session认证的弊端,特别适用于分布式微服务。

  • 单点登录友好:使用Session进行身份认证的话,由于Cookie无法跨域,难以实现单点登录。但是,使用Token进行认证的话, Token可以被保存在客户端的任意位置的内存中,不一定是Cookie,所以不依赖Cookie,不会存在这些问题.

  • 适合移动端应用:使用Session进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到Cookie(需要 Cookie 保存 SessionId),所以不适合移动端。

实战

下面做一个 SpringBoot 集成JWT的实战。

创建项目

创建一个 SpringBoot 项目,引入 JWT 依赖。


    org.springframework.boot
    spring-boot-starter-parent
    2.3.1.RELEASE



    
        org.springframework.boot
        spring-boot-starter-web
    
    
    
        com.auth0
        java-jwt
        3.4.0
    
    
    
        com.fasterxml.jackson.core
        jackson-core
        2.11.0
    

开发

定义一个用来进行权限验证的注解。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
    boolean required() default true;
}

定义一个用户实体。

public class User {
    private String id;
    private String username;
    private String password;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

编写 token 的生成方法。Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码,withAudience()存入需要保存在token的信息,这里把用户ID存入了token中。

@Service
public class TokenService {

    // 根据用户生成token
    public String getToken(User user) {
        String token = "";
        token = JWT.create().withAudience(user.getId())
                .sign(Algorithm.HMAC256(user.getPassword()));
        return token;
    }
}

编写一些用户方法。由于是demo,也没有数据库,这里就随便写了下。

@Service
public class UserService {

    public User findUserById(String id) {
        User user = new User();
        user.setId("1");
        user.setUsername("wyk");
        user.setPassword("password");
        return user;
    }

    public User findByUserName(User user) {
        user.setId("1");
        return user;
    }
}

接下来需要写一个拦截器去获取 Token并验证 Token。


public class AuthInterceptor implements HandlerInterceptor {
    @Autowired
    UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                             Object object) throws Exception {
        // 设置返回编码,这个应该有单独的地方设置,本项目为了省事放到了这里
        httpServletResponse.setCharacterEncoding("UTF-8");
        // 从 http 请求头token字段中取出token
        // 这个字段应该是约定的,可以使用其他的字段名
        String token = httpServletRequest.getHeader("token");
        // 如果不是映射到方法直接通过
        if(!(object instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod)object;
        Method method = handlerMethod.getMethod();

        // 检查有没有需要用户权限的注解
        if(method.isAnnotationPresent(UserLoginToken.class)) {
            UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
            if(userLoginToken.required()) {
                // 执行认证
                if(token == null) {
                    httpServletResponse.getWriter().write("未发现token,请重新登录");
                    return false;
                }
                // 获取 token 中的 user id
                String userId = "";
                try {
                    userId = JWT.decode(token).getAudience().get(0);
                } catch(JWTDecodeException e) {
                    httpServletResponse.getWriter().write("token不正确");
                    return false;
                }
                User user = userService.findUserById(userId);
                if(user == null) {
                    httpServletResponse.getWriter().write("用户不存在");
                    return false;
                }
                //验证 token
                JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
                try {
                    jwtVerifier.verify(token);
                } catch(JWTVerificationException e) {
                    httpServletResponse.getWriter().write("token校验失败");
                    return false;
                }
                return true;
            }

        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest,
                           HttpServletResponse httpServletResponse,
                           Object o, ModelAndView modelAndView) throws Exception {

    }
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest,
                                HttpServletResponse httpServletResponse,
                                Object o, Exception e) throws Exception {
    }
}

这里主要实现了 HandlerInterceptor 接口的 preHandle 方法。它是预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller,返回值为true表示继续流程(如调用下一个拦截器或处理器)或者接着执行 postHandle()afterCompletion();false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。

再配置一下拦截器。

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    // 添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor())
                .addPathPatterns("/**");
    }

    // 生成拦截器实例
    @Bean
    public AuthInterceptor authInterceptor() {
        return new AuthInterceptor();
    }
}

最好实现外部调用的接口。/login 是登陆接口,/getMessage使用来测试权限校验效果的接口。

@RestController
public class UserController {
    @Autowired
    UserService userService;
    @Autowired
    TokenService tokenService;

    @PostMapping("/login")
    public Object login(@RequestBody User user) {
        ObjectMapper objectMapper = new ObjectMapper();
        ObjectNode node = objectMapper.createObjectNode();
        User userForBase = userService.findByUserName(user);
        if(userForBase == null) {
            node.put("message", "登陆失败,用户不存在");
            return node;
        } else {
            if(!userForBase.getPassword().equals(user.getPassword())) {
                node.put("message", "登陆失败,密码错误");
                return node;
            } else {
                String token = tokenService.getToken(userForBase);
                node.put("token", token);
                node.put("username", userForBase.getUsername());
                node.put("id", userForBase.getId());
                return node;
            }
        }
    }

    @UserLoginToken
    @GetMapping("getMessage")
    public String getMessage() {
        return "通过验证!";
    }

}

测试

运行项目,在未登陆的情况下尝试访问 /getMessage ,结果如下。

jwt1.png

使用 /login 登录,获取 Token 。

jwt2.png

在请求报文头中添加 Token,再次访问/getMessage

jwt3.png

访问成功!

参考文章:

  • Introduction to JSON Web Tokens
  • 什么是 JWT -- JSON WEB TOKEN
  • SpringBoot集成JWT实现token验证
  • JWT详解

你可能感兴趣的:(JWT 概念原理及应用学习笔记)