springboot2.7.6与springsecurity和JWT整合方案

新版本的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)
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值