Jwt鉴权

1. JWT概念

JWT是JSON Web Token的缩写,它是一种开源标准(RFC 7519),是一种用来定义通信双方如何安全地交换信息的格式。

重点

  • JWT之所以叫JSON Web Token,是因为其头部和载荷在编码之前都是JSON格式的数据;

  • JWT是一种标准,它有很多的实现方案,我们的java代码中使用的就是java-jwt;(也就是我们引入的jjwt依赖包)

  • JWT规定以JSON的格式传递信息,载荷payload的数据格式是JSON的,通常使用base64编码;

  • JWT是自包含的,Token本身携带了验证信息,不需要借助其他工具就可以知道一个Token是否有效,以及载荷信息;

JWT常见误区:

  • JWT是不安全的,因为使用base64编码。这种理解是错误的,头部和载荷确实使用了base64编码,它的作用是编码而非加密,便于传输和前端解码获取信息。所以头部和载荷不要存放保密信息,因为头部和载荷相当于是未加密的,任何人获取以后都可以通过解码,得到编码前的内容。而签名的作用只是验证头部和载荷的信息是真实的,未被篡改的。
  • JWT是自包含,不需要借助数据库和缓存。这种理解是错误的,当需要高级功能,比如token刷新、黑名单、多人共享账号等,还是需要借助缓存和数据库。
  • 获取头部和载荷信息之后可以修改或者伪造token。这是不可能的,即使头部和载荷的信息完全一样,但是加密的密钥不对,签名也是不对的,后端验证也没法通过。

1.1.什么是JWT

json web token,通过数字签名的方式,以json为载体,在不同的服务之间安全的传输信息的一种技术

1.2.JWT有什么用

一般使用在授权认证的过程中,一旦用户登录,后端返回一个token给前端,相当于后端给了前端返回了一个授权码,之后前端向后端发送的每一个请求都需要包含这个token,后端在执行方法前会校验这个token(安全校验),校验通过才执行具体的业务逻辑。

1.3.JWT的组成结构

jwt由Header(头信息),PayLoad (载荷),signature(签名)三个部分组成

头部

Header头信息主要声明加密算法:(具体算法对称不对称加密不作为研究内容)

 通常直接使用 HMAC HS256这样的算法

{
  "typ":"jwt"
  "alg":"HS256" //加密算法
}

然后将头部进行base64编码,构成了第一部分。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

载荷

PayLoad(载荷)

{
  "username":"zhangsan",
  "name":"张三",
  "exp":1686117973
}

对其进行base64编码,得到Jwt的第二部分。

eyJ1c2VybmFtZSI6InpoYW5nc2FuIiwibmFtZSI6IuW8oOS4iSIsImV4cCI6MTY4NjExNzk3M30=

签名

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

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InpoYW5nc2FuIiwibmFtZSI6IuW8oOS4iSIsImV4cCI6MTY4NjExNzk3M30=.5tmHCpcsS_VuZ2_z5Rydf2OpsviBGwB-fJE5aS7gKqE

将以上的三个部分,通过 “.” 进行连接后,就得到了我们的jwt令牌

2. 开发使用JWT

2.1. 使用说明


为了使用jwt,我们需要以下准备工作

  1. 引入jjwt依赖
  2. 编写TokenUtils工具类,用来生成和验证token
  3. 在配置文件中写入密钥和token存活时间
  4. 编写JwtConfig文件,从配置文件中获取密钥和token存活时间等信息

在具体使用的时候,我们一般按以下步骤

  1. 在登录controller层引入JwtConfig对象
  2. 登录业务成功后,调用TokenUtils,获取token并返回给前端
  3. 新建登录拦截器,在请求头中获取token信息并验证,决定是否放行
  4. 在SpringMVCConfig.java文件中,配置登录拦截器

2.2. 配置信息

引入依赖

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

配置文件

jwt:
  # 签名密钥(按自己需求填写任意值)
  secureKey: 48587471A6610367927BE4168C892294
  # token存活时间(毫秒)
  ttlMills: 3600000

2.3. 其他文件

JwtUtils.java

jwt工具类,用来生成和校验token

/**
 * token工具类
 * @Author Mosfield
 */
public class JwtUtils {

    /**
     * 生成token
     * @param params    需要加入的参数
     * @param secureKey 密钥
     * @param ttlMills  token存活时间
     * @return
     */
    public static String createToken(Map<String, Object> params, String secureKey, long ttlMills) {
        // 1.声明加密的算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //2.声明过期时间
        long expMillis = System.currentTimeMillis() + ttlMills;
        Date exp = new Date(expMillis);
        //3.配置jwt中要存入的信息

        //4.创建密钥
        String secretKey = secureKey;

        //5.生成jwt
        String token = Jwts.builder()
                //配置jwt中需要保存的数据
                .setClaims(params)
                //配置加密算法&加密的key
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                //配置过期时间
                .setExpiration(exp)
                //根据配置信息生成jwt
                .compact();
        return token;
    }

    /**
     * 验证jwt token
     * @param token 被验证的token
     * @param secureKey 密钥
     * @return token中的载荷
     */
    public static Claims parseToken(String token,String secureKey){
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secureKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的TOKEN
                .parseClaimsJws(token)
                .getBody();
        return claims;

    }
}

【注意】

解析token的方法叫做 parseClaimsJws ,不是 parseClaimsJwt 。千万不要写错!

parseClaimsJws方法,解析的是带有jws签名的token,而parseClaimsJwt只能解析没有jws签名的token

JwtConfig.java

jwt相关配置信息,主要用来获取配置文件中的信息

/**
 * jwt相关配置信息
 * @Author Mosfield
 */
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtConfig {

    private String secureKey;

    private long ttlMills;
}

2.4. 实际使用案例

登录token验证。本例中使用对称加密。所有失败的处理使用全局异常处理解决。

EmpController.java

/**
 * 员工管理Controller
 */
@RestController
@Slf4j
@CrossOrigin
@Api(tags = "员工相关接口")
public class EmpController {

    @Autowired
    EmpService empService;

    @Autowired
    JwtConfig jwtConfig;

    /**
     * 登录校验,登录成功后发放token
     *
     * @param userDTO 用户登录DTO
     * @return
     */
    @ApiOperation(value = "登录接口")
    @PostMapping("/login")
    public Result login(@RequestBody UserDTO userDTO) {
        // 登录校验
        Emp emp = empService.login(userDTO);

        // 设置需要放入token负载中的数据
        HashMap<String, Object> params = new HashMap<>();
        params.put("uid", emp.getId());
        // 设置负载,密钥,过期时间,获取该用户对应的token
        String token = JwtUtils.createToken(params, jwtConfig.getSecureKey(), jwtConfig.getTtlMills());

        // 登录成功,返回token信息
        return Result.success(token);
    }
}

LoginInterceptor.java

登录拦截器,用于校验用户是否已登录

/**
 * 登录验证拦截器
 * @Author Mosfield
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    JwtConfig jwtConfig;

    /**
     * 检查用户是否登录
     * return true => 放行逻辑 => 可以继续走三层
     * return false => 拦截 =>到此为止了
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 从请求头里获取token
        String token = request.getHeader("token");
        try {
            // ======================== 具体的验证的逻辑(jwt) ========================
            Claims claims = JwtUtils.parseToken(token, jwtConfig.getSecureKey());
            // 登录成功
            return true;
        } catch (Exception e) {
            //被拦截了,我们需要给客户端提示(如:请登陆后继续操作)
            ObjectMapper objectMapper = new ObjectMapper();
            String jsonStr = objectMapper.writeValueAsString(Result.error("请登陆后继续操作"));
            //把json格式的数据写给前端
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(jsonStr);
            return false;
        }
    }
}

WebMVCConfig.java

在SpringMVC配置类中注册登录拦截器

/**
 * 对springMVC框架进行控制
 * @Author Mosfield
 */
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {

    @Autowired
    LoginInterceptor loginInterceptor;

    /**
     * 对拦截器进行配置
     * 拦截器对象在容器中
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 需要放行
        ArrayList<String> acl = new ArrayList();
        // 放行下列swagger相关路径,即那些页面不需要登录就能访问。本例子中放行登录和注册功能。
        acl.add("/login");
        acl.add("/register");

        // 配置LoginInterceptor登录拦截器
        registry.addInterceptor(loginInterceptor)
                // 拦截所有页面
                .addPathPatterns("/**")
                // 放行acl中的页面
                .excludePathPatterns(acl);
    }
}
如人饮水,冷暖自知。
最后更新于 2023-08-05