新版本的springsecurity与旧版本的使用方法有点不一样,最近探索了一下springsecurity与JWT的整合方案,进行一下总结。
文章目录
1、核心配置
首先来看一下springsecurity的核心配置文件
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
@Autowired
private CustomLogoutHandler logoutHandler;
@Autowired
private UnauthorizedEntryPoint authorizedEntryPoint;
@Autowired
private TokenAuthenticationFilter tokenAuthenticationFilter;
@Autowired
private AuthenticationConfiguration configuration;
@Autowired
private UserRoleMapper userRoleMapper;
@SneakyThrows
@Bean
public SecurityFilterChain filterChain(HttpSecurity http){
http.csrf().disable()
.authorizeRequests()
.antMatchers("/swagger-ui/**","/swagger-resources/**","/v3/api-docs").permitAll() //免登的访问路径
.antMatchers("/actuator/**").hasAuthority("actuator:view")
.anyRequest().authenticated()
.and()
.logout().logoutUrl("/logout") //退出的访问路径
.addLogoutHandler(logoutHandler) //退出的处理类对象
.and()
.exceptionHandling()
.authenticationEntryPoint(authorizedEntryPoint) //没有登录导致授权失败的处理类对象
.and()
.cors().configurationSource(corsConfigurationSource()) //设置跨越访问
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不创建session对象
.and()
.addFilterAt(new TokenLoginFilter(userRoleMapper,configuration), UsernamePasswordAuthenticationFilter.class) //自定义执行登录的过滤器
.addFilterAt(tokenAuthenticationFilter, BasicAuthenticationFilter.class); //自定义执行登录检查和授权的过滤器
return http.build();
}
private CorsConfigurationSource corsConfigurationSource(){
UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration=new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
source.registerCorsConfiguration("/**",corsConfiguration);
return source;
}
//配置密码加密的bean
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
这里面配置了执行登录的过滤器、执行登录检查和授权的过滤器、退出的访问路径、执行退出的处理类对象、未登录导致授权失败的处理类对象、支持跨越访问、禁止自动创建session。
下面会对这些类一个一个进行创建。
2、创建JWT的工具类
public class JWTUtil {
private static final String SECRET_KEY="jwtsecretkey256";
public static String getToken(SecurityUser securityUser, List<Integer> roleList){
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
String token = JWT.create()
.withClaim("userId",securityUser.getUserId())
.withClaim("username",securityUser.getUsername())
.withClaim("role", roleList)
.withClaim("createTime", LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8)))
//设置1天后过期
.withExpiresAt(Instant.now(Clock.offset(Clock.systemUTC(), Duration.of(8, ChronoUnit.HOURS))).plus(1, ChronoUnit.DAYS))
.sign(algorithm);
return token;
}
public static JWTResult verifyToken(String token){
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
try {
DecodedJWT decodedJWT = jwtVerifier.verify(token);
Integer userId = decodedJWT.getClaim("userId").asInt();
String username = decodedJWT.getClaim("username").asString();
List<String> roleIdList = decodedJWT.getClaim("role").asList(String.class);
Long createTime = decodedJWT.getClaim("createTime").asLong();
return new JWTResult(userId,username,roleIdList,createTime);
}catch (JWTVerificationException e){
throw new BadCredentialsException("token解析失败");
}
}
@Getter
@AllArgsConstructor
public static class JWTResult{
private Integer userId;
private String userName;
private List<String> roleIdList;
private Long createTime;
}
}
其中,getToken方法用于生成token,verifyToken方法用于做token校验,程序中将用户的角色id保存进了token里面,用于后面从缓存中获取权限,而不直接把权限保存进token里面,这样做可以防止因为权限数量太多导致token过长,过多占用网络带宽。
如果token解析失败,在过滤器中抛出BadCredentialsException异常,会自动调用AuthenticationEntryPoint中的commence方法,用于处理未登录导致授权失败的情况。
3、实现UserDetails接口
定义一个类,继承UserDetails,用于保存当前的用户信息
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SecurityUser implements UserDetails {
private Integer userId;
private String userName;
private String password;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return userName;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public SecurityUser(Integer userId, String userName) {
this.userId = userId;
this.userName = userName;
}
}
4、实现UserDetailsService接口
定义一个类,继承UserDetailsService,实现loadUserByUsername方法
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IUserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getUserByUserName(username);
if (user==null){
throw new UsernameNotFoundException("用户名不存在!");
}
SecurityUser securityUser=new SecurityUser(user.getUserId(),user.getUserName(),user.getPassword());
return securityUser;
}
}
5、创建登录的过滤器
定义一个类,继承AbstractAuthenticationProcessingFilter类,创建一个执行登录的过滤器
public class TokenLoginFilter extends AbstractAuthenticationProcessingFilter {
private UserRoleMapper userRoleMapper;
public TokenLoginFilter(UserRoleMapper userRoleMapper, AuthenticationConfiguration configuration) throws Exception {
super(new AntPathRequestMatcher("/login","POST"),configuration.getAuthenticationManager());
this.userRoleMapper=userRoleMapper;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username=request.getParameter("userName");
String password = request.getParameter("password");
return this.getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(username,password));
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SecurityUser securityUser = (SecurityUser) authResult.getPrincipal();
List<Integer> roleIdList=userRoleMapper.selectRoleIdByUserId(securityUser.getUserId());
String token = JWTUtil.getToken(securityUser,roleIdList);
ResponseUtil.out(response, Result.success(token));
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if (exception instanceof BadCredentialsException){
ResponseUtil.out(response, Result.fail("用户名或密码错误!"));
}else {
ResponseUtil.out(response,Result.error("系统内部异常"));
}
}
}
其中,attemptAuthentication方法用于执行登录,successfulAuthentication是登录成功后的处理方法,unsuccessfulAuthentication是登录失败后的处理方法。
这里用到了一个工具类ResponseUtil,用于发送响应数据
public class ResponseUtil {
@SneakyThrows
public static void out(HttpServletResponse response, Result result){
ObjectMapper mapper=new ObjectMapper();
response.setStatus(HttpStatus.OK.value());
response.setContentType("text/json;charset=UTF-8");
mapper.writeValue(response.getWriter(),result);
}
}
6、创建登录检查和授权的过滤器
定义一个类,继承OncePerRequestFilter,创建一个用于检查登录状态和授权的过滤器
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authentication");
if (token!=null){
if (redisTemplate.hasKey(RedisKey.USER_TOKEN+token)){
throw new BadCredentialsException("token已失效");
}
JWTUtil.JWTResult jwtResult = JWTUtil.verifyToken(token);
List<String> roleIdList = jwtResult.getRoleIdList();
HashOperations<String, String, List<String>> operations = redisTemplate.opsForHash();
List<List<String>> roleListList = operations.multiGet(RedisKey.ROLE_AUTHORITY_CODE, roleIdList);
List<SimpleGrantedAuthority> authorities = roleListList.stream()
.flatMap(Collection::stream)
.map(authority -> new SimpleGrantedAuthority(authority))
.collect(Collectors.toList());
SecurityUser securityUser=new SecurityUser(jwtResult.getUserId(), jwtResult.getUserName());
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(securityUser,token,authorities));
}
filterChain.doFilter(request,response);
}
}
基本逻辑是,首先从请求头获取token,检查缓存中是否有token的黑名单,如果有,就抛出BadCredentialsException异常,会自动调用AuthenticationEntryPoint接口的commence方法,处理授权失败的逻辑,如果没有,就解析token,获取用户的角色,通过角色id从缓存中获取用户权限码,再对用户进行授权。
如果token为空,就直接放行,进入下一个过滤器,由于没有调用setAuthentication方法,用户就没有被授权,后面的过滤器会自动调用AuthenticationEntryPoint接口的commence方法,作为未登录处理。
7、创建授权失败的处理类
定义一个类,继承AuthenticationEntryPoint接口,实现commence方法,用于创建未登录导致授权失败的处理类
@Component
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, new Result(ResultCode.UNAUTHORIZED));
}
}
8、创建执行退出的处理类
定义一个类,继承LogoutHandler接口,实现logout方法,用于创建退出的处理类
@Component
public class CustomLogoutHandler implements LogoutHandler {
@Autowired
private RedisTemplate<String,Integer> redisTemplate;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = request.getHeader("Authentication");
JWTUtil.JWTResult jwtResult = JWTUtil.verifyToken(token);
//计算token有效期的剩余时间
long tokenDuration=LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8))- jwtResult.getCreateTime();
long tokenRemian=60*60*24-tokenDuration;
redisTemplate.opsForValue().set(RedisKey.USER_TOKEN+token,jwtResult.getUserId(),tokenRemian, TimeUnit.SECONDS);
ResponseUtil.out(response, Result.success());
}
}
这里的基本逻辑是,从请求头获取token,对token进行解析,获取token的创建时间,然后计算token剩余的过期时间,然后将token加入缓存里面,作为黑名单,并设置过期时间。由于退出的用户只是占少数,因此把token加黑名单一般不会占用太多服务器内存。
9、加上权限注解
在接口上加上权限注解@PreAuthorize,进行权限访问控制
@PostMapping
@PreAuthorize("hasAuthority('user:save')")
public Result save(@RequestBody User user)