Spring Security的基本使用及注意事项(一)


文章目录

  • Spring Security的基本使用及注意事项(一)
  • Spring Security简介
  • 认证与授权
  • RBAC模型
  • Spring Security使用步骤
  • Manven坐标引入(使用SpringBoot进行整合)
  • 认证
  • 实现UserDetailsService接口
  • 详细步骤
  • 授权
  • 实现流程
  • Spring Seurtity配置类
  • 捕获filter中的异常
  • 实现跨域
  • 配置密码解析器
  • 插入数据时注意,需要将密码的格式设置为BCryptPasswordEncoder采取的格式



2022-08-05 16:19:47 星期五

Spring Security简介

  • Spring Security是Spring家族中的权限验证框架,主要可用于后台系统的认证与授权

认证与授权

  • 认证,即告知系统当前登录对象,并将用户信息保存;
  • 授权,即系统服务当前登录对象可使用哪些功能的过程,系统会在认证结束之后获取到当前用户具有的权限,例如,使用Add(添加信息)功能时,需要拥有管理员权限,则会判断当前用户是否具有管理员权限,如果具有则可以使用添加信息的功能,如果没有,则会返回403。

RBAC模型

  • 基于资源或角色的数据权限模型
  • 涉及到的数据关系模型:
  • 角色、权限、用户、(菜单),其中权限关系是中心
  • 角色——权限:多对多关系,用户——权限:多对多关系,菜单——权限:多对多关系
  • 共计四个关系,三个联系,数据库中设计为7个表结构
  • 常用的是以权限资源为中心进行验证,可扩展性高,健壮性较强。

Spring Security使用步骤

Spring Security本质上是一条拦截器链,通过各种各样的拦截器进行验证,最终实现完整的权限验证过程。

springsecurity 白名单不走自定义的拦截器 spring security 白名单规则_spring

Manven坐标引入(使用SpringBoot进行整合)

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
     </dependency>
  • UsernamePasswordAuthenticationFilter:负责校验用户名与密码的拦截器
  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException(授权时发生的异常)和AuthenticationException(认证时发生的异常)。
  • **FilterSecurityInterceptor:**负责权限校验的过滤器。

认证

springsecurity 白名单不走自定义的拦截器 spring security 白名单规则_ci_02

  • 用户名与密码传递到UsernamePasswordAuthenticationFilter拦截器中
  • 拦截器进行对象的封装usernamePasswordAuthenticationToken,通过AuthenticationManager类中的authenticate方法进行验证,验证通过封装成Authentication对象,保存用户的信息,包括但不限于用户名,密码,权限等。
  • 其中authenticate方法在认证时需要使用到UserDetailService接口,我们需要实现该接口
  • 判断Authentication对象是否为空,如果是null,则代表认证未通过
  • 将用户id生成Token令牌返回给用户,将Authentication对象存入缓存中

springsecurity 白名单不走自定义的拦截器 spring security 白名单规则_spring boot_03

实现UserDetailsService接口

@Override
	  public UserDetails loadUserByUsername(String username) throws 	UsernameNotFoundException {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getTel, username);
        User user = mapperUser.selectOne(queryWrapper);
        if (Objects.isNull(user)) {
            return null;
        }

        //        获取用户权限的操作
        List<String> strings = mapperPermission.SelectPermissionId(user.getTel());
        LambdaQueryWrapper<Permission> queryWrapper1 = new LambdaQueryWrapper<>();
        queryWrapper1.select(Permission::getKeyword).in(Permission::getId, strings);
        List<Permission> permissions = mapperPermission.selectList(queryWrapper1);
        List<String> permissionlist =       permissions.stream().map(Permission::getKeyword).collect(Collectors.toList());

        System.out.println("权限集合是:" + permissionlist.get(0));

        Login login = new Login();
        login.setUser(user);
        login.setPermissions(permissionlist);

        return login;
    }
详细步骤
  • UserDetilsService接口是Spring Security框架实现用户名密码验证的服务层对象,通过
  • 重写UserDetilsService接口loadUserByUsername方法
  • loadUserByUsername方法实现自定义的用户名密码验证过程;
  • loadUserByUsername方法内部主要实现的任务如下:
  • 根据用户名数据库中查找该用户信息,如果没有,则返回空,SPring Security会**抛出异常和AuthenticationException **;
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
      queryWrapper.eq(User::getTel, username);
      User user = mapperUser.selectOne(queryWrapper);
      if (Objects.isNull(user)) {
          return null;
      }

注意的是:在实际开发过程中我们最好将异常信息进行统一的管理,交由Cotroller层统一返回给用户,所以我们返回结果为null,使Spring Security抛出异常

  • 查找用户所具有的权限信息
List<String> strings = mapperPermission.SelectPermissionId(user.getTel());
     LambdaQueryWrapper<Permission> queryWrapper1 = new LambdaQueryWrapper<>();
     queryWrapper1.select(Permission::getKeyword).in(Permission::getId, strings);
     List<Permission> permissions = mapperPermission.selectList(queryWrapper1);
     List<String> permissionlist = permissions.stream().map(Permission::getKeyword).collect(Collectors.toList());
  • 将用户信息与权限信息封装成UserDetil对象,在本案例中,Login是实现自UserDetil接口,最后返回Login对象
Login login = new Login();
     login.setUser(user);
     login.setPermissions(permissionlist);

授权

  • 在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
  • 所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
  • 然后设置我们的资源所需要的权限即可。
实现流程
  • 获取前端传递的TOken值,取出缓存中存取的UserDetil对象信息,实现过程就是通过新建过滤器,在过滤器中完成解析
http.addFilterBefore(sercityFilter,  UsernamePasswordAuthenticationFilter.class);

过滤器类

@Component
public class SercityFilter extends OncePerRequestFilter {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {


        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            ArrayList<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_niming"));
            UsernamePasswordAuthenticationToken authenticationToken =

                    new UsernamePasswordAuthenticationToken(null, null, grantedAuthorities);
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);


            //放行
            filterChain.doFilter(request, response);
            return;
        }
//        解析token
        Claims claims = null;
        try {
            claims = JWTUntil.parseJWT(token);
        } catch (Exception e) {
            e.printStackTrace();
        }
        String tel = claims.getSubject();
        System.out.println("电话是" + tel);
//        获取缓存中的内容
        String login = (String) redisTemplate.opsForHash().get("login", tel);
        Login login1 = JSON.parseObject(login, Login.class);



        login1.getAuthorities().stream().forEach(new Stream.Builder<GrantedAuthority>() {
            @Override
            public void accept(GrantedAuthority grantedAuthority) {
                System.out.println("第一种" + grantedAuthority);
            }

            @Override
            public Stream<GrantedAuthority> build() {
                return null;
            }
        });


        if (Objects.isNull(login)) {

            throw new RuntimeException("用户未登录");
        }

  //        TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(login1.getUsername(),   login1.getPassword(), login1.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
        filterChain.doFilter(request, response);
    }
	
}
  • 添加Authentication到Sercurity全局对象中,授权过程结束
UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(login1.getUsername(), login1.getPassword(), login1.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
        filterChain.doFilter(request, response);

认证与授权的基本过程为上述内容

Spring Seurtity配置类

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class SecrityConfigure  extends WebSecurityConfigurerAdapter {

//    @Autowired
//    private CrosFileter crosFileter;
    @Autowired
    private WebApplicationContext applicationContext;
    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;
    @Autowired
    private SercityFilter sercityFilter;

     @Bean
     public PasswordEncoder passwordEncoder(){
         return NoOpPasswordEncoder.getInstance();
     }

    /**
     * 获取标有注解 AnonymousAccess 的访问路径
     */
    private String[] getAnonymousUrls() {

        RequestMappingHandlerMapping bean = SpringUtils.getBean(RequestMappingHandlerMapping.class);

//        new Webappl
        // 获取所有的 RequestMapping
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = bean.getHandlerMethods();
        Set<String> allAnonymousAccess = new HashSet<>();
        // 循环 RequestMapping
        for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethods.entrySet()) {
            HandlerMethod value = infoEntry.getValue();
            // 获取方法上 AnonymousAccess 类型的注解
            AnonymousAccess methodAnnotation = value.getMethodAnnotation(AnonymousAccess.class);
            // 如果方法上标注了 AnonymousAccess 注解,就获取该方法的访问全路径
            if (methodAnnotation != null) {
                if (infoEntry.getKey().getPatternsCondition()!=null){
                    allAnonymousAccess.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                }
            }
        }
        allAnonymousAccess.stream().forEach(System.out::println);
        return allAnonymousAccess.toArray(new String[0]);



    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http.cors();
        http.csrf().disable()
                //跨域请求会先进行一次options请求

                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
//                .antMatchers(HttpMethod.GET,"/video/").anonymous()
//解决跨域问题
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
//
//                .antMatchers("/video/user/**").hasAuthority("userpersission")
                .antMatchers("/user/login/**").permitAll()
                .antMatchers(HttpMethod.POST,"/user").permitAll()
                .antMatchers(getAnonymousUrls()).anonymous()
                .anyRequest().authenticated()
                .and()

                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and();
        http.addFilterBefore(sercityFilter,  UsernamePasswordAuthenticationFilter.class);
        //允许跨域
//        http.addFilterBefore(crosFileter,sercityFilter.getClass());
     }

//    @Bean
//    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
关闭csrf
//               return http.csrf().disable()
//                       .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//                       .and()
//                       .authorizeRequests()
//                       .antMatchers("/user/login").anonymous()
//                       .anyRequest().authenticated()
//                       .and()
                       .addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class)
//                       .build();
//
//
//
//    }

    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
        return authenticationManager;
    }

}
  • 首先需要继承WebSecurityConfigurerAdapter
  • 重写configure方法
@Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http.cors();
        http.csrf().disable()
                //跨域请求会先进行一次options请求

                .authorizeRequests()//开启请求验证
                .antMatchers(HttpMethod.OPTIONS).permitAll()
//                .antMatchers(HttpMethod.GET,"/video/").anonymous()
//解决跨域问题
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
//
//                .antMatchers("/video/user/**").hasAuthority("userpersission")
                .antMatchers("/user/login/**").permitAll()
                .antMatchers(HttpMethod.POST,"/user").permitAll()
                .antMatchers(getAnonymousUrls()).anonymous()
                .anyRequest().authenticated()
                .and()

                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and();
        http.addFilterBefore(sercityFilter,  UsernamePasswordAuthenticationFilter.class);
        //允许跨域
//        http.addFilterBefore(crosFileter,sercityFilter.getClass());
     }

注意事项 permitAll()是允许所有角色访问,anonymous()是允许匿名访问,当发送到后端的请求中带有token时anonymous()将不起作用
角色必须带有ROLE_前缀

  • 注入AuthenticationManager类,用于创建Authentication对象
@Bean
    public AuthenticationManager authenticationManager() throws Exception {
        AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
        return authenticationManager;
    }
  • 开启注解,可以在方法上指定权限类型
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
  • prePostEnabled较为常用的注解,使用该注解指定角色时,不需要添加ROLE_前缀
捕获filter中的异常
  • 进行请求转发到controller层
httpServletRequest.setAttribute("Loginecption", new RuntimeException("用户未登录"));
            httpServletRequest
                    .getRequestDispatcher("/user/error")
                    .forward(httpServletRequest, httpServletResponse);
@GetMapping("error")
    public String error(HttpServletRequest request, HttpServletResponse response) throws Exception {
        Exception ecption =(Exception) request.getAttribute("Loginecption");
       throw  new LoginEcption(ecption.getMessage());

实现跨域

public class SpringmvcConfigure implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
//        WebMvcConfigurer.super.addCorsMappings(registry);
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);

    }
}

在security中也需要设置跨域

//    允许跨域
        http.cors()
@Configuration
public class SecurotyConfig extends WebSecurityConfigurerAdapter {
//    @Resource
//    private AuthFailCompent authFailCompent;
//    采用Security提供的密码加密器BCryptPasswordEncoder
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Resource
    public JwtAuthticateTokenFilter jwtAuthticateTokenFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        http//关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                .antMatchers("/test/**").anonymous()
//               除了上面的登录接口,其他都需要认证身份
                .anyRequest().authenticated().and();
//        .and().formLogin().failureHandler(authFailCompent);
//        将token解析的过滤器,添加到用户名密码校验的过滤器前
        http.addFilterBefore(jwtAuthticateTokenFilter, UsernamePasswordAuthenticationFilter.class);
//    允许跨域
        http.cors();
    }

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
       return super.authenticationManagerBean();
    }
}
配置密码解析器
@Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

插入数据时注意,需要将密码的格式设置为BCryptPasswordEncoder采取的格式

@Resource
    private PasswordEncoder passwordEncoder;
@Test
    void contextLoads() {
        String encode = passwordEncoder.encode("123456");
        User build = User.builder().userName("lmx").password(encode).nickName("飘零书剑").userType("1").build();
       userMapper.insert(build);	
    }