在尝试访问Spring Security登录页面时出现了访问被拒绝的异常。

5

我正在使用基于Java的Spring安全机制。我创建了自定义访问决策投票者实现。

但是当我运行应用程序时,无法打开登录页面,因为它显示访问被拒绝。

这发生在我添加自定义访问决策投票者实现后。我猜测问题出在自定义AccessDecisionVoter中以下代码的原因。

if(authentication instanceof AnonymousAuthenticationToken)
            return ACCESS_DENIED;

但我需要这样做,以便未登录用户不会受到权限检查的限制。

然而,它会进入无限循环:登录页面、访问决策投票器、访问被拒绝、登录页面等等。

下面是Spring Security配置代码。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AffirmativeBased accessDecisionManager;

    @Bean
    @Autowired
    public AffirmativeBased accessDecisionManager(AccessDecisionVoterImpl accessDecisionVoter) {
        List<AccessDecisionVoter<?>> accessDecisionVoters = new ArrayList<AccessDecisionVoter<?>>();
        accessDecisionVoters.add(accessDecisionVoter);
        AffirmativeBased accessDecisionManager = new AffirmativeBased(accessDecisionVoters);
        return accessDecisionManager;
    }

    @Override
    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        PasswordEncoder passwordEncoder = new PasswordEncoder();
        passwordEncoder.setStringDigester(stringDigester());
        return passwordEncoder;
    }

    @Bean
    public PooledStringDigester stringDigester() {
        PooledStringDigester psd = new PooledStringDigester();

        psd.setPoolSize(2);
        psd.setAlgorithm("SHA-256");
        psd.setIterations(1000);
        psd.setSaltSizeBytes(16);
        psd.setSaltGenerator(randomSaltGenerator());

        return psd;
    }

    @Bean
    public RandomSaltGenerator randomSaltGenerator() {
        RandomSaltGenerator randomSaltGenerator = new RandomSaltGenerator();
        return randomSaltGenerator;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers("/static/**")
                .antMatchers("/i18n/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
        .and()
            .formLogin()
            .loginPage("/login")
            .loginProcessingUrl("/checkLogin")
            .defaultSuccessUrl("/home")
            .failureUrl("/login?login_error=1")
            .usernameParameter("username")
            .passwordParameter("password")
            .permitAll()
        .and()
            .logout()
            .logoutUrl("/logout")
            .logoutSuccessUrl("/login?isLoggedOut=1")
            .deleteCookies("JSESSIONID")
            .invalidateHttpSession(true)
            .permitAll()
        .and()
            .authorizeRequests()
            .antMatchers("/login**").permitAll()
            .antMatchers("/error**").permitAll()
            .antMatchers("/checkLogin**").permitAll()
            .anyRequest()
            .authenticated()
            .accessDecisionManager(accessDecisionManager)
        .and()
            .exceptionHandling()
            .accessDeniedPage("/accessDenied")
        .and()
            .headers()
            .frameOptions()
            .disable()
        .and()
            .sessionManagement()
            .invalidSessionUrl("/login")
            .maximumSessions(1);
    }

}

以及我的自定义投票者实现

@Component
public class AccessDecisionVoterImpl implements AccessDecisionVoter {

    @Autowired
    private ModuleService moduleService;

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class clazz) {
        return true;
    }

    @Override
    public int vote(Authentication authentication, Object object, Collection collection) {
// i have given this so that if user is not logged in then should not check permission at all 
        if(authentication instanceof AnonymousAuthenticationToken)
            return ACCESS_DENIED;

             HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
    String requestedOperation = request.getParameter("operation");

    if (requestedOperation != null && !requestedOperation.isEmpty()){
        String [] requestURISplit = request.getRequestURI().split("/");
        String requestedModuleName = requestURISplit[2];

        if(SecurityUtils.hasPermission(requestedModuleName, requestedOperation)){
           return ACCESS_GRANTED;
        }
    } else {
        return ACCESS_GRANTED;
    }

    return ACCESS_DENIED; 

此外,如果我从我的投票者中删除以下行,如果用户未登录并尝试访问受保护的页面,则会继续访问。它应该重定向到登录页面。
if(authentication instanceof AnonymousAuthenticationToken)
                return ACCESS_DENIED;

这是我第一次尝试使用Spring Boot。因此,我不确定所有的配置问题。

antMatchers的顺序是否有问题?

请帮忙。


我想检查用户是否具有访问受保护内容所需的权限。当用户登录时,我会将他的权限(如addUser、activateUser等)保存在会话中。当他访问URL时,我会将这些权限传递到URL中,然后根据会话中的权限进行检查。这就是我在AccessDecisionVoterImpl中所做的事情。 - ajm
不要使用 AccessDecisionVoter 来实现此功能。您可以在 antMatcher 中使用 hasRole 来确保访问权限。 - Ali Dehghani
你能否用真实的代码或至少一些代码替换 //// some other code to check role module based permissions. - Ali Dehghani
更新了代码。基本上,模块就像用户,对其进行的操作就像添加用户、删除用户、列出用户、编辑用户、激活用户等。 - ajm
事实上,我以前使用基于XML的配置成功地完成过这个任务。但这是我第一次尝试使用Spring Boot和Java配置。 - ajm
显示剩余6条评论
3个回答

4

您的 访问决策投票者(AccessDecisionVoter) 需要执行以下操作之一:

  • 弃权 (AccessDecisionVoter.ACCESS_ABSTAIN):如果投票者无法做出决策(例如,用户未经授权,无法从请求上下文中获取模块等)
  • 授予权限 (AccessDecisionVoter.ACCESS_GRANTED):如果可以识别模块并且用户已经获得授权
  • 拒绝访问 (AccessDecisionVoter.ACCESS_DENIED):如果可以识别模块并且用户未被授权

通过您的 AccessDecisionManager 配置,您基本上取消了基于URL的访问限制,例如:

http.authorizeRequests()
    .antMatchers("/css/**", "/img/**", "/js/**", "/font/**").permitAll()
    .antMatchers("/login**").permitAll()
    .antMatchers("/error**").permitAll()
    .antMatchers("/checkLogin**").permitAll()
    .anyRequest()
        .authenticated()

默认情况下,Spring使用WebExpressionVoter来实现这个目的。然而,AffirmativeBased AccessDecisionManager会在至少有一个AccessDecisionVoter授予资源访问权限时授权(这可能不是您想要的)。根据您的要求,包含WebExpressionVoterConsensusBased AccessDecisionManager将是最佳选择。
@Bean
public AccessDecisionManager accessDecisionManager() {
    List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<>();
    decisionVoters.add(new WebExpressionVoter());
    decisionVoters.add(new ModuleAccessDecisionVoter());
    ConsensusBased consensusBased = new ConsensusBased(decisionVoters);

    // deny access if voters equally voted to allow and deny access
    consensusBased.setAllowIfEqualGrantedDeniedDecisions(false);
    return consensusBased;
}

您的 AccessDecisionVoter 实现:

static class ModuleAccessDecisionVoter implements AccessDecisionVoter<FilterInvocation> {

    public int vote(Authentication authentication, FilterInvocation object, Collection<ConfigAttribute> attributes) {
        if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
            return ACCESS_ABSTAIN;
        }

        // determine module and grant or deny access
        // if module cannot be determined abstain from voting
        String module = determineModule(object);
        if (module != null) {
            return isAccessGranted(module, authentication) ? ACCESS_GRANTED : ACCESS_DENIED
        }

        return ACCESS_ABSTAIN;
    }
}

匿名访问应导致以下结果:
- /login:WebExpressionVoter:+1,ModuleVoter:0 -> 1 = ACCESS_GRANTED - /foo-module:WebExpressionVoter:-1,ModuleVoter:-1 -> -2 = ACCESS_DENIED 给定一个允许查看Foo模块的用户应产生以下结果:
- /foo-module:WebExpressionVoter:+1,ModuleVoter:+1 -> 2 = ACCESS_GRANTED - /bar-module:WebExpressionVoter:+1(因为用户已经验证),ModuleVoter:-1 -> 0 = ACCESS_DENIED(由于ConsensusBased.setAllowIfEqualGrantedDeniedDecisions(false)

2
如果我理解正确,您正在尝试解决用户在匿名登录时返回ACCESS_DENIED的问题。这样做是不好的。请删除自定义代码,并在安全配置中进行以下更改:
anyRequest().hasRole("USER")

然后,确保用户登录时拥有所需的角色。

另一种选择是禁用匿名登录,这样您就不需要角色(从长远来看,这不是推荐的做法)。

http.anonymous().disable()

1
是的,我在回答中有些过于仓促了(已编辑)。但如果我理解正确的话,问题在于由于自定义身份验证投票器的原因,您甚至无法到达 permitAll() 被检查的部分,所以匿名用户不被允许访问应用程序的任何部分。通过确保非匿名登录至少拥有一个用户角色(通常是 ROLE_USER),更容易区分非匿名登录。 - Teemu Ilmonen
是的,如何解决这个问题? - ajm
但是投票者不应该在身份验证之前被调用吗? - ajm
好的,如果我删除它,即使对于未经身份验证的用户,检查模块权限的代码也会被调用。这就是问题所在。因此,我在投票者中添加了那个if条件。如果用户没有登录,则不应检查权限。 - ajm
如果这是唯一的问题,只需在调用SecurityUtils时将检查移到模块名称之前(而不是在操作参数不存在时进行检查),以允许其他情况的正常程序。话虽如此,我强烈建议您将特定于操作的安全检查移动到带有@Secured注释的服务类中,如原始帖子中所示。 - Teemu Ilmonen
显示剩余2条评论

2
问题出在您的投票器上。 如果相关的“ConfigAttribute”被配置了,那么您的投票器应该允许匿名访问。 以下是您应该添加的内容(这取决于您的Spring Security版本):
for (ConfigAttribute attribute : collection) {
    if ("IS_AUTHENTICATED_ANONYMOUSLY".equals(attribute.getAttribute())) {
        return ACCESS_GRANTED;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接