springboot security 配置

146 阅读9分钟

Spring Security 提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。

中文文档

中文文档

过滤器类

  • DelegatingFilterProxy 是Spring 提供 Servlet Filter的实现,延迟地获取被注册为Spring Bean的 Filter,将工作委托给 Spring Bean
  • FilterChainProxy 是 Spring Security 提供的一个特殊的 Filter,允许通过 SecurityFilterChain 委托给许多 Filter 实例。由于 FilterChainProxy 是一个Bean,它通常被包裹在DelegatingFilterProxy官网作用图
  • SecurityFilterChain 是被 FilterChainProxy 用来确定当前请求应该调用哪些 Spring Security Filter 实例 官网作用图

主要认证类

  • SecurityContextHolder 是 Spring Security 存储认证用户
  • Authentication 可以是 AuthenticationManager 的输入,以提供用户提供的认证凭证或来自 SecurityContext 的当前用户
  • UsernamePasswordAuthenticationTokenAuthentication的实现 用于用户名/密码的认证
  • AuthenticationManager 认证管理器,定义 Spring Security 的 Filter 如何执行认证的API
  • ProviderManager 最常见的 AuthenticationManager 的实现
  • AuthenticationProviderProviderManager 用于执行特定类型的认证
  • DaoAuthenticationProviderAuthenticationProvider 的一个实现类,用的比较多,支持基于用户名/密码的认证
  • JwtAuthenticationProviderAuthenticationProvider 的一个实现类,支持认证JWT令牌
  • UserDetailsService 用于加载用户信息

主要授权类

  • AuthorizationManager 定义 Spring Security 如何执行授权API
  • PreAuthorizeAuthorizationManager AuthorizationManager 实现类,处理方法上@PreAuthorize注解最常用,是否符合EL表达式,
  • SecuredAuthorizationManager AuthorizationManager 实现类,处理方法上@Secured注解,判断是否有对应角色

常用请求权限控制

  • #(String... antPatterns) 方法,配置匹配的 URL 地址,基于 Ant 风格路径表达式 ,可传入多个。
  • 【常用】#permitAll() 方法,所有用户可访问。
  • 【常用】#denyAll() 方法,所有用户不可访问。
  • 【常用】#authenticated() 方法,登录用户可访问。
  • 【常用】#hasRole(String role) 方法, 拥有指定角色的用户可访问。
  • 【常用】#hasAnyRole(String... roles) 方法,拥有指定任一角色的用户可访问。
  • 【常用】#hasAuthority(String authority) 方法,拥有指定权限(authority)的用户可访问。
  • 【常用】#hasAuthority(String... authorities) 方法,拥有指定任一权限(authority)的用户可访问。
  • 【最牛】#access(String attribute) 方法,当 Spring EL 表达式的执行结果为 true 时,可以访问。#anonymous() 方法,无需登录,即匿名用户可访问。
  • 【不常用】#rememberMe() 方法,通过 remember me 登录的用户可访问。
  • 【不常用】#fullyAuthenticated() 方法,非 remember me 登录的用户可访问。
  • 【不常用】#hasIpAddress(String ipaddressExpression) 方法,来自指定 IP 表达式的用户可访问。

简单栗子

采用Springboot 集成 Spring Security ,通过两个内置用户登录测试请求权限校验,并且提供了新旧版两种安全配置

基于内存内置两个用户

  1. 管理用户 admin,admin角色,密码 123456 ,密码加密器采用 BCryptPasswordEncoder
  2. 普通用户 normal,normal角色,并且具有更新、新增用户权限,密码 123456,密码加密器采用 BCryptPasswordEncoder

加入依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>       
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

旧版本安全配置

通过继承WebSecurityConfigurerAdapter配置

/**
 * In Spring Security 5.7.0-M2 WebSecurityConfigurerAdapter 已被弃用
 * Spring 团队鼓励用户转向基于组件的安全配置
 * @author LGC
 */
@EnableWebSecurity // 其实加了下面方法级别校验注解可以不用添加
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfigurationOld extends WebSecurityConfigurerAdapter  {


    /**
     * 注册密码加密器 bean
     *
     * @return 密码加密器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    /**
     * 旧版本注册认证管理器 bean
     *
     * @return 认证管理器
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

 /**
     * 旧版本自定义用户数据源 自定义数据源储在内存中
     * 密码加密器与注入的一致,否则校验不通过
     * <p>
     * 1. 实现一个自定义用户类继承自UserDetails,下面直接使用了Security自带User构建者模式创建
     * 2. 实现一个类继承自UserDetailsSerive类,然后将这个类配置到认证管理器中
     * 下面直接使用了内置的InMemoryUserDetailsManager类,并注入成一个Bean
     *
     * @return 用户数据源
     */
    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        UserDetails admin = User.builder()
                .username("admin")
                .password("123456")
                .passwordEncoder(s -> passwordEncoder().encode(s))
                .roles("admin")
                .build();
        UserDetails normal = User.builder()
                .username("normal")
                .password("123456")
                .passwordEncoder(s -> passwordEncoder().encode(s))
                .roles("normal")// 角色
                .authorities("system:user:update", "system:user:add")// 权限
                .build();
        return new InMemoryUserDetailsManager(admin, normal);
    }


    /**
     * 旧版
     * 1.将用户数据源配置到认证管理器中
     * 2.将密码加密器配置到认证管理器中
     *
     * @param auth the {@link AuthenticationManagerBuilder} to use
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }


    /**
     * 旧版本配置过滤拦截链
     *
     * @param http the {@link HttpSecurity} to modify
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors()// 开启跨域
                .and()
                .csrf().disable()// CSRF禁用
                // 自定义的 Spring Security 处理器
                .exceptionHandling()
                // 配置自定义未认证(未登录)处理器,一般都会配置上并由前端控制跳转到登录页,未配置则自动跳转到默认登陆页
//                .authenticationEntryPoint(authenticationEntryPoint())
                // 配置无权限访问自定义处理器,未配置则自动跳转到自己创建403页面,如未创建自己403页面则直接报错
//                .accessDeniedHandler(accessDeniedHandler())
                .and()


                // 设置 Form 表单登录
                .formLogin()
//                .loginPage("/login") // 自定义登录页面,不配置为默认登录页面
                .successForwardUrl("/api/user/index") // 登陆成功后请求url,不配置重定向之前范围地址比如请求/api/user/admin
                .permitAll()// 所有用户可访问
                .and()


                // 配置退出登录
                .logout()
//                .logoutUrl("/logout") // 自定义退出页面,不配置为默认退出页面
                .permitAll() // 所有用户可访问
                .and()


                // 配置请求地址的权限
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()// 静态资源,可匿名访问
                .antMatchers("/api/user/admin").hasAnyRole("admin")// 需要 admin 角色
                .antMatchers("/api/user/update").hasAuthority("system:user:update") // 需要权限key system:user:update
                .antMatchers("/api/user/add").access("hasAuthority('system:user:add')")// 需要权限key system:user:add
                .antMatchers("/api/version").permitAll()
                .anyRequest().authenticated()// 任何请求,访问的用户都需要经过认证才能访问
                .and()
                //.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)// 自定义过滤器
        ;

    }

}

新版本安全配置

Spring团队鼓励用户转向基于组件的安全配置,可以查看源码WebSecurityConfigurerAdapter 类上面的注释信息 ,上面有相关伪代码配置

/**
 * 新版本安全配置类
 * 基于组件的安全配置
 * @author LGC
 */
//@EnableWebSecurity // 其实加了下面方法级别校验注解可以不用添加
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfigurationNew {

    /**
     * 注册密码加密器 bean
     * <p>
     * 新版不用配置手动配置到认证管理器中,注入为bean后自动注入到认证管理器中
     * 旧版需要手动配置
     *
     * @return 密码加密器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 新版注入认证管理器 bean
     *
     * @param authenticationConfiguration
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 新版本自定义用户数据源 自定义用户数据源储在内存中
     * 密码加密器与注入的一致,否则校验不通过
     * 新版不用配置手动配置到认证管理器中,注入为bean后自动注入到认证管理器中
     * 旧版需要手动配置
     *
     * @return 用户数据源
     */
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails admin = User.builder()
                .username("admin")
                .password("123456")
                .passwordEncoder(s -> passwordEncoder().encode(s))
                .roles("admin")
                .build();
        UserDetails normal = User.builder()
                .username("normal")
                .password("123456")
                .passwordEncoder(s -> passwordEncoder().encode(s))
                .roles("normal")// 角色
                .authorities("system:user:update", "system:user:add")// 权限
                .build();
        return new InMemoryUserDetailsManager(admin, normal);
    }


    /**
     * 新版配置过滤拦截链
     *
     * @param httpSecurity
     * @return
     * @throws Exception
     */
    @Bean
    protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .cors()// 开启跨域
                .and()
                .csrf().disable()// CSRF禁用
                // 自定义的 Spring Security 处理器
                .exceptionHandling()
                // 配置自定义未认证(未登录)处理器,一般都会配置上并由前端控制跳转到登录页,未配置则自动跳转到默认登陆页
//                .authenticationEntryPoint(authenticationEntryPoint())
                // 配置无权限访问自定义处理器,未配置则自动跳转到自己创建403页面,如未创建自己403页面则直接报错
//                .accessDeniedHandler(accessDeniedHandler())
                .and()


                // 设置 Form 表单登录
                .formLogin()
//                .loginPage("/login") // 自定义登录页面,不配置为默认登录页面
                .successForwardUrl("/api/user/index") // 登陆成功后请求url,不配置重定向之前范围地址比如请求/api/user/admin
                .permitAll()// 所有用户可访问
                .and()


                // 配置退出登录
                .logout()
//                .logoutUrl("/logout") // 自定义退出页面,不配置为默认退出页面
                .permitAll() // 所有用户可访问
                .and()


                // 配置请求地址的权限
                .authorizeRequests()
                .antMatchers("/api/user/admin").hasAnyRole("admin")// 需要 admin 角色
                .antMatchers("/api/user/update").hasAuthority("system:user:update") // 需要权限key system:user:update
                .antMatchers("/api/user/add").access("hasAuthority('system:user:add')")// 需要权限key system:user:add
                .antMatchers("/api/version").permitAll()
                .anyRequest().authenticated()// 任何请求,访问的用户都需要经过认证才能访问
                .and()
                //.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)// 自定义过滤器

        ;
        return httpSecurity.build();
    }


   /**
     * 新版忽略安全校验
     * 可以将忽略的路径配置在这里,一般都是配置所有模块共同忽略安全校验 比如:静态资源等
     * 一般都会这么实现:
     
     * 模块独有的安全校验配置在自己模块中,然后再本配置类SecurityFilterChain过滤链中加载所有模块自定义的安全配置
     * 这样就实现了模块的独立开发,互不影响
     *
     * @return
     */
    @Bean
    public WebSecurityCustomizer authorizeRequestsCustomizer() {
        return new WebSecurityCustomizer() {
            @Override
            public void customize(WebSecurity web) {
                web.ignoring()
                        .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js")
                        .antMatchers("/doc.html")
                        .antMatchers("/v2/api-docs/**")
                        .antMatchers("/swagger-ui.html")
                        .antMatchers("/swagger-ui/**")
                        .antMatchers("/swagger-resources/**")
                        .antMatchers("/webjars/**");
            }
        };
    }

}

权限测试类

/**
 * @author LGC
 */
@RestController
public class UserController {

    @RequestMapping("/api/user/index")
    public String index() {
        return "登陆成功首页index";
    }

    @PreAuthorize("hasRole('admin')")
    @GetMapping("/api/user/admin")
    public String admin() {
        return "具有 admin角色 访问成功";
    }

    @PreAuthorize("hasAuthority('system:user:update')")
    @GetMapping("/api/user/update")
    public String update() {
        return "具有权限key system:user:update 访问成功";
    }

    @PreAuthorize("hasAuthority('system:user:add')")
    @GetMapping("/api/user/add")
    public String add() {
        return "具有权限key system:user:add 访问成功";
    }

    @GetMapping("/api/version")
    public String version() {
        return "lgc version 1.0";
    }

}

编程方式授权方法

灵活定义授予验证逻辑,在项目中比较常用,下面是简单配置使用:

  1. 自定义授权Bean 命名为as,里面逻辑自己实现, 比较简单,也就是从SecurityContextHolder.getContext()中获取认证对象然后进行权限校验

    /**
     * 自定义授权校验Bean 命名为as
     *
     * @author LGC
     */
    @Component(value = "as")
    public class AuthorizationService {
    
        public boolean hasPermission(String permission) {
            return hasAnyPermissions(permission);
        }
    
        public boolean hasAnyPermissions(String... permissions) {
            return false;
        }
    
        public boolean hasRole(String role) {
            return hasAnyRoles(role);
        }
    
        public boolean hasAnyRoles(String... roles) {
            return false;
        }
    
        public boolean hasScope(String scope) {
            return hasAnyScopes(scope);
        }
    
        public boolean hasAnyScopes(String... scope) {
            return false;
        }
    }
    
  2. 配置使用

        @PreAuthorize("@as.hasPermission('system:user:test')")
        @GetMapping("/api/test")
        public String test() {
            return "编程方式授权方法 当请求/api/test 路径时会进入自定义授权Bean对应方法逻辑,进行自定义权限校验";
        }
    

配置忽略校验

在yaml配置文件中配置忽略校验的白名单,然后再安全配置类中加载配置如下:

# 安全配置配置类
security:
  permitAllUrls:
    - /api/version
    - /api/user/login
    - /api/user/register
/**
 * 安全配置配置类
 *
 * @author LGC
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
    private List<String> permitAllUrls = Collections.emptyList();
}

配置请求地址的权限

@Resource
private  SecurityProperties securityProperties;
    
// 配置请求地址的权限
httpSecurity
.authorizeRequests()
.antMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll()//所有用户可访问
.anyRequest().authenticated()// 任何请求,访问的用户都需要经过认证才能访问