spring security filter+token权限校验

March 25, 2021 · 默认分类 · 169次阅读

Spring security一个重量级安全框架,可以用来做权限认证和访问控制。下面以一个实际项目来进行filter+token的验证。

功能介绍

  1. 实现一个登录带有验证码的校验并且在登录成功后返回给前端token,实现token认证。
  2. 对请求进行过滤验证,实现访问权限功能,访问控制力度到每一个操作上。
    登录.png

权限管理页面.png

步骤一 表设计

表设计很简单,用户,权限和角色三张表加联系三张表的两个中间表。
表结构.png

步骤二:工程结构

│ WebSecurityConfigurer.java

├─detail
│ │ CustomUserDetailsService.java
│ │
│ └─entity
│ CustomUserDetailsUser.java

├─handler
│ CustomAuthenticationFailHandler.java
│ CustomAuthenticationSuccessHandler.java
│ CustomLogoutSuccessHandler.java
│ TokenAuthenticationFailHandler.java

└─provider CustomDaoAuthenticationProvider.java

步骤三

  1. 首先必须有一个用来校验用户名密码成功后返回用户权限列表的类。这个类实现了SpringSecurity中的UserDetailService接口。其中getDetails是用户名密码从数据库比对成功后,去获取该用户的权限列表,(真正执行用户名密码校验的方法在DaoAuthenticationProvider中,可以继承这个类重写校验密码错误抛出的异常,也可以不重写springsecurity会自动执行)如下:
    @Component

public class CustomUserDetailsService implements UserDetailsService {

@Autowired
private SysUserService sysUserService;

@Autowired
private PermissionsService permissionsService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getUsername,username));
    if(ObjectUtil.isNull(sysUser)){
        throw new UsernameNotFoundException("用户不存在");
    }
    return getDetail(sysUser);
}

public UserDetails loadUserByUserId(Long userId) throws UsernameNotFoundException {
    SysUser sysUser = sysUserService.getById(userId);
    if(ObjectUtil.isNull(sysUser)){
        throw new UsernameNotFoundException("用户不存在");
    }
    return getDetail(sysUser);
}

private UserDetails getDetail(SysUser sysUser){
    Set<String> permissions =  permissionsService.getUserPermissions(sysUser.getUserId());
    String[] roles = new String[0];
    if(CollUtil.isNotEmpty(permissions)){
        roles = permissions.stream().map(role -> "ROLE_" + role).toArray(String[]::new);
    }
    Collection<? extends GrantedAuthority> authorities  = AuthorityUtils.createAuthorityList(roles);
    CustomUserDetailsUser customUserDetailsUser = new CustomUserDetailsUser(sysUser.getUserId(),sysUser.getUsername(),sysUser.getPassword(),authorities);
    return customUserDetailsUser;
}

}

2.创建一个springsecurity的配置类,其中有一个过滤链,过滤链中要配置登录的请求地址(并不是登录页面地址)

.loginProcessingUrl(Constant.TOKEN_ENTRY_POINT_URL) \\路径配置在常量中

配置了这个路径后,springsecurity会去自动调用实现了UserDetailService接口的类中的loadUserByUsername方法去校验用户名密码,成功后将在springsecurity上下文环境中存入权限列表。

public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private AuthIgnoreConfig authIgnoreConfig;


    //过滤链,接收一条请求后的操作
    @SneakyThrows
    @Override
    protected void configure(HttpSecurity http) {
        List<String> permitAll = authIgnoreConfig.getIgnoreUrls();
        permitAll.add("/actuator/**");
        permitAll.add("/error");
        permitAll.add("/v2/**");
        permitAll.add(Constant.TOKEN_ENTRY_POINT_URL);
        String[] urls = permitAll.stream().distinct().toArray(String[]::new);
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
        registry.antMatchers(urls).permitAll().anyRequest().authenticated().and().csrf().disable();
        http
            // 基于token,所以不需要session
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .headers().frameOptions().disable()
            .and()
            .formLogin()
            .loginProcessingUrl(Constant.TOKEN_ENTRY_POINT_URL)
            .successHandler(authenticationSuccessHandler())
            .failureHandler(authenticationFailureHandler())
            .and()
            .logout()
            .logoutUrl(Constant.TOKEN_LOGOUT_URL)
            .addLogoutHandler(logoutHandler())
            .logoutSuccessUrl("/sys/logout")
            .permitAll()
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(new TokenAuthenticationFailHandler())
            .and()
             // 如果不用验证码,注释这个过滤器即可
            .addFilterBefore(new ValidateCodeFilter(redisTemplate,authenticationFailureHandler()),UsernamePasswordAuthenticationFilter.class)
            .addFilterBefore(new AuthenticationTokenFilter(authenticationManagerBean(),redisTemplate,customUserDetailsService), UsernamePasswordAuthenticationFilter.class);
    }


    // handler中权限验证失败的情况
    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler(){
        return new CustomAuthenticationFailHandler();
    }

    @Bean
    public LogoutHandler logoutHandler(){
        return new CustomLogoutSuccessHandler();
    }

    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler(){
        return new CustomAuthenticationSuccessHandler();
    }

    @Bean
    @Override
    @SneakyThrows
    public AuthenticationManager authenticationManagerBean() {
        return super.authenticationManagerBean();
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/css/**");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  1. 之后执行过滤连中的successHandler方法,该方法中参数为定义的CustomAuthenticationSuccessHandler对象。该类实现了AuthenticationSuccessHandler 方法,即在登录成功后将token信息返回给前端。登录失败会进入CustomAuthenticationFailHandler()返回错误信息。
.successHandler(authenticationSuccessHandler())
@Slf4j
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private RedisTemplate redisTemplate;

    private ObjectMapper objectMapper = new ObjectMapper();

    @SneakyThrows
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
        String token;
        Long userId = 0l;
        if(authentication.getPrincipal() instanceof CustomUserDetailsUser){
            CustomUserDetailsUser userDetailsUser = (CustomUserDetailsUser) authentication.getPrincipal();
            token = SecureUtil.md5(userDetailsUser.getUsername() + System.currentTimeMillis());
            userId = userDetailsUser.getUserId();
        }else {
            token = SecureUtil.md5(String.valueOf(System.currentTimeMillis()));
        }
        redisTemplate.opsForValue().set(Constant.AUTHENTICATION_TOKEN + token,token,Constant.TOKEN_EXPIRE, TimeUnit.SECONDS);
        redisTemplate.opsForValue().set(token,userId,Constant.TOKEN_EXPIRE, TimeUnit.SECONDS);

        response.setCharacterEncoding(CharsetUtil.UTF_8);
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        PrintWriter printWriter = response.getWriter();
        printWriter.append(objectMapper.writeValueAsString(R.ok().put(Constant.TOKEN,token)));
    }
}
  1. 之后前端的每一次请求都会经过AuthenticationTokenFilter过滤器进行token验证。
public class AuthenticationTokenFilter extends BasicAuthenticationFilter {

    private RedisTemplate redisTemplate;
    private CustomUserDetailsService customUserDetailsService;
    private ObjectMapper objectMapper = new ObjectMapper();

    public AuthenticationTokenFilter(AuthenticationManager authenticationManager,RedisTemplate template,CustomUserDetailsService customUserDetailsService) {
        super(authenticationManager);
        this.redisTemplate = template;
        this.customUserDetailsService = customUserDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String token = request.getHeader(Constant.TOKEN);
        if(StrUtil.isBlank(token) || StrUtil.equals(token,"null")){
            token = request.getParameter(Constant.TOKEN);
        }

        if(StrUtil.isNotBlank(token) && !StrUtil.equals(token,"null")){
            Object userId = redisTemplate.opsForValue().get(token);
            if(ObjectUtil.isNull(userId)){
                writer(response,"无效token");
                return;
            }
            UserDetails userDetails = customUserDetailsService.loadUserByUserId((Long) userId);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }


    @SneakyThrows
    public void writer(HttpServletResponse response,String msg){
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().write(objectMapper.writeValueAsString(R.error(HttpServletResponse.SC_UNAUTHORIZED,msg)));
    }
}

标签:none

最后编辑于:2021/03/26 10:55

添加新评论

控制面板