jwt

发布时间 2023-07-31 09:23:03作者: YxinHaaa

JWT基础知识

前后端分离的解决方案:

什么是JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

发生在用户和服务器端的一个加密的json格式的字符串。

JWT的构成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。

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

在头部指明了签名算法是HS256算法。我们进行BASE64编码https://base64.us/,编码后的字符串如下:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码

载荷(playload)

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

(1)标准中注册的声明(建议但不强制使用)

iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
{"iss":"张三","sub":"everyone","iat":"2022-9-19 15:30:00","exp":"2022-9-20 15:30:00"}

(2)公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密. 这个部分叫公共声明

(3)私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。用户名 地址 支付宝账号等等信息

这个指的就是自定义的claim。比如下面面结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。用户名 密码等信息存放在此处

定义一个payload:

{"sub":"1234567890","name":"John Doe","admin":true,"address":"山西省太原市晋源区"}

然后将其进行base64加密,得到Jwt的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

签名(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

头部 (base64后的) eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

payload (base64后的) eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

密钥 xiaohua

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret,那就意味着客户端是可以自我签发jwt了。

JJWT的介绍和使用

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

官方文档:

https://github.com/jwtk/jjwt

创建TOKEN

(1)依赖引入

在项目中的pom.xml中添加依赖:

<!--鉴权-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

(2)创建测试

test/java下创建测试类,并设置测试方法

public class JwtTest {

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

*/
    @Test
    public void testCreateJwt(){
       	JwtBuilder builder= Jwts.builder()
      	.setId("888")//设置唯一编号
      	.setSubject("小白")//设置主题可以是JSON数据
      	.setIssuedAt(new Date())//设置签发日期
      	.signWith(SignatureAlgorithm.HS256,"mmkj");//设置签名使用HS256算法,并设置SecretKey(字符串)
      	//构建并返回一个字符串
       	System.out.println( builder.compact());
    }
}

运行打印结果:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE2MTI3ODk0MjN9.adpNtewLbNZ-r6WUr8fyRLxFj1KF6PPP2UQq_VOxICo

再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。

TOKEN解析

我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。

/***
*解析Jwt令牌数据
*/
@Test
public void testParseJwt(){
 		String 			compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4";
 		Claims claims = Jwts.parser().
 		setSigningKey("mmkj").
 		parseClaimsJws(compactJwt).
 		getBody();
 		System.out.println(claims);
}

运行打印效果:

{jti=888, sub=小白, iat=1562062287}

试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token.

设置过期时间

有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。

image-20230728104313883

解释:

.setExpiration(date)//用于设置过期时间,参数为Date类型数据

运行,打印效果如下:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjI5MjUsImV4cCI6MTU2MjA2MjkyNX0._vs4METaPkCza52LuN0-2NGGWIIO7v51xt40DHY1U1Q

解析TOKEN

/***
*解析Jwt令牌数据
*/
@Test
public void testParseJwt(){
     String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjI5MjUsImV4cCI6MTU2MjA2MjkyNX0._vs4METaPkCza52LuN0-2NGGWIIO7v51xt40DHY1U1Q";
     Claims claims = Jwts.parser().
     setSigningKey("mmkj").
     parseClaimsJws(compactJwt).
     getBody();
     System.out.println(claims);
}

打印效果:

image-20230728104333898

当前时间超过过期时间,则会报错。

自定义claims

我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。

创建测试类,并设置测试方法:

创建token:

 @Test
    void contextLoads1() {
        JwtBuilder builder = Jwts.builder();
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.HOUR,1);
        builder.setId("idxxx");
        builder.setIssuer("张三");
        builder.setExpiration(calendar.getTime());
        builder.setIssuedAt(new Date());
        builder.signWith(SignatureAlgorithm.HS256,"yxh111");
        HashMap<String, Object> stringObjectHashMap = new HashMap<>();
        stringObjectHashMap.put("username","admin");
        stringObjectHashMap.put("password","123456");
        builder.addClaims(stringObjectHashMap);
        System.out.println(builder.compact());
    }

运行打印效果:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE2MTI3OTAyOTAsImV4cCI6MTYxMjc5MDI5MCwiYWRkcmVzcyI6IuWMl-S6rOW4guacnemYs-WMuuS6lOaWueahpeWfuuWcsCIsIm5hbWUiOiLkuK3lhazkvJjlsLHkuJoifQ.VwVrb-wuxb_BCLlSn2GY_m8z2pWpdU-KdtkeAY8gp5k

解析TOKEN:

/***
*解析Jwt令牌数据
*/
 @Test
    void contextLoads2() {
        String str = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJpZHh4eCIsImlzcyI6IuW8oOS4iSIsImV4cCI6MTY5MDUxOTY3NywiaWF0IjoxNjkwNTE2MDc3LCJwYXNzd29yZCI6IjEyMzQ1NiIsInVzZXJuYW1lIjoiYWRtaW4ifQ.krOL6LzcjmL6a0hiHDcMwAlrDbCQ7WV7o9vX-B7eu88";
        Claims body = Jwts.parser().setSigningKey("xiaob").parseClaimsJws(str).getBody();
        System.out.println(body);
    }

JWT的好处:

1.我们可以在JWT中存储一些用户的基本信息

2.jwt可以自己去验证是否正确

总结

jwt是发生在客户端和服务器之间的一个加密的json格式的字符串

他包括三部分 头部 载荷 签名

采用的是base64的加密算法进行的加密

jwt的签发和验证都是由服务器端来完成的

使用场合

前后端分离的项目,前端在异步请求后端项目时,没有在请求头中带上cookie 就没有携带sessionid

上述问题 只要在前端请求头中手动加入cookie就可以解决

为什么还是不建议把验证成功的数据放入session中?

1.当会话变多,对应的session就会很多,session占用的是服务器的内存,这种方式也不是很好

spring security和 jwt的结合

我们需要在登录成功后将jwt下发给客户端其实就是

前后端分离的项目 我们表单是异步提交,而且提交路径不一定是/login 用户名 密码也可以是自定义的

登录成功后 我们不需要跳转页面 可以返回登录成功的信息(jwt)

这个需要配置一下

  1. 我们自定义一个登录的controller方法(第一次访问登录时使用) 前后端分离 表单访问的数据
@RequestMapping("/login")
    public String login(@RequestBody TUser user) {
        String token = null;
        try {
            //通过用户名到数据库中查找用户信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
            //对密码进行编码及比对,目前使用的是BCryptPasswordEncoder类实现PasswordEncoder接口
            if (!passwordEncoder.matches(user.getPassword(), userDetails.getPassword())) {
                throw new BadCredentialsException("密码不正确");
            }
            /**
             * UsernamePasswordAuthenticationToken继承AbstractAuthenticationToken实现Authentication
             * 所以当在页面中输入用户名和密码之后首先会进入到UsernamePasswordAuthenticationToken验证(Authentication),
             * 然后生成的Authentication会被交由AuthenticationManager来进行管理
             * 而AuthenticationManager管理一系列的AuthenticationProvider,
             * 而每一个Provider都会通UserDetailsService和UserDetail来返回一个
             * 以UsernamePasswordAuthenticationToken实现的带用户名和密码以及权限的Authentication
             */
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
            token = jwtTokenUtil.generateToken(userDetails);
        } catch (AuthenticationException e) {
            log.warn("登录异常:{}", e.getMessage());
        }
        return token;
    }
  1. 编写一个jwt的工具类(用于验证)

    package com.mmkj.springsecurity.util;
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author yangfan
     * @description
     *
     * Jwt生成的工具类
     * JWT的格式:header.payload.signature
     * header的格式(算法、token的类型):
     * {"alg": "HS512","typ": "JWT"}
     * payload的格式(用户名、创建时间、生成时间):
     * {"sub":"wang","created":1489079981393,"exp":1489684781}
     * signature的生成算法:
     * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
     *
     * @modified By
     */
    
    @Component
    @Slf4j
    public class JwtTokenUtil {
        //用户名
        private static final String CLAIM_KEY_USERNAME = "sub";
        //生成时间
        private static final String CLAIM_KEY_CREATED = "created";
        //jwt密钥
        @Value("${jwt.secret}")
        private String secret;
        //jwt过期时间
        @Value("${jwt.expiration}")
        private Long expiration;
    
        /**
         * 根据负责生成JWT的token
         *  subject    主体,即用户信息的JSON
         *  issuer     签发人
         *  claims     自定义参数
         */
        private String generateToken(Map<String, Object> claims) {
            return Jwts.builder()
                    // 自定义属性
                    .setClaims(claims)
                    // 过期时间
                    .setExpiration(generateExpirationDate())
                    // 签名算法以及密匙
                    .signWith(SignatureAlgorithm.HS512, secret)
                    // 主题:代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                    //.setSubject(subject)
                    // 签发人
                    //.setIssuer(Optional.ofNullable(issuer).orElse(ISS))
                    .compact();
        }
    
        /**
         * 从token中获取JWT中的负载
         */
        private Claims getClaimsFromToken(String token) {
            Claims claims = null;
            try {
                claims = Jwts.parser()
                        .setSigningKey(secret)
                        .parseClaimsJws(token)
                        .getBody();
            } catch (Exception e) {
                log.info("JWT格式验证失败:{}",token);
            }
            return claims;
        }
    
        /**
         * 生成token的过期时间
         */
        private Date generateExpirationDate() {
            return new Date(System.currentTimeMillis() + expiration * 1000);
        }
    
        /**
         * 从token中获取登录用户名
         */
        public String getUserNameFromToken(String token) {
            String username;
            try {
                Claims claims = getClaimsFromToken(token);
                username =  claims.getSubject();
            } catch (Exception e) {
                username = null;
            }
            return username;
        }
    
        /**
         * 验证token是否还有效
         *
         * @param token       客户端传入的token
         * @param userDetails 从数据库中查询出来的用户信息
         */
        public boolean validateToken(String token, UserDetails userDetails) {
            String username = getUserNameFromToken(token);
            return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
        }
    
        /**
         * 判断token是否已经失效
         */
        private boolean isTokenExpired(String token) {
            Date expiredDate = getExpiredDateFromToken(token);
            return expiredDate.before(new Date());
        }
    
        /**
         * 从token中获取过期时间
         */
        private Date getExpiredDateFromToken(String token) {
            Claims claims = getClaimsFromToken(token);
            return claims.getExpiration();
        }
    
        /**
         * 根据用户信息生成token
         * @param userDetails  自定义参数
         *
         */
        public String generateToken(UserDetails userDetails) {
            Map<String, Object> claims = new HashMap<>();
            claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
            claims.put(CLAIM_KEY_CREATED, new Date());
            return generateToken(claims);
        }
    
        /**
         * 判断token是否可以被刷新
         */
        public boolean canRefresh(String token) {
            return !isTokenExpired(token);
        }
    
        /**
         * 刷新token
         */
        public String refreshToken(String token) {
            Claims claims = getClaimsFromToken(token);
            claims.put(CLAIM_KEY_CREATED, new Date());
            return generateToken(claims);
        }
    }
    
    1. 以后每次登录都需要走这个方法

      OncePerRequestFilter是Spring Boot里面的一个过滤器抽象类,其同样在Spring Security里面被广泛用到,这个过滤器抽象类通常被用于继承实现并在每次请求时只执行一次过滤。

      package com.mmkj.springsecurity.config;
      
      import com.mmkj.springsecurity.util.JwtTokenUtil;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
      import org.springframework.security.core.context.SecurityContextHolder;
      import org.springframework.security.core.userdetails.UserDetails;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
      import org.springframework.stereotype.Component;
      import org.springframework.web.filter.OncePerRequestFilter;
       
      import javax.servlet.FilterChain;
      import javax.servlet.ServletException;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
       
      /**
       * @author yangfan
       * @description JWT登录授权过滤器
       * @modified By
       */
      @Component
      @Slf4j
      public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
          @Autowired
          private UserDetailsService userDetailsService;
          @Autowired
          private JwtTokenUtil jwtTokenUtil;
          @Value("${jwt.tokenHeader}")
          private String tokenHeader;
          @Value("${jwt.tokenHead}")
          private String tokenHead;
       
          @Override
          protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
              String authHeader = request.getHeader(this.tokenHeader);
              if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
                  String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
                  String username = jwtTokenUtil.getUserNameFromToken(authToken);// 从jwt中获取到用户名
                  log.info("checking username:{}", username);
                  //SecurityContextHolder保留系统当前的安全上下文细节,其中就包括当前使用系统的用户的信息
                  if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                      //在SpringSecurityConfig类中,自己实现UserDetailsService接口的loadUserByUsername方法,
                      //此方法获取用户信息,并保存在AdminUserDetails对象中
                      //而AdminUserDetails这个类实现了UserDetails接口,所以可使用UserDetails接口接收
                      UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                      //验证前端传来的token是否过期、是否被篡改
                      if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                          UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
       
                          /**
                           *  springsecurity默认只验证用户的username和password信息,
                           *  所以我们如果想实现验证码登录,需要重写WebAuthenticationDetails类,
                           *  使其能通过HttpServletRequest获取到用户输入的验证码的信息
                           *  我们这里登陆暂时还没做验证码的功能
                           */
                          authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                          log.info("authenticated user:{}", username);
                          SecurityContextHolder.getContext().setAuthentication(authentication);
                      }
                  }
              }
              filterChain.doFilter(request, response);
          }
      }
      
  2. 我们需要在spring security中进行配置

      @Override
        protected void configure(HttpSecurity http) throws Exception {
            //http.addFilterBefore(new JwtAuthencationFilter(authenticationManager, UsernamePasswordAuthenticationFilter.class))
            //关闭csrf防护 跨站请求防护
            http
                    //认证配置
                    .authorizeRequests()
                    .antMatchers("/user/login").permitAll()   // 设置/user/login不需要权限
                    //任何请求
                    .anyRequest()
                    //都需要身份验证
                    .authenticated();
    
             // 设置这个过滤器在验证用户名和密码过滤器之前
            http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);
            // 设置已经登录过 但是没有权限访问要走的对象
            http.exceptionHandling().accessDeniedHandler(deniedHandler);
            // 如果没有登录 走这个方法
            http.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint);
    
            // 设置登录失败的对象
    
            //配置退出
            http.logout()
                    //退出路径
                    .logoutUrl("/logout")
                    ;
        }
    

6.未登录成功 配置

package com.mmkj.springsecurity.config;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 当未登录或者token失效访问接口时,自定义的返回结果
 *
 * @author Pymjl
 * @version 1.0
 * @date 2022/8/20 16:42
 **/
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println("未登录,没有权限访问");
        response.getWriter().flush();
    }
}