You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot3+Spring Security6中hasRole("ROLE_USER")失效求助

Spring Boot 3 + Spring Security 6 自定义LoginFilter认证后无法访问Dashboard问题排查

问题描述

我在Spring Boot中创建了LoginFilter,意图在过滤器中创建用户认证对象后重定向到dashboard页面,但遇到以下问题:

  • 直接访问/dashboard时,页面跳转到登录页,尽管已在LoginFilter中创建带ROLE_USER权限的UsernamePasswordAuthenticationToken并放入SecurityContext
  • 若将/dashboard的权限规则改为permitAll(),能访问页面但Principal对象为null

使用版本:Spring Boot 3、Spring Security 6


相关代码文件

LoginFilter.java

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.List;

@Component
public class LoginFilter extends GenericFilterBean {
    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException,ServletException
    {
           Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
           SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");
           List<SimpleGrantedAuthority> updatedAuthorities = new ArrayList<SimpleGrantedAuthority>();
           updatedAuthorities.add(authority);

           authentication =  new UsernamePasswordAuthenticationToken("myemail.gamil.com", null, updatedAuthorities);
           SecurityContextHolder.getContext().setAuthentication(authentication);
            
           this.redirectStrategy.sendRedirect((HttpServletRequest) request, (HttpServletResponse) response,"/dashboard");
    }
}

WebSecurityConfigDashboard.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class WebSecurityConfigDashboard {

    @Autowired
    private LoginFilter loginFilter;

    @Bean
    public UserDetailsService userDetailsService1(PasswordEncoder encoder) {
        UserDetails admin = User.withUsername("user1")
                .password(encoder.encode("123"))
                .roles("ROLE_USER")
                .build();
        UserDetails user = User.withUsername("user2")
                .password(encoder.encode("123"))
                .roles("ROLE_USER")
                .build();
        return new InMemoryUserDetailsManager(admin, user);
    }

    @Bean
    public SecurityFilterChain securityFilterChain1(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeHttpRequests()
                .requestMatchers("/dashboard*").hasRole("ROLE_USER")
                .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .failureUrl("/loginError")
                .and().logout()
                .invalidateHttpSession(true)
                .logoutSuccessUrl("/logout")
                .logoutUrl("/j_spring_security_logout").and()
                .sessionManagement()
                .maximumSessions(12)
                .expiredUrl("/logout");

        http.addFilterAfter(loginFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer1() {
        return (web) -> web.ignoring().requestMatchers("/resources/images/**");
    }

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

LoginController.java

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.security.Principal;

@Controller
public class LoginController {

    @RequestMapping(value = "/dashboard")
    public String dashboard(Model model,Principal principal,HttpServletRequest request, HttpServletResponse response)
    {
        System.out.println("/dashboard called:------------------------>");
        System.out.println("PRINCIPAL:--------------->"+principal.getName());
        return "dashboard";
    }

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login(ModelMap model) {
        System.out.println("/login called:------------------------>");
        return "login";
    }

    @RequestMapping(value = "/logout", method = RequestMethod.GET)
    public String logout(ModelMap model) {
        System.out.println("/logout called:------------------------>");
        return "logout";
    }

    @RequestMapping(value = "/loginError", method = RequestMethod.GET)
    public String loginError(ModelMap model) {
        model.addAttribute("error", "LOGIN FAILED");
        return "login";
    }
    @RequestMapping(value = "/error", method = RequestMethod.GET)
    public String error(ModelMap model,HttpServletRequest request, HttpServletResponse response) {
        return "error";
    }
}

问题原因分析

  1. 过滤器执行顺序错误:你把LoginFilter添加在了UsernamePasswordAuthenticationFilter之后,但Spring Security的权限校验过滤器(FilterSecurityInterceptor)在UsernamePasswordAuthenticationFilter之后执行。直接访问/dashboard时,权限校验先触发,此时LoginFilter还未执行,无认证信息,因此跳转到登录页。
  2. 认证对象未持久化到Session:SecurityContext默认绑定当前线程,重定向是新请求,线程不同,之前的认证信息丢失,因此permitAll()时Principal为null。
  3. hasRole权限前缀不匹配:hasRole()会自动添加ROLE_前缀,配置.hasRole("ROLE_USER")实际校验的是ROLE_ROLE_USER权限,和你创建的ROLE_USER不匹配。

解决方案

1. 调整过滤器执行顺序

将LoginFilter放在权限校验过滤器之前,修改配置为:

http.addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class);

2. 持久化SecurityContext到Session

在LoginFilter中设置认证对象后,将其存入HttpSession,确保重定向后能获取到:

// 在LoginFilter的doFilter方法中添加
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpSession session = httpRequest.getSession();
session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());

3. 修正hasRole权限配置

hasRole()自动加前缀,因此只需写角色名即可,或者用hasAuthority()(不自动加前缀):

// 方式1:用hasRole,自动补全ROLE_前缀
.requestMatchers("/dashboard*").hasRole("USER")
// 方式2:用hasAuthority,和你创建的权限完全匹配
.requestMatchers("/dashboard*").hasAuthority("ROLE_USER")

4. 优化LoginFilter触发逻辑(可选)

当前过滤器会对所有请求执行认证和重定向,需添加触发条件,比如仅在自定义登录接口触发:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException,ServletException {
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    // 仅当访问自定义登录接口时执行认证
    if ("/login/custom".equals(httpRequest.getRequestURI())) {
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                "myemail.gamil.com", 
                null, 
                List.of(new SimpleGrantedAuthority("ROLE_USER"))
        );
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // 存入Session
        HttpSession session = httpRequest.getSession();
        session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
        this.redirectStrategy.sendRedirect(httpRequest, (HttpServletResponse) response,"/dashboard");
    } else {
        // 其他请求继续走过滤器链
        filterChain.doFilter(request, response);
    }
}

内容的提问来源于stack exchange,提问作者Kunal Bhangale

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 08:38:49