SpringSecurity的UsernamePasswordAuthenticationFilter用于处理认证。要整合JWT,只需在认证成功后生成TOKEN并通过响应头写回客户端。在新增一个过滤器用于校验TOKEN。
新建SpringBoot项目,添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
我使用的JAVA版本是17,版本较高,需要添加jaxb-api依赖。
在application.properties配置
#60分钟
token.expire=3600000
token.key=123456HJKsdsf,';dfs
配置TOKEN的有效时间和秘钥。
增加TOKEN管理接口:
public interface TokenManager {
public String createToken(String username);
public String getUserFromToken(String token);
}
增加实现类:
@Service
@Slf4j
public class JwtTokenManager implements TokenManager {
@Value("${token.expire}")
private long tokenExpiration = 3600;
@Value("${token.key}")
private String tokenSignKey;
@Override
public String createToken(String username) {
String token = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
log.info("用户:{}生成token:{}", username, token);
return token;
}
@Override
public String getUserFromToken(String token) {
String user = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
log.info("从token:{}解析的用户名:{}", token, user);
return user;
}
}
增加常量:
public final class GlobalConstant {
private GlobalConstant() {
}
public final static String TOKEN_NAME = "MY_TOKEN";
}
保存TOKEN的名字。
配置SpringSecurity:
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private TokenManager tokenManager;
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("123")
.roles("USER")
.and()
.withUser("user")
.password("123")
.roles("TEMP");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.successHandler((request,response,authentication) -> {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = tokenManager.createToken(userDetails.getUsername());
response.setHeader(TOKEN_NAME, token);
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("登录成功");
out.flush();
out.close();
})
.failureHandler(((request, response, exception) -> {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(exception.getMessage());
out.flush();
out.close();
}))
.permitAll()
.and()
.logout()
.logoutSuccessHandler(((request, response, authentication) -> {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("注销成功");
out.flush();
out.close();
}))
.permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint(((request, response, authException) -> {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("尚未登录,请先登录");
out.flush();
out.close();
}))
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf()
.disable()
;
JwtAuthencationFilter jwtAuthencationFilter = new JwtAuthencationFilter(tokenManager, http.getSharedObject(UserDetailsService.class));
http.addFilterBefore(jwtAuthencationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
为了简单,将用户信息保存到内存中。successHandler
用于处理认证成功后的操作,这里生成TOKEN并写到客户端。failureHandler
用于处理认证失败的操作。logoutSuccessHandler
用于处理注销成功后的操作。authenticationEntryPoint
用于处理未登录的操作。JwtAuthencationFilter
用于校验TOKEN。JwtAuthencationFilter加到UsernamePasswordAuthenticationFilter的前面。还配置了session的创建策略为SessionCreationPolicy.STATELESS
,即不创建Session。
public class JwtAuthencationFilter extends OncePerRequestFilter {
private TokenManager tokenManager;
private UserDetailsService userDetailsService;
public JwtAuthencationFilter(TokenManager tokenManager, UserDetailsService userDetailsService) {
this.tokenManager = tokenManager;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader(TOKEN_NAME);
if (token != null) {
String username = tokenManager.getUserFromToken(token);
if (username != null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails == null) {
throw new RuntimeException("非法TOKEN");
}
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(username, token, new ArrayList<>()));
} else {
throw new RuntimeException("非法TOKEN");
}
}
filterChain.doFilter(request, response);
}
}
JwtAuthencationFilter从TOKEN中取出用户名,校验用户是否有效。这里还可以将用户保存到redis,避免每次都要查询数据库。还可以校验TOKEN的有效性,以及TOKEN的续期。如果访问不带TOKEN,则由配置的authenticationEntryPoint
处理未登录情形。