JWT笔记
JWT笔记
一、简单介绍
JWT由三部分组成:
header,payload,Signature(头部,载荷,签证)
二、工具类
工具类是一个封装类,将一些对Jwt相关的操作进行整理,以便调用。
1. JwtToken创建
(不同版本,方法或有变动)调用构造方法:Jwts.builder();
该方法是链式调用,依次对jwt的三个组成部分进行配置。
- Header:
头部主要声明类型和使用的加密算法(加密算法现在后自动添加,也可以主动声明) - Payload:
载荷由claims传入,.claims()方法接收Map类型,里面存入数据可自行定义(通常会存入用户名,id这些安全性要求不高的数据) - Signature:
签证由signWith()进行,参数为密钥和签名方法,该版本的密钥有所要求,需要符合一定要求(有些版本要求会低一些) - 构建
最后由.compact()进行构建,返回的token由根据 . 分隔的三部分组成。
public String createToken(Map<String, Object> claims) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + expire * 1000L);
try {
return Jwts.builder()
.header() // 设置头部
.add("typ", "JWT") // 设置头部类型
.and()
.claims(claims) // 自定义负载
.issuedAt(nowDate)
.expiration(expireDate)
.signWith(getSigningKey(), Jwts.SIG.HS256)// 改为更安全的密钥处理方式SecretKey
.compact();
} catch (IllegalStateException e) {
LOGGER.error("创建JWT失败: 签名密钥不可用", e);
throw e;
} catch (Exception e){
LOGGER.error("创建JWT失败", e);
throw new RuntimeException("创建 JWT 失败", e);
}
}
2. 密钥构建
Keys.hmacShaKeyFor()将字节数组包装成SecretKey,即被用于签名的密钥;(推荐使用至少 32 字节,不符合安全检查会抛出异常)
所以我们的私钥如果不是字节数组,要主动将其转换(调用Decoders.BASE64.decode())
private SecretKey getSigningKey(){
if(secret == null || secret.trim().isEmpty()){
LOGGER.error("JWT密钥未配置,请在配置文件中设置jwt.secret");
throw new IllegalStateException("JWT密钥未配置,请在配置文件中设置jwt.secret");
}
try{
byte[] keyBytes = Decoders.BASE64.decode(secret);//解码为字节数组
return Keys.hmacShaKeyFor(keyBytes);//生成密钥
} catch(IllegalArgumentException e){
try{
LOGGER.info("BASE64解码失败,尝试使用原始字符串作为密钥");
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));//备用方案,直接使用字符串的字节数组(不推荐)
}catch (Exception ex){
LOGGER.error("JWT密钥无效,请检查jwt.secret的配置", ex);
throw new IllegalStateException("JWT密钥无效,请检查jwt.secret的配置", ex);
}
}
}
3. token解析
固定的链式调用:
- 调用Jwts.parser()开启配置解析器,
- 调用.verifyWith()配置签名认证使用的密钥
- .build()返回已配置的 JwtParser 实例
- .parseSignedClaims()根据解析器执行解析(返回Jws
用于后续操作) - .getPayload()获取负载
// 解析 JWT token,获取负载payload
public Claims getTokenClaim(String token) {
try {
LOGGER.info("开始解析JWT:{}",token);
return Jwts
.parser()
.verifyWith(getSigningKey()) // 使用 verifyWith(SecretKey),替代已弃用的 setSigningKey
.build()
.parseSignedClaims(token) // 使用 parseSignedClaims 方法解析签名的 JWT
.getPayload(); // 获取负载部分
} catch (IllegalStateException e) {
LOGGER.error("解析JWT失败: 签名密钥不可用", e);
throw e;
} catch (Exception e) {
LOGGER.info("JWT格式验证失败:{}",token);
return null;
}
}
4. 读取负载信息
以过期时间为例,从解析到的负载中调用相关方法即可(claims.getExpiration())
public Date getExpirationDateFromToken(String token) {
try {
Claims claims = getTokenClaim(token);
return claims != null ? claims.getExpiration() : null;
} catch (Exception e) {
LOGGER.info("获取Token过期时间失败:{}", token);
return null;
}
}
三、于SpringSecurity集成
1. token过滤器
在SpringSecurity的验证过滤器前新增过滤器用于token验证(原本我认为是要重写验证,网上方案却多是新增过滤器)
@Component
public class JwtValidationFilter extends OncePerRequestFilter{
private static final Logger LOGGER = LoggerFactory.getLogger(JwtValidationFilter.class);
// 注入UserDetailsService,SecretKey,JwtUtil
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
@Value("${jwt.secret}")
private String secretKey;
@Autowired
public JwtValidationFilter(UserDetailsService userDetailsService, JwtUtil jwtUtil) {
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 0.获取请求token
String token = request.getHeader("Authorization");
// 1.检查请求头内Authorization信息
try{
// 1.1 如果没有,放行
// 1.2 如果有,且不是以Bearer开头,放行
if(StringTools.isBlank(token) || !token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)){
filterChain.doFilter(request, response);
return;
}
// 2.去除Bearer前缀
token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
// 3.解析 Token
boolean isValidDate = jwtUtil.isTokenExpired(token);
// 4.检查 Token有效,无效捕获并则抛出异常
if(!isValidDate){
LOGGER.error("JwtValidationFilter error: token is invalid");
throw new RuntimeException(ResultEnum.UNAUTHORIZED.getMessage());
}
// 4.1.有效,进行验证
Claims payloads = jwtUtil.getTokenClaim(token);//获取负载
String username = payloads.getSubject();//都读取负载上用户名
UserDetailsImpl userDetails = (UserDetailsImpl) userDetailsService.loadUserByUsername(username);//根据用户名加载用户信息
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());//创建认证令牌
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));//设置请求详情
SecurityContextHolder.getContext().setAuthentication(authenticationToken);//将认证令牌存入安全上下文
} catch (Exception e) {
// 4.2.无效,清除上下文并抛出异常
LOGGER.error("JwtValidationFilter error: {}", e.getMessage());
SecurityContextHolder.clearContext();
}
// 5.继续执行过滤链
filterChain.doFilter(request,response);
}
}
2. 配置SecurityConfig
在配置文件中设置token的过滤器
@Configuration
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
@Autowired
public SecurityConfig(UserDetailsService userDetailsService, JwtUtil jwtUtil) {
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
}
@Bean
public PasswordEncoder passwordEncoder() {
// 也可用有参构造,取值范围是 4 到 31,默认值为 10。数值越大,加密计算越复杂
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
//过滤链配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests(authorize -> authorize //开启授权
.requestMatchers("/login","/register","/api/register", "/register.html", "/api/login") //放行请求
.permitAll() //允许所有人访问
.anyRequest() //允许所有请求
.authenticated() //认证后访问自动授权
)
.formLogin(Customizer.withDefaults()) //使用默认的登陆登出页面进行授权登陆
.rememberMe(Customizer.withDefaults()); // 启用“记住我”功能
http.csrf(csrf -> csrf.disable()); //关闭csrf
http.addFilterBefore(new JwtValidationFilter(userDetailsService, jwtUtil), UsernamePasswordAuthenticationFilter.class); //添加JWT过滤器
return http.build();
}
}
3. 登录生成token
现在有了验证流程,生成token的流程还没有加入。
- 在登录模块调用token的生成方法
- 将生成的token封装进dto中(封装的数据结构)
@Override
public LoginDto login(String username, String password) {
//TODO 0.准备工作,注入AuthenticationManager、SecurityProperties
LoginDto loginDto = new LoginDto();
String token;
Long expiration;
try{
//TODO 1.调用认证方法
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
SecurityContextHolder.getContext().setAuthentication(authentication);
//TODO 2.生成JWT
token = jwtUtil.createToken(authentication);
loginDto.setToken(token);
}catch (Exception e){
LOGGER.error("登录异常:{}", e.getMessage());
throw new ApiException(e.getMessage());
}
//TODO 3.返回封装信息
return loginDto;
}

浙公网安备 33010602011771号