什么是JWT
JWS 是 JSON Web Token
的缩写,用JSON作为对象在系统之间安全地传输信息。关于JWT更多信息,请参考阮一峰的 JSON Web Token 入门教程
注意:本章是基于第三章 Spring Security基于数据库登录实现的
Maven依赖
<!-- 新增redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 新增jwt依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
</dependency>
新增JWT工具类
public class JwtTokenUtils {
public static final String JWT_SECRET_KEY = "C*F-JaNdRgUkXn2r5u8x/A?D(G+KbPeShVmYq3s6v9y$B&E)H@McQfTjWnZr4u7w";
private static final byte[] API_KEY_SECRET_BYTES = DatatypeConverter.parseBase64Binary(JWT_SECRET_KEY);
// 秘钥
private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(API_KEY_SECRET_BYTES);
private static final long EXPIRATION = 20 * 1000;
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String TOKEN_TYPE = "JWT";
public static final String ROLE_CLAIMS = "rol";
public static String createToken(String username, Integer id, List<String> roles) {
Date now = new Date();
Date expirationDate = new Date(now.getTime() + EXPIRATION);
String tokenPrefix = Jwts.builder()
.setHeaderParam("type", TOKEN_TYPE)
.signWith(SECRET_KEY, SignatureAlgorithm.HS256)
.claim(ROLE_CLAIMS, String.join(",", roles))
.setId(id.toString())
.setIssuer("butterflyzh")
.setIssuedAt(now)
.setSubject(username)
.setExpiration(expirationDate)
.compact();
return TOKEN_PREFIX + tokenPrefix;
}
public static UsernamePasswordAuthenticationToken getAuthentication(String token) {
Claims claims = getClaims(token);
List<SimpleGrantedAuthority> authorities = getAuthorities(claims);
String username = claims.getSubject();
return new UsernamePasswordAuthenticationToken(username, token, authorities);
}
private static List<SimpleGrantedAuthority> getAuthorities(Claims claims) {
String roles = (String) claims.get(ROLE_CLAIMS);
return Arrays.stream(roles.split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
public static String getId(String token) {
Claims claims = getClaims(token);
return claims.getSubject() + ":" + claims.getId();
}
private static Claims getClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
}
RedisConfig配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
}
新增Jwt过滤器链
这里我们需要在 UsernamePasswordAuthenticationFilter
过滤器之前新增 JwtAuthenticationFilter
过滤器链。该过滤器主要工作:解析请求头中 Authorization
字段,然后从Redis中取出服务器存储token,最后比对请求携带的token和服务器存储的token是否一样。
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final StringRedisTemplate redisTemplate;
private final RequestMatcher requestMatcher = new AntPathRequestMatcher("/login");
public JwtAuthenticationFilter(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// /login登录地址不应该拦截
if (requestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
String token = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
if (token == null || !token.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
SecurityContextHolder.clearContext();
filterChain.doFilter(request, response);
return;
}
String tokenValue = token.replace(JwtTokenUtils.TOKEN_PREFIX, "");
UsernamePasswordAuthenticationToken authentication = null;
try {
String oldToken = redisTemplate.opsForValue().get(JwtTokenUtils.getId(tokenValue));
if (!token.equals(oldToken)) {
SecurityContextHolder.clearContext();
filterChain.doFilter(request, response);
return;
}
authentication = JwtTokenUtils.getAuthentication(tokenValue);
} catch (JwtException e) {
logger.error("Invalid jwt : " + e);
}
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
自定义AuthenticationSuccessHandler
分析一下,我们需要在哪里地方去生成 token
呢?我觉得在登录之后生成比较好。之前分析登录流程时,登录成功时,过滤器 AbstractAuthenticationProcessingFilter
会调用 AuthenticationSuccessHandler
处理器进行重定向。那现在我们可以自定义处理器,在完成登录时,生成 token
并存储到redis中。
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private StringRedisTemplate redisTemplate;
public MyAuthenticationSuccessHandler(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
User user = (User) authentication.getPrincipal();
String username = user.getUsername();
List<String> authorities = user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
String token = JwtTokenUtils.createToken(username, user.getId(), authorities);
redisTemplate.opsForValue().set(user.getId().toString(), token);
httpServletResponse.setHeader(JwtTokenUtils.TOKEN_HEADER, token);
}
}
WebSecurityConfig配置
这里我修改 configure(HttpSecurity http)
方法。这里我们需要配置 sessionCreationPolicy(SessionCreationPolicy.STATELESS)
创建策略,因为使用jwt作为token,就不需要session保持状态。
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(new MyAuthenticationSuccessHandler(redisTemplate))
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable();
http.addFilterBefore(new JwtAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class);
}