SpringBoot 项目如何集成 JWT
JWT
JSON Web Token (JWT) 是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。
在 Oauth2 中,其实就是返回访问令牌 (access_token)代替用户名和密码来使用,访问令牌只需要满足唯一性,不可猜测性等条件即可,那么 JWT 结构化令牌也可以达到这样的效果,且内部可以包含一些更有效的信息
{
"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxIiwiZXhwIjoxNzMzNTYwMzY5fQ.AC3oOXbpz2cLYnCHTcYUCQxdTe_dqvwSSDpGi7W2IXg"
}
比如上面这个 token ,看起来只是一串由三个.
分割出的字符串罢了,但是如果你放到 https://jwt.io/ 上进行解析,就可以得到下面的结果:
JWT 可以分为以下三个部分:
- HEADER 表示装载令牌类型和算法等信息,是 JWT 的头部。其中,typ 表示第二部分 PAYLOAD 是 JWT 类型,alg 表示使用 HS256 对称签名的算法。
- PAYLOAD 表示是 JWT 的数据体,代表了一组数据。
- SIGNATURE 表示对 JWT 信息的签名,虽然有了 HEADER 和 PAYLOAD,信息已经可以进行传输了,但是我们为了安全,需要对其进行加密签名,SIGNATURE 就是对其进行加密签名的结果。
为什么需要加密签名呢,以登录流程为例,后端服务在用户登录成功后,返回了 jwt 并存放到了 cookie 里,前端每次发请求都携带这个 jwt,后端收到后,根据加密时候所用的加密串进行解密,如果解密成功,说明 jwt 没有被伪造,继续执行请求,否则认为这个 jwt 是伪造的,返回错误信息,需要前端重新登录,获取新 jwt。
SpringBoot 集成 JWT
- 登录时的返回信息中为用户新增token字段
package com.zwj.pojo;import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Date;@Data
@AllArgsConstructor
@NoArgsConstructor
// 这个类用于登录成功时返回,由前端存入 cookie 里,用于一些展示,比如 #{username},欢迎使用
public class UserCookie {private String username;private String email;private String token;
}
package com.zwj.utils;import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.zwj.pojo.Admin;
import com.zwj.service.AdminService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;@Component
@Slf4j
public class TokenUtil {// 生成 Token 的方法public static String genToken(String adminId, String sign) {return JWT.create().withAudience(adminId) // 将 adminId 保存到 token 里面,作为负载.withExpiresAt(DateUtil.offsetHour(new Date(), 2)) // 2小时后token过期.sign(Algorithm.HMAC256(sign)); // 以 password 作为 token 的签名密钥}
}
package com.zwj.controller;import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.MD5;
import com.zwj.controller.request.AdminPageRequest;
import com.zwj.controller.request.UserPageRequest;
import com.zwj.pojo.Admin;
import com.zwj.service.AdminService;
import com.zwj.utils.MD5Util;
import com.zwj.utils.Result;
import com.zwj.utils.TokenUtil;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;
import java.security.Security;
import java.util.Date;
import java.util.List;@RestController
@RequestMapping("/admin")
public class AdminController {@ResourceAdminServiceImpl adminService;@PostMapping("/login")public Result<?> login(@RequestBody Admin user){// 密码进行 MD5 加密user.setPassword(MD5Util.MD5Encode(user.getPassword(),"UTF-8"));// 找到登录的这个用户Admin admin = adminService.getAdminByUsernameAndPassword(user.getUsername(),user.getPassword());if (admin != null){if (admin.getStatus() == 1){UserCookie cookie = new UserCookie();// 为其生成 jwt,负载是用户ID,签名密钥是用户密码的MD5加密结果String token = TokenUtil.genToken(String.valueOf(admin.getId()), admin.getPassword());cookie.setToken(token);cookie.setUsername(admin.getUsername());// 返回这个用户信息,用于前端存入 cookiereturn Result.success(cookie);} else {return Result.error("您的权限已冻结!");}}return Result.error("用户名或密码错误!");}}
- 前端将信息存入 cookie 中
request.post("/admin/login", this.admin).then(res => {if (res.code === '200'){if (res.data != null){// 登录成功,将返回结果里的数据存入 cookie 中,key 是 'user'this.loginAdmin = res.data;Cookies.set('user', JSON.stringify(this.loginAdmin));}} else {this.$notify.error(res.msg);}
})
- 前端设置后续每次发送请求时,带上这个 token
request.interceptors.request.use(config => {config.headers['Content-Type'] = 'application/json;charset=utf-8';// 从 cookie 里取当前登录者信息let user = Cookies.get('user');if (user != null){// 如果能取到,放在 header 里config.headers['token'] = JSON.parse(user).token;}return config;
}, error => {return Promise.reject(error)
});
- 后端设置拦截器,拦截除登录、重置密码等不需要 token 外的所有请求
package com.zwj.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebConfig implements WebMvcConfigurer {@AutowiredJwtInterceptor jwtInterceptor;@Overridepublic void configurePathMatch(PathMatchConfigurer configurer) {// 指定controller统一的接口前缀configurer.addPathPrefix("/api", clazz -> clazz.isAnnotationPresent(RestController.class));}// 加自定义拦截器JwtInterceptor,设置拦截规则@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 这里的 jwtInterceptor 是我们自定义的,定义内容在下一个代码块registry.addInterceptor(jwtInterceptor).addPathPatterns("/api/**").excludePathPatterns("/api/admin/login","/api/files/**");}
}
package com.zwj.config;import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.zwj.pojo.Admin;
import com.zwj.excepion.ServiceException;
import com.zwj.service.AdminService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@Component
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {private static final String ERROR_CODE_401 = "401";@Resourceprivate AdminService adminService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {// 从 header 里取 tokenString token = request.getHeader("token");// 如果没有直接抛出异常if (StrUtil.isBlank(token)) {throw new ServiceException(ERROR_CODE_401, "无token,请重新登录");}// 获取 token 中的 adminIdString adminId;Admin admin;try {adminId = JWT.decode(token).getAudience().get(0);// 根据 token 中的 adminId 查询数据库admin = adminService.getAdminById(Integer.parseInt(adminId));} catch (Exception e) {String errMsg = "token验证失败,请重新登录";log.error(errMsg + ", token=" + token, e);throw new ServiceException(ERROR_CODE_401, errMsg);}if (admin == null) {throw new ServiceException(ERROR_CODE_401, "用户不存在,请重新登录");}try {// 用户密码加签验证 tokenJWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(admin.getPassword())).build();jwtVerifier.verify(token); // 验证token} catch (JWTVerificationException e) {throw new ServiceException(ERROR_CODE_401, "token验证失败,请重新登录");}return true;}
}
以上就是 SpringBoot 集成 JWT 的流程,其中的重点:
- 生成 token 时的内容和顺序
比如这里,设置了 audience 的值是 adminId,签名密钥是该用户的密码的 MD5 加密结果
- 验证的时候要按照相同的顺序取,使用相同的密钥验证
当然也可以不选择密码作为密钥验证,使用任意字符串也可以,但是使用密码做验证有这样的好处:如果用户此时修改了密码,那么这个 JWT 也就随之失效了,验证签名的时候会校验失败。但是如果使用其他常量作为密钥,就只能等待 JWT 主动到期,在修改密码到主动到期这段时间,JWT 依然可以被正常使用。