Spring Security 中的访问总是被拒绝 - DenyAllPermissionEvaluator

Posted

技术标签:

【中文标题】Spring Security 中的访问总是被拒绝 - DenyAllPermissionEvaluator【英文标题】:Access is Always Denied in Spring Security - DenyAllPermissionEvaluator 【发布时间】:2017-06-11 20:23:07 【问题描述】:

我已经在我的 Spring Boot 应用程序中配置了 ACL。 ACL配置如下:

@Configuration
@ComponentScan(basePackages = "com.company")
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class ACLConfigration extends GlobalMethodSecurityConfiguration 

    @Autowired
    DataSource dataSource;

    @Bean
    public EhCacheBasedAclCache aclCache() 
        return new EhCacheBasedAclCache(aclEhCacheFactoryBean().getObject(), permissionGrantingStrategy(), aclAuthorizationStrategy());
    

    @Bean
    public EhCacheFactoryBean aclEhCacheFactoryBean() 
        EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
        ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject());
        ehCacheFactoryBean.setCacheName("aclCache");
        return ehCacheFactoryBean;
    

    @Bean
    public EhCacheManagerFactoryBean aclCacheManager() 
        return new EhCacheManagerFactoryBean();
    

    @Bean
    public DefaultPermissionGrantingStrategy permissionGrantingStrategy() 
        ConsoleAuditLogger consoleAuditLogger = new ConsoleAuditLogger();
        return new DefaultPermissionGrantingStrategy(consoleAuditLogger);
    

    @Bean
    public AclAuthorizationStrategy aclAuthorizationStrategy() 
        return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ACL_ADMIN"));
    

    @Bean
    public LookupStrategy lookupStrategy() 
        return new BasicLookupStrategy(dataSource, aclCache(), aclAuthorizationStrategy(), new ConsoleAuditLogger());
    

    @Bean
    public JdbcMutableAclService aclService() 
        return new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache());
    

    @Bean
    public DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() 
        return new DefaultMethodSecurityExpressionHandler();
    

    @Override
    public MethodSecurityExpressionHandler createExpressionHandler() 
        DefaultMethodSecurityExpressionHandler expressionHandler = defaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService()));
        expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService()));
        return expressionHandler;
    

参考资料:

SO Q1 SO Q2

安全配置如下:

@Configuration
@EnableWebSecurity
public class CustomSecurityConfiguration extends WebSecurityConfigurerAdapter 

    @Bean
    public AuthenticationEntryPoint entryPoint() 
        return new LoginUrlAuthenticationEntryPoint("/authenticate");
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 

        http
                .csrf()
                .disable()
                .authorizeRequests()
                .antMatchers("/authenticate/**").permitAll()
                .anyRequest().fullyAuthenticated()
                .and().requestCache().requestCache(new NullRequestCache())
                .and().addFilterBefore(authenticationFilter(), CustomUsernamePasswordAuthenticationFilter.class);
    

    @Override
    public void configure(WebSecurity web) throws Exception 
        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
    

    @Bean
    public CustomUsernamePasswordAuthenticationFilter authenticationFilter()
            throws Exception 
        CustomUsernamePasswordAuthenticationFilter authenticationFilter = new CustomUsernamePasswordAuthenticationFilter();
        authenticationFilter.setUsernameParameter("username");
        authenticationFilter.setPasswordParameter("password");
        authenticationFilter.setFilterProcessesUrl("/authenticate");
        authenticationFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
        authenticationFilter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
        authenticationFilter.setAuthenticationManager(authenticationManagerBean());
        return authenticationFilter;
    

    @Bean
    public PasswordEncoder passwordEncoder() 
        return new BCryptPasswordEncoder();
    

我的CustomAuthenticationProvider班级:

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider 

    @Autowired
    private UsersService usersService;

    @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException 

        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        User user = usersService.findOne(username);

        if(user != null && usersService.comparePassword(user, password))

            return new UsernamePasswordAuthenticationToken(
                    user.getUsername(),
                    user.getPassword(),
                    AuthorityUtils.commaSeparatedStringToAuthorityList(
                            user.getUserRoles().stream().collect(Collectors.joining(","))));
         else 
            return null;
        
    

    @Override
    public boolean supports(Class<?> authentication) 
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    

这是我的CustomUsernamePasswordAuthenticationToken

public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter 

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException 

        if(!request.getMethod().equals("POST"))
            throw new AuthenticationServiceException(String.format("Authentication method not supported: %s", request.getMethod()));

        try 

            CustomUsernamePasswordAuthenticationForm form = new ObjectMapper().readValue(request.getReader(), CustomUsernamePasswordAuthenticationForm.class);

            String username = form.getUsername();
            String password = form.getPassword();

            if(username == null)
                username = "";

            if(password == null)
                password = "";

            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);

            setDetails(request, token);

            return getAuthenticationManager().authenticate(token);

         catch (IOException exception) 
            throw new CustomAuthenticationException(exception);
        
    

    private class CustomAuthenticationException extends RuntimeException 
        private CustomAuthenticationException(Throwable throwable) 
            super(throwable);
        
    

除了上述之外,我还有CustomAuthenticationFailureHandlerCustomAuthenticationSuccessHandlerCustomNoRedirectStrategyCustomUsernamePasswordAuthenticationForm,为了这个问题的长度,我跳过了它们。

我正在使用可以在here 找到的 mysql 架构。

我正在向我的 acl 相关表中添加如下条目:

INSERT INTO acl_class VALUES (1, com.company.project.domain.users.User)
INSERT INTO acl_sid VALUES (1, 1, "demo")

(我有一个用户名为demo

INSERT INTO acl_object_identity VALUES (1, 1, 1, NULL, 1, 0)
INSERT INTO acl_entry VALUES (1, 1, 1, 1, 1, 1, 1, 1)

但我得到的只是:

Denying user demo permission 'READ' on object com.company.project.domain.users.User@4a49e9b4

在我的

@PostFilter("hasPermission(filterObject, 'READ')")

我怀疑这里有几个问题:

    hasPermission 表达式:我已将其替换为 'READ' 和 '1',但没有达到任何程度。 我的数据库条目不正确 我没有实现自定义权限评估程序。这是必需的,还是expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService())); 足够?

更新

使用@PostFilter的示例方法:

@RequestMapping(method = RequestMethod.GET)
    @PostFilter("hasPermission(filterObject, 'READ')")
    List<User> find(@Min(0) @RequestParam(value = "limit", required = false, defaultValue = "10") Integer limit,
                    @Min(0) @RequestParam(value = "page", required = false, defaultValue = "0") Integer page,
                    @RequestParam(value = "email", required = false) String email,
                    @RequestParam(value = "firstName", required = false) String firstName,
                    @RequestParam(value = "lastName", required = false) String lastName,
                    @RequestParam(value = "userRole", required = false) String userRole) 

        return usersService.find(
                limit,
                page,
                email,
                firstName,
                lastName,
                userRole);
    

更新 #2:

问题现在反映了有关身份验证/授权/ACL 的所有设置。

更新 #3:

我现在非常接近解决这个问题,唯一剩下的就是解决这个问题:

https://***.com/questions/42996579/custom-permissionevaluator-not-called-although-set-as-permissionevaluator-deny

如果有人可以帮助我解决这个问题,我终于可以写下我为解决这个问题所经历的事情了。

【问题讨论】:

是@PostFilter 在实现接口的公共上的方法吗? 不,它在@RestController@Controller 上。我真的怀疑数据库条目,或者不存在的组件。 这个方法怎么样,你把@PostFilter注解放在哪里了?您的服务器日志中是否有任何 Stacktrace? 我不是很喜欢 ACL,但您是否混淆了 @PreAuthorize@PostFilter 的用法?见concretepage.com/spring/spring-security/… 并非如此。正如官方文档(以及您共享的链接)所建议的那样,@PostFilter 根据经过身份验证的用户是否有权执行表达式中所述的操作来过滤返回对象。这正是我想要达到的目标。问题是,无论数据库中的 ACL 条目如何,每个对象都会被过滤。 【参考方案1】:

我升级了我的应用程序以使用 Spring Security 4.2.1.RELEASE,然后我开始遇到在所有 @PreAuthorize 注释方法中被拒绝的意外访问,这在升级之前工作得很好。 我调试了 spring 安全代码,我意识到问题是所有要检查的角色都以默认字符串“ROLE_”作为前缀,而不管我将默认前缀设置为空,如下面的代码所示。

auth.ldapAuthentication()
        .groupSearchBase(ldapProperties.getProperty("groupSearchBase"))
        .groupRoleAttribute(ldapProperties.getProperty("groupRoleAttribute"))
        .groupSearchFilter(ldapProperties.getProperty("groupSearchFilter"))

        //this call used to be plenty to override the default prefix
        .rolePrefix("")

        .userSearchBase(ldapProperties.getProperty("userSearchBase"))
        .userSearchFilter(ldapProperties.getProperty("userSearchFilter"))
        .contextSource(this.ldapContextSource);

我所有的控制器方法都用 @PreAuthorize("hasRole('my_ldap_group_name')") 注释,但是,框架没有考虑我的空角色前缀设置,因此它使用 ROLE_my_ldap_group_name 来检查实际角色。 p>

在深入研究了框架的代码后,我意识到org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler 类的默认角色前缀仍然设置为"ROLE_"。我跟进了它的值的来源,我发现它首先检查类 org.springframework.security.config.core.GrantedAuthorityDefaults 的声明 bean 以在第一次初始化 bean org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer 期间查找默认前缀,但是,因为这个初始化器 bean 可以找不到它声明,它最终使用了上述默认前缀。

我相信这不是预期的行为:Spring Security 应该考虑与 ldapAuthentication 相同的 rolePrefix,但是,为了解决这个问题,有必要将 bean org.springframework.security.config.core.GrantedAuthorityDefaults 添加到我的应用程序上下文中(我正在使用注释基于配置),如下:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CesSecurityConfiguration extends WebSecurityConfigurerAdapter 

    private static final String ROLE_PREFIX = "";
    //... ommited code ...
    @Bean
    public GrantedAuthorityDefaults grantedAuthorityDefaults() 
        return new GrantedAuthorityDefaults(ROLE_PREFIX);
    


也许你遇到了同样的问题 - 我可以看到你正在使用 DefaultMethodSecurityExpressionHandler 并且它还使用 bean GrantedAuthorityDefaults,所以如果你使用与我相同的 Spring Security 版本 - 4.2.1.RELEASE 你是可能会遇到同样的问题。

【讨论】:

感谢您的回答,非常感谢。我知道 Spring 需要 ROLE_ 前缀,我所有的角色都以 ROLE_ 开头。所以我几乎不认为它应该与角色名称有关。另外,我不根据角色进行过滤或授权,而是使用 acl 根据对象/用户检查权限。 好了,这就是我要分享的,希望你能解决问题,我从来没有使用过ACL... 感谢您的彻底调查和精心设计的答案!当我升级到 spring-security-acl 5.3.0.RELEASE 时,这解决了我的问题 谢谢!这完全救了我。我从 2.2.7 升级到 spring-boot 2.4.4(使用 spring 5.3.5),突然 @Secured 注释将不再起作用。显然,通过扩展 GlobalMethodSecurityConfiguration 和覆盖 accessDecisionManager 来设置角色前缀已经不够了。按照您的建议设置grantAuthorityDefaults 后,它再次起作用!【参考方案2】:

您在数据库中的数据和您的配置看起来不错。我一直使用@PostFilter("hasPermission(filterObject, 'READ')")

我会检查以确保您扩展 UserDetails 的用户类通过您在数据库中的 getUsername() 返回相同的用户名。除了检查以确保您的安全性和应用程序处于同一上下文中。

hasPermission 方法将 Authentication 对象作为第一个参数。

boolean hasPermission(Authentication authentication,
                      Object targetDomainObject,
                      Object permission)

Authentication 对象是一个实现类,通常是UsernamePasswordAuthenticationToken。因此,getPrincipal() 方法需要返回一个对象,该对象具有 getUserName() 方法,该方法返回与数据库中相同的内容。

看看PrincipalSid

public PrincipalSid(Authentication authentication) 
    Assert.notNull(authentication, "Authentication required");
    Assert.notNull(authentication.getPrincipal(), "Principal required");

    if (authentication.getPrincipal() instanceof UserDetails) 
        this.principal = ((UserDetails) authentication.getPrincipal()).getUsername();
    
    else 
        this.principal = authentication.getPrincipal().toString();
    

【讨论】:

我不太会使用UserDetailsUserDetailsService。我只从我的班级返回UsernamePasswordAuthenticationToken implements AuthenticationProvider。我必须这样做吗? 添加更多细节。听起来您没有正确设置身份验证对象。 我同意。我已更新问题以反映我为身份验证/授权和 ACL 配置的任何内容。 你应该在你的用户对象上添加一个 toString() 方法,它会打印出比内存位置更有用的东西。例如,用户名。 我还要仔细检查一下在 CustomUsernamePasswordAuthenticationFilter 中是否正确创建了 UsernamePasswordAuthenticationToken【参考方案3】:

这是期待已久的答案:

documentation 明确描述:

要使用 hasPermission() 表达式,您必须显式配置一个 PermissionEvaluator 在您的应用程序上下文中。这看起来 像这样:

所以基本上我是在我的AclConfiguration 中做的,它扩展了GlobalMethodSecurityConfiguration

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() 
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService()));
        expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService()));
        return expressionHandler;
    

Spring 没有处理它!

我不得不将AclConfigGlobalMethodSecurityConfiguration 分开。当后者中定义了@Beans 时,上述方法没有得到处理,这可能是一个错误(如果没有,欢迎对主题进行任何澄清)。

【讨论】:

对我来说看起来有点不一致。花了几个小时修复我在重构时破坏的东西。后来我在尝试您的解决方案时意识到:1.使用了错误的 PermissionEvaluator 2.添加 @component 没有添加显式配置。【参考方案4】:

显然,AclPermissionEvaluator 不受尊重。我的路上也出现了同样的问题,分两步解决:

    在被覆盖的方法中创建表达式处理程序而不是注入 WebSecurityConfig 是在与 GlobalMethodSecurityConfiguration 相同的类中创建的,否则,GlobalMethodSecurityConfiguration 将不起作用并且不会覆盖处理程序。

【讨论】:

以上是关于Spring Security 中的访问总是被拒绝 - DenyAllPermissionEvaluator的主要内容,如果未能解决你的问题,请参考以下文章

Spring Security 总是返回 HTTP 403 访问被拒绝 [关闭]

在struts2中使用spring security总是显示访问被拒绝页面

在 Spring Security 中启用 CSRF 时,访问被拒绝 403

Spring security hasRole() 给出错误 403 - 访问被拒绝

从 Spring Boot Starter 1.3.5.RELEASE 升级到 1.5.2 RELEASE 时 Spring Security 中的访问被拒绝错误

面对拒绝访问 (403) - spring security oauth2 中的禁止错误