avatar

目录
Spring学习 依赖注入

Spring 学习 保护Web应用

学习内容:

  • Spring Security介绍
  • 使用Servlet 规范中的Filter保护 Web应用
  • 基于数据库和LDAP进行认证

Spring Security简介

Spring Security是为基于Spring的应用程序提供声明式安全保护的安全性框架。 Spring Security 提供了完整的安全性解决方案,它能够在Web请求级别和方法调用级别身份认证和授权。

因为基于Spring框架,所以Spring Security 充分利用了 依赖注入面向切面 的技术。

在Spring Security 3.2版本中,从两个角度来解决安全性问题

  • 使用 Servlet 规范中的Filter保护Web请求并限制URL级别的访问
  • 使用 Spring AOP 保护方法调用 ———借助于对象代理和使用通知,能够确保只有具备适当权限的用户才能访问安全保护的方法

Spring Security 的模块

不管你想使用Spring Security保护哪种类型的应用程序(这里只讲保护Web应用程序),第一件需要做的就是将Spring Security模块添加到应用程序的类路径下。
它一共有11个模块,如下所示
QQ截图20191004003057.png
其中箭头标注的,无论哪种应用都是必须的,Web应用程序还需要添加 Web 模块

过滤Web请求

借助于Spring的小技巧,我们只需配置一共Filter就可以提供各种安全性功能了。
DelegatingFilterProxy 是一个特殊的 Servlet Filter,它本身所做的工作并不多。知识将一个工作委托给一共 javax.servlet.Filter 实现类。这个实现类作为一个\注册在Spring应用的上下文中,如图所示
QQ截图20191004003626.png

DelegatingFilterProxy 的配置方法如下

  • 在传统的 web.xml 中配置

    QQ截图20191004003641.png

    注意这里将\设置成了 springSecurityFilterChain
    当我们配置 Spring Security 到 Web 安全性中, 这里会有一个名为 springSecurityFilterChain 的 Filter bean ,DelegatingFilterProxy 会将过滤逻辑委托给它

  • 以Java的方式来配置

    package spitter.config;
    import org,springframework.security.web.context.AbstractSecurityWebApplicationInitializer;  
    
    public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {}  
    

    AbstractSecurityWebApplicationInitializer 实现了 WebApplicationInitializer,因此Spring会发现他,并用它在Web容器中注册DelegatingFilterProxy 。

不管是用哪种方式来配置 DelegatingFilterProxy ,它都会拦截发送应用中的请求,并将请求委托给ID 为 springSecurityFilterChain bean .

springSecurityFilterChain 本身是另一个特殊的 Filter,它也被成为 FilterChainProxy. 它可以链接任意一个或多个其他的 Filter. Spring Security 依赖一系列 Servlet Filter 来提供不同的安全特性。
你不需要显示声明 springSecurityFilterChain 以及它所链接在一起的其他Filter。当我们启用 Web 安全性的时候,会自动创建这些Filter。

编写简单的安全性配置

在Spring 3.2之前 安全性配置是很麻烦的
但在 Spring 3.2 之后,引入了新的Java 配置方案,完全不在需要通过XML来配置安全性功能了。
以下是最简单的安全性配置

package spitter.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigureAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
}

@EnableWebSecurity 注解将会启用 Web 安全功能。但他本身并没有什么用处, Spring Security 必须配置在一个实现了 WebSecurityConfigurer 的 bean中,或者拓展了WebSecurityConfigurerAdapter。 在Spring 应用上下文中,任何实现了 WebSecurityConfigurer 的bean都可以用来配置 Spring Security ,但是最简单的方式还是拓展 WebSecurityConfigurerAdapter 类

@EnableWebSecurity 可以启用任意Web应用的安全性功能,不过,如果你的应用时Spring MVC 那应该考虑使用 @EnableWebMvcSecurity 替代它。 该注解还配置了一个 Spring MVC 参数解析器,这样的话处理器方法就能狗通过带有 @AuthenticationPrincipal 注解的参数获得认证用户的 principal。它同时还配置了一个 bean ,在使用 Spring 表单绑定标签库来定义表单时,这个 bean 会自动添加一个 CSRF token 输入域。

package spitter.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebMvcSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigureAdapter;
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
}

除此之外,我们还可以通过重载 WebSecurityConfigureAdapter 的 configure() 方法来配置 Web 安全性。
QQ截图20191004144512.png

当我们没有重写上述三个方法的任何一个时,默认的Filter 即 configure(HttpSecurity) 实际上等同于

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
        .and()
        .formLogin().and()
        .httpBasic()
}

这个默认配置中 authorizeRequests() 和 anyRequest().authenticated() 会要求所有进入应用的HTTP请求都要进行认证。formLogin()使Spring Security支持基于表单的登录 httpBasic() 使应用支持 HTTP Basic方式的认证。

同时,因为我们没有重载 configure(AuthenticationManagerBuilder)方法,所以没有用户存储支撑认证功能。

所以为了让 Spring Security 满足我们应用的需求,还需要添加一点其他配置,具体来讲:

  • 配置用户存储 (重载configure(AuthenticationManagerBuilder)方法)
  • 指定哪些请求需要认证,哪些请求不需要认证,以及所需要的权限(重载configure(HttpSecurity)方法)
  • 提供一个自定义的登录页面,替代原来简单的默认登录页。

选择查询用户详细信息的服务(重载configure(AuthenticationManagerBuilder)方法)

Spring Security 非常灵活,能够基于各种数据存储来认证用户

  • 内存
  • 关系型数据库
  • LDAP

使用基于内存的用户存储

package spitter.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebMvcSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigureAdapter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth.inMemoryAuthentication()
                    .withUser("user").password("password").roles("USER").and()
                .withUser("admin").password("password").roles("USER","ADMIN");
    }
}
  • imMemoryAuthentication()指定使用基于内存的用户存储
  • withUser()方法为内存用户存储添加新的用户,其用户名为传入其中的字符串
  • password()方法为用户指定密码
  • roles() 为给定用户授予一个或多个角色权限(它是 authorities() 方法的简写形式,roles()方法所给定的值都会添加一个”ROLE_”前缀,并将其作为权限授予给用户)

以下是其他一些方法和它们的功能描述
QQ截图20191004152616.png

基于内存的用户存储,适用于调试和开发人员的测试。
对于生产环境,通常最好将用户数据保存在某种类型的数据库之中。

基于数据库表进行认证

最少配置如下所示

@Autowired
DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.jdbcAuthentication()
            .dataSource(dataSource);
}

该配置会使用默认的用户查询功能,查询语句如下
QQ截图20191004153652.png

第一个查询用于用户认证
第二个查询用于用户授权,查看用户被赋予的角色
第三个查询用于用户授权,查看用户所在用户组所赋予的权限

当默认的查询功能不符合我们需求时,我们就需要设置自己的查询。可以通过如下方式

@Autowired
DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery(
                    "select username,password,true" +
                    " from Spitter where username=?")
            .authoritiesByUsernameQuery(
                    "select username, 'ROLE_USER' from Spitter where username=?"
            );
}
  • usersByUsernameQuery() 对应上面第一个查询语句,用于用户认证
  • authoritiesByUsernameQuery() 对应上面第二个查询语句,用于用户授权验证

同时还可以指定一个密码转换器,防止黑客窃取用户密码

@Autowired
DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery(
                    "select username,password,true" +
                    " from Spitter where username=?")
            .authoritiesByUsernameQuery(
                    "select username, 'ROLE_USER' from Spitter where username=?"
            .passwordEncoder(new StandarPasswordEncoder("53cr3t"))
            );
}

passwordEncoder()方法可以接受Spring Security 中 PasswordEncoder 接口的任意实现。
Spring Security 的加密模块包括了三个这样的实现:

  • BCryptPasswordEncoder
  • NoOpPasswordEncoder
  • StandardPasswordEncoder

如果内置的实现无法满足需求,你可以提供自己的实现,
PasswordEncoder 接口定义如下

public interface PasswordEncoder {
    String encode(CharSequence rawPassword);
    boolean matches(CharSequence rawPassword, String encodedPassword);
}

基于 LDAP 进行认证

简单配置如下

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.ldapAuthentication()
            .userSearchFilter("{uid={0}}")
            .groupSearchFilter("member={0}");
}
  • userSearchFilter() 对应上述第一个查询语句,用于用户认证
  • groupSearchFilter() 对应上述第三个查询语句,用于用户授权,检查用户所在组被赋予的权限。

当不配置上述两个方法的基础查询时,表名搜索会在LDAP层级结构的根开始。但是我们可以指定查询基础来改变这个默认行为。

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.ldapAuthentication()
            .userSearcheBase("ou=people")
            .userSearchFilter("{uid={0}}")
            .groupSearchBase("ou=groups")
            .groupSearchFilter("member={0}");
}  

配置密码比对
使用 passwordCompare() 方法

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.ldapAuthentication()
            .userSearcheBase("ou=people")
            .userSearchFilter("{uid={0}}")
            .groupSearchBase("ou=groups")
            .groupSearchFilter("member={0}")
            .passwordCompare();
}

默认情况下,在登录表单中提供的密码将会与用户的LDAP条目中的userPassword属性进行比对。如果密码被保存在不同的属性中,可以通过 passwordAttribute() 方法来声明密码属性的名称。同时我们可以通过passwordEncoder()方法对其进行加密

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.ldapAuthentication()
            .userSearcheBase("ou=people")
            .userSearchFilter("{uid={0}}")
            .groupSearchBase("ou=groups")
            .groupSearchFilter("member={0}")
            .passwordCompare()
            .passwordEncoder(new Md5PasswordEncoder())
            .passwordAttribute("passcode");
}  

引用远程的LDAP服务器
可以使用 contextSource()方法来配置远程地址

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.ldapAuthentication()
            .userSearcheBase("ou=people")
            .userSearchFilter("{uid={0}}")
            .groupSearchBase("ou=groups")
            .groupSearchFilter("member={0}")
            .contextSource()
                .url("ldap://habuma.com:389/dc=habuma,dc=com");
}

引用嵌入式的LDAP服务器
同样使用 contextSource() 方法来配置

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.ldapAuthentication()
            .userSearcheBase("ou=people")
            .userSearchFilter("{uid={0}}")
            .groupSearchBase("ou=groups")
            .groupSearchFilter("member={0}")
            .contextSource()
                .root("classpath:users.ldif");
}  

配置自定义的用户服务

当我们使用菲关系型数据库,例如mongodb,redis时,就需要自定义用户服务了。我们需要提供一个自定义的 UserDetailsService 接口实现
UserDetailsService 接口定义如下

public interface UserDetailsService{
    Userdetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

我们所需要做的就是实现 loadUserByUsername() 方法,根据给定的用户名来查找用户。

以下是一个 UserDetailsService 实现
QQ截图20191004170440.png

另外一种值得考虑的方案就是修改Spitter,让其实现UserDetails。这样的话,loadUserByUsername()就能直接返回 Spitter 对象了,而不必将它的值复制到 User 对象

在自己实现了一个 UserDetailsService 后,我们就可以使用它了

@Autowired
SpitterRepository spitterRepository;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
    auth.userDetailsService(new SpitterUserService(spitterRepository));
}

拦截请求(重载configure(HttpSecurity)方法)

在Web应用中,有些请求需要认证,有些请求需要授权,有些请求什么都不需要,这里就需要通过重载 configure(HttpSecurity) 方法来实现了
需要注意的是,在配置时,描述越详细的请求应放在最前面,以防止被覆盖
示例如下

    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("spitters/me").authenticated()
            .antMatchers(HttpMethod.POST,"spittles").authenticated()
            .anyRequest().permitAll();
}  

其中

  • authorizeRequests() 返回的对象的方法用来配置请求级别的安全性细节
  • antMatchers() 支持Ant风格的通配符,用来匹配路径
  • regaxMatchers() 支持正则表达式,用法同上
  • anyRequest() 表示任意请求

还有其他的方法如下所示
QQ截图20191004171926.png

遗憾的是,上面的方法大多都是一维的,也就是说,我们不能在相同的路径上同时使用上面的方法。
幸运的是,我们可以借助 access()方法和 Spring表达式语言(SpEL)来提供多种访问限制

使用Spring表达式进行安全保护

Spring Security 通过一些安全性相关的表达式拓展了Spring 表达式语言,表格如下
QQ截图20191004173756.png

用法如下

.antMatchers("spitters/me")
    .access("hasRole('ROLE_SPITTER') and hasIpAddress('192.168.1.2')")

强制通道的安全性

下例将强制要求对”/spitter/form”的请求,需要使用HTTPS协议,并自动将请求重定向到HTTPS上

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("spitters/me").authenticated()
            .antMatchers(HttpMethod.POST,"spittles").authenticated()
            .anyRequest().permitAll()
        .and()
        .requiresChannel()
            .antMatchers("spitter/form").requiresSecure();
}

下例会对”/“的请求,重定向到HTTP上

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("spitters/me").authenticated()
            .antMatchers(HttpMethod.POST,"spittles").authenticated()
            .anyRequest().permitAll()
        .and()
        .requiresChannel()
            .antMatchers("spitter/form").requiresSecure()
            .antMatchers("/").requiresInsecure();
}

防止CSRF攻击

Spring Security3.2开始,默认就会启用 CSRF 防护,它通过一个同步 token方式来实现CSRF防护的功能。
它将会拦截状态变化的请求。并检查 CSRF token。 如果请求中不包含 CSRF Token的话,或者 token 不能与服务器端的token相匹配,请求将会失败,并抛出 CsrfException 异常。

这意味着应用中所有的表单必须在一个”_csrf”域中提交token。
而Spring Security 已经简化了将 token放到请求的属性中这一任务。
以下是两个示例

  • Thymleaf作为页面模板

    <form method="POST" th:action="@{/spittles}">
        ...
    </form>
    
  • JSP作为页面模板

    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />  
    

我们也可以禁用 CSRF 防护(不推荐)

protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();
}  

认证用户

提供自定义认证表单

protected void configure(HttpSecurity http) throws Exception {
    http
        .formLogin()
            .loginPage("/login")
    ......
}

对指定域启用HTTP Basic 认证

protected void configure(HttpSecurity http) throws Exception {
    http
        .formLogin()
            .loginPage("/login")
        .and()
        .httpBasic()
            .realmName("Spittr")
        .and()
    ......
}  

启用Remember-me功能

protected void configure(HttpSecurity http) throws Exception {
    http
        .formLogin()
            .loginPage("/login")
        .and()
        .rememberMe()
            .tokenValiditySeconds(2419200)
            .key("spittrKey")
    ......
}  

退出

protected void configure(HttpSecurity http) throws Exception {
    http
        .formLogin()
            .loginPage("/login")
        .and()
        .logout()
            .logoutSuccessUrl("/")
            .logoutUrl("/signout")      //不设置时,默认登出路径为"/logout"
    ......
}  

保护方法应用

上面的方法是用于保护应用的 Web 层,这种保护是比较笼统的,你不能执行更细粒度的保护。 例如当出现了一个 URL ,管理员和普通用户都能访问,但管理员可以查看所有用户的信息,但普通用户只能查看自己的信息时,上述保护 Web 层的方法就显得有点鸡肋了,因为如果你想实现这种需求,运用上述方法,你只能写两个不同的方法来分别响应不同的请求了。

所以现在我们学习应该如何保护场景后面的方法,这样就能保证如果用户不具备权限的话,就无法执行相应的逻辑了,上述的例子你就可以将其合并为一个方法。

Spring Security 提供了三种不同的安全注解:

  • Spring Security 自带的 @Secured 注解
  • JSR-250 的 @RolesAllowed 注解
  • 表达式驱动的注解,包括@PrsAuthorize、@PostAuthorize、@PreFilter、@PostFilter
    @Secured 和 @RolesAllowed 方案非常类似,能够基于用户所授予的权限限制对方法的访问。当我们需要在方法上定义更灵活的安全规则时, Spring Security 提供了@PreAuthorize 和 @PostAuthorize 而 @PreFilter 、 @PostFilter 能够过滤方法返回的以及传入方法的集合

使用 @Secured 注解限制方法调用

在 Spring 中,如果要启用基于注解的方法安全性,关键之处在于要在配置类上使用 @EnableGlobalMethodSecurity。

例子如下所示

@Configuration
@EnableGlobalMethodSecurity(securedEnabled=true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

}

注意

  • @EnableGlobalMethodSecurity 启用的是基于注解的方法安全性
  • @EnableGlobalMethodSecurity 中的securedEnabled属性的值为 true 时,将会创建一个切点,这样的话 Spring Security 切面就会包装带有 @Secured 注解的方法。
  • GlobalMethodSecurityConfiguration 类能够为方法级别的安全性提供更精细的配置
  • @EnableWebSecurity 启用的是基于注解的Web安全性
  • WebSecurityConfigurerAdapter 类能够为 Web级别的安全性提供配置

同样的,我们可以通过重载 GlobalMethodSecurityConfiguration 的 configure() 方法来实现 Web层的安全配置中设置认证:

@Configuration
@EnableGlobalMethodSecurity(securedEnabled=true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth.inMemoryAuthentication()
                    .withUser("user").password("password").roles("USER").and()
                .withUser("admin").password("password").roles("USER","ADMIN");
    }
}  

我们还可以重载 createExpressionHandler() 方法,提供一些自定义的安全表达式处理行为

然后我们就可以在控制器中使用 @Secured 注解了

@Secured({"ROLE_SPITTER","ROLE_ADMIN"})
public void addSpittle(Spittle spittle) {

}

如果方法被没有认证的用户或没有所需权限的用户调用,保护这个方法的切面将抛出一个 Spring Security 异常( AuthenticationException 或 AccessDeniedException)。它们是非检查型异常,但这个异常最终必须要被捕获和处理。如果被保护的方法是在 Web 请求中调用的,这个异常会被 Spring Security 的过滤器自动处理。否则的话,你需要编写代码来处理这个异常.

使用 JSR-250 的 @RolesAllowed 注解

使用@RoleAllowed 注解 和 @Secured 注解 在各个方面基本上都是一直的。唯一显著的区别在于 @RolesAllowed 注解 是 JSR-250 定义的Java 标准注解。 当使用其他框架(非 Spring)时,@RolesAllowed将更会有意义。

我们先实现配置类

@Configuration 
@EnableGlobalMethodSecurity(jsr250Enabled=true)  
public class MathodSecurityConfig extends GlobalMethodSecurityConfiguration {

}
  • 当 jsr250Enabled 属性设置为 true 之后,将会启用一个切点,这样带有 @RolesAllowed 注解的方法都会被 Spring Security 的切面包装起来。

接下来我们就可以使用它

@RolesAllowed("ROLE_SPITTER")
public void addSpittle(Spittle spittle) {

}  

无论是 @RolesAllowed 注解还是 @Secured 注解。它们都有一个共同的不足。它们只能根据用户有没有授予特定的权限来限制方法的调用。在判断方法是否执行方面,无法使用其他的因素,接下来我们看一下如何使用 SpEL 与 Spring Security 所提供的方法调用前后注解,实现基于表达式的方法安全性。

使用表达式实现方法级别的安全性

Spring Security 3.0 引入了几个新注解,这些注解的值参数中都可以接受一个 SpEL 表达式。表达式可以是任意合法的 SpEL 表达式。如果表达式的计算结果为 true ,那么安全规则通过,否则就会失败。安全规则通过或失败的结果会因为所使用注解的差异而有所不同。

QQ截图20191011212215.png

同样。我们首先应该实现配置类

@Configuration  
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration{

}  
  • 当prePostEnabled 设置为 true 时 ,它会创建一个切点,这样的话 Spring Security 切面就会包装带有上述4个注解的方法了

表述方法访问规则

使用以下两个注解

  • @PreAuthorize
    表达式会在方法调用之前执行,如果表达式的计算结果不为 true 的话,将会阻止方法执行

  • @PostAuthorize
    表达式会在方法调用之后执行,如果表达式的计算结果不为 true 的话,将会抛出安全性的异常

它们两个比 @Secured 和 @RolesAllowed 更灵活

  • @PreAuthorize 使用示例

    以下是例子,它会检查字数,如果是普通用户那么字数应该在140字以内,如果是付费用户,则不限制字数

    @PreAuthorize( "(hasRole('ROLE_SPITTER') and #spittle.text.length()<= 140>)" + "or hasRole('ROLE_PREMIUM')")
    public void addSpittle(Spittle spittle) {
    
    }  
    

    表达式中的 #spittle 部分直接引用了方法中的同名参数

  • @PostAuthorize

    @PostAuthorize("returnObject.spitter.username == principal.username")  
    public Spittle getSpittleById(long id) {
    
    }
    

    为了遍历的访问受保护方法的返回对象 Spring Security 在 SpEL 中提供了名为 returnObject 的变量。
    而 principal 是另一个 Spring Security 内置的特殊名称,它代表了当前认证用户的主要信息

过滤方法的输入与输出

使用以下两个注解

  • @PreFilter
    该注解会过滤传入方法的参数(过滤输入)
    下例,能保证方法的列表中只包含当前用户有权限删除的 Spittle

    @PreAuthorize( "(hasRole({'ROLE_SPITTER','ROLE)ADMIN'})")
    @PreFilter( "hasRole('ROLE_ADMIN') || " + 'targetObject.spitter.username == principal.name')
    public void deleteSpittle(List<Spittle> spittles) {
        ...
    }   
    

    targetObject 是 Spring security 提供的另外一个值,它代表了要进行计算的当前列表元素

  • @PostFilter
    该注解会过滤方法返回的值(过滤输出)

    @PreAuthorize( "(hasRole({'ROLE_SPITTER','ROLE)ADMIN'})")
    @PostFilter( "hasRole('ROLE_ADMIN') || " + 'filterObject.spitter.username == principal.name')
    public List<Spittle> getoffensiveSpittles() {
        ...
    }   
    

    FilterObject 是 Spring security 提供的另外一个值,它代表了这个方法所返回的列表中的某一个元素

上面的两个注释对于安全规则的设置很灵活,但当安全规则的逻辑很复杂时,并不推荐直接写在注释里,一是可读性不好,二是其测试困难,所以此时我们会选择定义许可计算器来解决问题

定义许可计算器
使用 hasPermission()
hasPermission() 函数是 Spring Security 为SpEL提供的拓展,它为开发者提供了一个世纪能够在执行计算的时候插入任意的逻辑。我们所需要做的就是编写并注册一个自定义的许可计算器。
为此,我们需要实现 Spring Security 的 PermissionEvaluator 接口,该接口需要实现两个不用的 hasPermission() 方法。

  • 其中的一个 hasPermission() 方法把要评估的对象作为第二个参数
  • 第二个 hasPermission() 方法在只有目标对象的 ID 可以得到的时候才有用,并将 ID 作为 Serializable 传入第二个参数

下面是一个示例

QQ截图20191011221747.png

我们就可以使用

@PreAuthorize("hasAnyRole({'ROLE_SPITTER','ROLE_ADMIN'})")
@PreFilter("hasPermission(targetObject,'delete')")
public void deleteSpittles(List<Spittle> spittles) { ... }
文章作者: f1rry
文章链接: http://yoursite.com/2019/07/29/保护Web应用/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 F1rry's blog