【Spring Boot + Spring Security】从入门到源码精通:藏经阁权限设计与过滤器链深度解析

版本说明 :本教程基于 Spring Boot 3.xSpring Security 6.x 版本,采用了新的 Lambda DSL 配置风格。如果你使用的是旧版本,配置方式会略有不同。


第一回:初来乍到,藏经阁危在旦夕

我叫小白,是一名刚飞升上来的"代码修仙者"。我的第一份差事,就是担任"星宿宗"的藏经阁管理员。

藏经阁,那可是宗门的核心重地:

  • 一楼: 公共休息区,谁都能进 (/, /home)。

  • 二楼: 普通秘籍区,只有本门弟子才能翻阅 (/books/**)。

  • 三楼: 绝学禁区,只有长老才能进入 (/secret/**)。

我刚上任第一天,老阁主就拍拍我的肩膀,语重心长地说:"小白啊,现在的藏经阁,谁都能上三楼,跟逛菜市场似的。我们的《如来神掌》和《九阳神功》秘籍危矣!你的任务,就是给它建立起一套'护阁大阵'!"

我一脸懵:"阁主,阵法一道,晚辈才疏学浅啊..."

老阁主神秘一笑,掏出一本古籍:"此乃 Spring Boot 心法,能让你快速开宗立派。再配合这本 Spring Security 阵法大全,可布下天罗地网!"


第二回:开宗立派,初布大阵 (项目初始化)

推理时刻: 要布阵,先得有地盘。用 Spring Boot 创建项目是最快的方式。

我按照古籍记载,在 pom.xml 这个"灵气汇聚阵"中,引入了两大核心依赖:

XML 复制代码
<!-- Spring Boot 核心心法,提供内力 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security 阵法核心,提供规则 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

然后,我创建了几个简单的"房间"(Controller):

java 复制代码
@RestController
public class LibraryController {

    @GetMapping("/")
    public String home() {
        return "欢迎来到藏经阁公共休息区!";
    }

    @GetMapping("/books/java")
    public String getJavaBook() {
        return "《Java 编程思想》";
    }

    @GetMapping("/secret/kungfu")
    public String getSecretKungfu() {
        return "《如来神掌》秘籍!";
    }
}

我信心满满地启动了应用 (SpringBootApplication.run)

诡异的事情发生了! 我访问首页 http://localhost:8080,没有看到欢迎语,反而跳转到了一个陌生的登录页面!用户名是 user,密码则在控制台的一长串乱码里。

严谨推理:

  1. 一旦引入 spring-boot-starter-security,Spring Boot 的 自动配置 机制就启动了。

  2. 它会为应用 自动套上一个默认的安全结界

  3. 这个默认结界规定:所有请求都需要认证

  4. 它还会自动生成一个随机密码的用户,并提供一个基础的登录页。

关于默认密码的详细说明:

当你第一次启动 Spring Security 应用时,会在控制台看到类似这样的信息:

Using generated security password: 9a2b8f7c-3d6e-4a5b-8c9d-0e1f2a3b4c5d

This generated password is for development use only. Your security configuration must be updated before running in production.

这个随机密码的生成逻辑:

  • Spring Boot 检测到项目中存在 Spring Security 但没有显式配置 UserDetailsServiceAuthenticationManager

  • 会自动创建一个 InMemoryUserDetailsManager 实例

  • 生成一个用户名为 user 的账户

  • 密码是通过 UUID 随机生成器 创建的,确保每次启动应用时都不同

  • 这是一种安全措施,强制开发者在生产环境中配置真实的用户管理

示例控制台输出:

org.springframework.security.web.context.SecurityContextHolderFilter@34567890, ...]

2023-10-01T10:30:45.124+08:00 INFO 12345 --- [ main] .s.s.UserDetailsServiceAutoConfiguration :

Using generated security password: 9a2b8f7c-3d6e-4a5b-8c9d-0e1f2a3b4c5d

This generated password is for development use only. Your security configuration must be updated before running in production.

所以,我还没开始布阵,Spring Security 已经用它的"默认阵法"把我的藏经阁保护起来了------虽然保护得有点蠢,连公共区都进不去。


第三回:自定义大阵,权限分明 (核心配置)

老阁主看了直摇头:"你这阵法敌我不分啊!看我的。"

他带我创建了一个名为 SecurityConfig 的"阵法核心枢纽"。

java 复制代码
@Configuration
@EnableWebSecurity // 宣告:此乃本宗自定义安全大阵!
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // HttpSecurity 就是我们的"阵法编织器"
        http.authorizeHttpRequests(authz -> authz
                .requestMatchers("/").permitAll()           // 公共区,无需认证
                .requestMatchers("/books/**").hasRole("USER") // 普通秘籍区,需"弟子"身份
                .requestMatchers("/secret/**").hasRole("ADMIN") // 禁区,需"长老"身份
                .anyRequest().authenticated()               // 其他所有请求,都需要登录
            )
            .formLogin(withDefaults()); // 使用默认的登录页面

        return http.build();
    }

    // 创建"身份令牌"发放处 - 基于真实数据库查询
    @Bean
    public UserDetailsService userDetailsService(UserRepository userRepository) {
        return username -> {
            // 从数据库中根据用户名查询用户信息
            UserEntity userEntity = userRepository.findByUsername(username);
            
            if (userEntity == null) {
                throw new UsernameNotFoundException("用户不存在: " + username);
            }
            
            // 查询用户的角色权限列表
            List<SimpleGrantedAuthority> authorities = userRepository.findRolesByUserId(userEntity.getId())
                    .stream()
                    .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                    .collect(Collectors.toList());
            
            // 构建Spring Security需要的UserDetails对象
            return org.springframework.security.core.userdetails.User.builder()
                    .username(userEntity.getUsername())
                    .password(userEntity.getPassword()) // 数据库中存储的应该是加密后的密码
                    .authorities(authorities)
                    .accountExpired(!userEntity.isAccountNonExpired())
                    .accountLocked(!userEntity.isAccountNonLocked())
                    .credentialsExpired(!userEntity.isCredentialsNonExpired())
                    .disabled(!userEntity.isEnabled())
                    .build();
        };
    }

    // 密码编码器 - 用于密码加密和验证
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 使用BCrypt强哈希函数进行密码加密
        return new BCryptPasswordEncoder();
    }
}

对应的数据库实体类和Repository:

java 复制代码
// 用户实体类
@Entity
@Table(name = "users")
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false)
    private String username;
    
    @Column(nullable = false)
    private String password;
    
    private boolean accountNonExpired = true;
    private boolean accountNonLocked = true;
    private boolean credentialsNonExpired = true;
    private boolean enabled = true;
    
    // getters and setters
}

// 角色实体类
@Entity
@Table(name = "roles")
public class RoleEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false)
    private String name;
    
    // getters and setters
}

// 用户角色关联实体类
@Entity
@Table(name = "user_roles")
public class UserRoleEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "user_id")
    private UserEntity user;
    
    @ManyToOne
    @JoinColumn(name = "role_id")
    private RoleEntity role;
    
    // getters and setters
}

// 用户Repository
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
    
    // 根据用户名查找用户
    UserEntity findByUsername(String username);
    
    // 根据用户ID查找角色列表
    @Query("SELECT r.name FROM RoleEntity r " +
           "JOIN UserRoleEntity ur ON ur.role.id = r.id " +
           "WHERE ur.user.id = :userId")
    List<String> findRolesByUserId(Long userId);
}

数据库表结构示例:

sql 复制代码
-- 用户表
CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(100) NOT NULL,  -- 存储BCrypt加密后的密码
    account_non_expired BOOLEAN DEFAULT TRUE,
    account_non_locked BOOLEAN DEFAULT TRUE,
    credentials_non_expired BOOLEAN DEFAULT TRUE,
    enabled BOOLEAN DEFAULT TRUE
);

-- 角色表
CREATE TABLE roles (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) UNIQUE NOT NULL
);

-- 用户角色关联表
CREATE TABLE user_roles (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    role_id BIGINT NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (role_id) REFERENCES roles(id)
);

-- 插入测试数据(密码都是"password",但经过BCrypt加密)
INSERT INTO users (username, password) VALUES 
('disciple', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVwUiW'),
('elder', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVwUiW');

INSERT INTO roles (name) VALUES ('USER'), ('ADMIN');

INSERT INTO user_roles (user_id, role_id) VALUES 
(1, 1), -- disciple 有 USER 角色
(2, 1), -- elder 有 USER 角色
(2, 2); -- elder 有 ADMIN 角色

故事解读与严谨推理:

  1. @EnableWebSecurity: 这是启动我们自定义安全规则的"咒语",它告诉 Spring Boot:"别用你的默认阵法了,用我的!"

  2. HttpSecurity: 这是整个故事的核心,我们的"阵法编织器"。通过配置它,我们可以精确控制访问规则。

  3. authorizeHttpRequests : 这是 权限规则定义,是我们大阵的"识别逻辑"。

    • .requestMatchers("/").permitAll(): 匹配根路径,permitAll() 表示完全放行。推理: 这里不进行任何安全拦截。

    • .requestMatchers("/books/**").hasRole("USER"): 匹配 /books/ 开头的所有请求,hasRole("USER") 表示请求者必须拥有 ROLE_USER 角色。推理: 系统会检查当前登录用户是否具备该角色。

    • .anyRequest().authenticated(): 这是一个兜底策略,对于其他所有请求,只需要登录(认证)即可,不管是什么角色。

  4. UserDetailsService : 这是 用户详情服务 ,是我们的"身份令牌发放处"。现在我们改为从真实数据库中查询用户信息和角色。推理: 当用户登录时,Spring Security 会调用这个服务,根据用户名从数据库查找用户的密码和角色信息,用于验证身份和授权。

  5. PasswordEncoder : 这是 密码编码器,使用 BCrypt 强哈希算法对密码进行加密和验证,确保密码安全。

现在,让我们测试一下大阵效果:

  • 访问 /: 直接进入!(符合 permitAll)

  • 访问 /books/java: 跳转到登录页。

    • disciple/password 登录:成功看到《Java 编程思想》!

    • elder/password 登录:也能看到!(因为长老也有 USER 角色)

  • 访问 /secret/kungfu: 跳转到登录页。

    • disciple/password 登录:结果:403 Forbidden 错误!禁止访问! 推理: 弟子只有 USER 角色,没有 ADMIN 角色,大阵识别出他权限不足。

    • elder/password 登录:成功看到《如来神掌》!

完美!我们的护阁大阵开始起作用了!


第四回:识破阵法玄机------内置拦截器(过滤器链)

老阁主看我悟性不错,便带我走到大阵的幕后。只见一道道灵光(HTTP请求)进入藏经阁,需要经过一个长长的"过滤走廊",走廊里有各式各样的"拦截器弟子"在执勤。

"看,这就是 Spring Security 的 过滤器链 (Filter Chain),"老阁主说,"每个过滤器都是一个大阵的组成部分。"

几个你必须认识的"核心执勤弟子":

  1. SecurityContextPersistenceFilter (身份凭证保管员)

    • 职责: 当一个请求来时,他从 Session 中取出用户的登录凭证(Authentication)。请求结束时,他再把凭证存回去。这样用户在一个会话中只需要登录一次。
  2. UsernamePasswordAuthenticationFilter (账房先生)

    • 职责: 专管表单登录。当你在登录页提交用户名和密码时,就是他来处理的。他负责验证你的身份,并给你发放"身份令牌"。
  3. FilterSecurityInterceptor (权限判官)

    • 职责: 这是 最重要 的拦截器之一!我们之前在 HttpSecurity 里配置的所有访问规则 (hasRole, permitAll 等),最终都是由他来执行的。他会在请求到达 Controller 之前,根据规则决定是"放行"还是"抛出异常(403)"。
  4. ExceptionTranslationFilter (异常处理外交官)

    • 职责: 他专门处理 FilterSecurityInterceptor 抛出的异常。

    • 推理流程:

      • 如果 FilterSecurityInterceptor 说:"此人未认证!",外交官就会引导用户去登录页(发起认证)。

      • 如果 FilterSecurityInterceptor 说:"此人权限不足!",外交官就会返回 403 Forbidden 错误。

推理链条总结:

一个请求 GET /secret/kungfu 的冒险之旅:

  1. SecurityContextPersistenceFilter 从 Session 中取出令牌,发现是"弟子"。

  2. 请求一路向前,没有触发登录,所以绕过了 UsernamePasswordAuthenticationFilter

  3. 到达 FilterSecurityInterceptor!判官拿出规则手册一查:"/secret/** 需要 ADMIN 角色"。再一看令牌:"弟子,角色 USER"。权限不足!抛出异常!

  4. ExceptionTranslationFilter 接到"权限不足"异常,直接返回 403 状态码。


第五回:阵法玄机------核心拦截器与源码详解

老阁主将我带到藏经阁的"过滤走廊"幕后,指着那一排排正在执勤的"拦截器弟子",说道:"小白,知其然,更要知其所以然。今日,我便传你这护阁大阵的核心运转法则!"

第一式:SecurityContextPersistenceFilter (身份凭证保管员)

职责 :他是整个过滤链的第一个和最后一个执勤弟子,负责在请求开始时从Session中取出用户凭证,并在请求结束时清理现场,防止信息泄露。

java 复制代码
/**
 * SecurityContextPersistenceFilter - 身份凭证保管员
 * 
 * 核心源码逻辑分析:
 * 1. 在请求开始时,从Session中加载SecurityContext(安全上下文)
 * 2. 将SecurityContext设置到SecurityContextHolder中,供后续过滤器使用
 * 3. 在请求结束时,清理SecurityContextHolder,防止线程复用导致的信息泄露
 */
public class SecurityContextPersistenceFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        // 1. 在请求开始时,尝试从Session中获取SecurityContext(安全上下文)
        // SecurityContext 包含当前用户的认证信息 Authentication
        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
        SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
        
        try {
            // 2. 将获取到的SecurityContext设置到SecurityContextHolder中
            // SecurityContextHolder相当于一个全局的、线程安全的储物柜,后续过滤器都从这里拿用户信息
            // 关键点:使用ThreadLocal实现,确保每个请求线程都有自己的安全上下文
            SecurityContextHolder.setContext(contextBeforeChainExecution);
            
            // 3. 放行,让请求继续走后续的过滤器链
            // 这里会调用下一个过滤器,最终会调用到FilterSecurityInterceptor进行权限判断
            chain.doFilter(holder.getRequest(), holder.getResponse());
            
        } finally {
            // 4. 请求结束后,无论如何,清理SecurityContextHolder
            // 这是非常重要的安全措施!防止线程池复用时,用户信息泄露到其他请求
            SecurityContextHolder.clearContext();
            
            // 同时也会将更新后的SecurityContext保存回Session(如果认证状态有变化)
            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
            repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
        }
    }
}

第二式:UsernamePasswordAuthenticationFilter (账房先生)

职责 :专管表单登录,默认拦截 /login 的POST请求。他负责接收用户提交的用户名密码,并尝试进行认证。

java 复制代码
/**
 * UsernamePasswordAuthenticationFilter - 账房先生
 * 
 * 核心源码逻辑分析:
 * 1. 只处理/login路径的POST请求
 * 2. 从请求参数中提取用户名和密码
 * 3. 创建未认证的Authentication令牌
 * 4. 委托给AuthenticationManager进行实际认证
 */
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
    // 默认只处理 /login 的 POST 请求
    // 这就是为什么我们提交登录表单时必须是POST到/login
    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        
        // 1. 从请求中获取用户名和密码
        // 默认从username和password参数获取,但可以重写这些方法
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        
        if (username == null) {
            username = "";
        }
        if (password == null) {
            password = "";
        }
        
        username = username.trim();

        // 2. 使用获取到的信息,创建一个「未认证」的令牌 (Authentication)
        // 此时的Authentication的authenticated属性为false
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        
        // 3. 设置一些额外的信息,如远程IP地址、Session ID等
        // 这些信息在后续的审计日志等场景中很有用
        setDetails(request, authRequest);
        
        // 4. 将这个令牌交给「认证经理」(AuthenticationManager) 进行核实
        // AuthenticationManager会找到合适的AuthenticationProvider来执行认证
        // 如果认证成功,返回一个充满详细信息的Authentication对象(authenticated=true)
        // 如果失败,则抛出AuthenticationException异常
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    
    // 从请求中获取用户名的默认实现
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter("username");
    }
    
    // 从请求中获取密码的默认实现  
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter("password");
    }
}

第三式:ExceptionTranslationFilter (异常处理外交官)

职责 :他站在 FilterSecurityInterceptor 的身后,专门处理在安全过滤链中抛出的两类异常:AuthenticationException(认证异常)和 AccessDeniedException(访问拒绝异常)。

java 复制代码
/**
 * ExceptionTranslationFilter - 异常处理外交官
 * 
 * 核心源码逻辑分析:
 * 1. 捕获后续过滤器抛出的异常
 * 2. 如果是AuthenticationException,启动认证流程
 * 3. 如果是AccessDeniedException,检查用户是否已认证
 * 4. 根据情况返回登录页面或403错误
 */
public class ExceptionTranslationFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        try {
            // 放行,让请求继续往下走(主要是走向最终的权限判官 FilterSecurityInterceptor)
            chain.doFilter(request, response);
            
        } catch (Exception e) {
            // 捕获异常并进行判断
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(e);
            
            // 1. 如果是「认证异常」(用户未登录或登录失败)
            RuntimeException ase = (AuthenticationException) this.throwableAnalyzer
                    .getFirstThrowableOfType(AuthenticationException.class, causeChain);
            if (ase != null) {
                // 启动认证流程 - 比如跳转到登录页
                handleAuthenticationException(request, response, chain, (AuthenticationException) ase);
                return;
            }
            
            // 2. 如果是「访问拒绝异常」(权限不足)
            ase = (AccessDeniedException) this.throwableAnalyzer
                    .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
            if (ase != null) {
                // 处理权限不足的情况
                handleAccessDeniedException(request, response, chain, (AccessDeniedException) ase);
                return;
            }
        }
    }
    
    private void handleAuthenticationException(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, AuthenticationException failed)
            throws IOException, ServletException {
        
        // 将AuthenticationException信息保存到SecurityContextHolder中
        SecurityContextHolder.getContext().setAuthentication(null);
        
        // 重要:触发认证入口点,通常是跳转到登录页面
        // 在表单登录中,这会重定向到登录页
        this.authenticationEntryPoint.commence(request, response, failed);
    }
    
    private void handleAccessDeniedException(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, AccessDeniedException denied)
            throws IOException, ServletException {
        
        // 获取当前认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        
        // 关键判断:如果用户是匿名用户(未登录)或者RememberMe用户
        if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
            // 用户未登录,触发认证流程
            this.authenticationEntryPoint.commence(request, response,
                    new InsufficientAuthenticationException("Full authentication is required to access this resource"));
        } else {
            // 用户已登录但权限不足 - 返回403 Forbidden错误
            this.accessDeniedHandler.handle(request, response, denied);
        }
    }
}

第四式:FilterSecurityInterceptor (权限判官)

职责 :这是整个安全链的最后一关,负责根据配置的权限规则做出最终的访问决策。

java 复制代码
/**
 * FilterSecurityInterceptor - 权限判官
 * 
 * 核心源码逻辑分析:
 * 1. 在请求到达Controller前进行拦截
 * 2. 从SecurityContextHolder获取当前用户认证信息
 * 3. 调用AccessDecisionManager进行权限决策
 * 4. 根据决策结果决定是否放行
 */
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        // 创建FilterInvocation对象,封装请求信息
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        
        // 核心:进行权限校验
        InterceptorStatusToken token = super.beforeInvocation(fi);
        
        try {
            // 如果权限校验通过,执行后续的过滤器链,最终到达Controller
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            // 请求完成后的清理工作
            super.finallyInvocation(token);
        }
        
        // 调用完成后的后置处理
        super.afterInvocation(token, null);
    }
    
    // 在AbstractSecurityInterceptor中定义的核心方法
    protected InterceptorStatusToken beforeInvocation(Object object) {
        
        // 1. 获取当前请求对应的配置属性(就是我们配置的hasRole、permitAll等规则)
        Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
                .getAttributes(object);
        
        if (attributes == null || attributes.isEmpty()) {
            // 如果没有配置安全规则,直接放行
            return null;
        }
        
        // 2. 从SecurityContextHolder中获取当前认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        
        // 3. 核心:委托给AccessDecisionManager进行权限决策
        // AccessDecisionManager会调用一系列的AccessDecisionVoter进行投票
        try {
            this.accessDecisionManager.decide(authentication, object, attributes);
        } catch (AccessDeniedException accessDeniedException) {
            // 如果决策结果是拒绝访问,抛出AccessDeniedException
            // 这个异常会被前面的ExceptionTranslationFilter捕获
            throw accessDeniedException;
        }
        
        // 4. 如果权限校验通过,创建并返回InterceptorStatusToken
        return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
    }
}

完整请求流程的源码级推演:

java 复制代码
/**
 * 一个请求的完整生命周期 - 源码级推演
 * 请求:GET /secret/kungfu (由已认证但无权限的"弟子"用户发起)
 */
public void demonstrateRequestFlow() {
    // 1. SecurityContextPersistenceFilter从Session中恢复SecurityContext
    //    SecurityContext包含弟子用户的Authentication对象(authenticated=true, authorities=[ROLE_USER])
    
    // 2. 请求经过一系列过滤器,到达FilterSecurityInterceptor
    
    // 3. FilterSecurityInterceptor.beforeInvocation()被调用:
    //    - 获取配置属性: [hasRole('ADMIN')]
    //    - 获取当前认证: 弟子用户(只有ROLE_USER角色)
    //    - 调用AccessDecisionManager.decide()
    
    // 4. AccessDecisionManager进行投票决策:
    //    - RoleVoter检查:用户有[ROLE_USER],需要[ROLE_ADMIN]
    //    - 投票结果:ACCESS_DENIED
    
    // 5. AccessDecisionManager抛出AccessDeniedException
    
    // 6. ExceptionTranslationFilter捕获AccessDeniedException:
    //    - 检查用户已认证 → 调用AccessDeniedHandler
    //    - 返回403 Forbidden响应
    
    // 7. 请求结束,SecurityContextPersistenceFilter清理SecurityContextHolder
}

终回:大道至简,万法归宗

通过这场"藏经阁守护战"和深入的源码分析,我们明白了:

  1. 集成如此简单: 只需一个依赖,Spring Boot 就为你带来了 Spring Security 的强大能力。

  2. 默认密码机制: Spring Security 会自动生成随机密码并在控制台显示,这是开发阶段的保护措施。

  3. 配置核心是 HttpSecurity 就像编织阵法,你用它来精确指定 哪些路径需要什么权限

  4. 用户与角色: 通过 UserDetailsService 可以从数据库查询真实的用户和权限信息。

  5. 密码安全: 使用 BCryptPasswordEncoder 对密码进行强加密存储。

  6. 理解过滤器链: 明白请求背后四大核心过滤器的工作流程和源码实现,是解决复杂权限问题的钥匙。

源码层面的核心收获:

  • SecurityContextPersistenceFilter 通过 ThreadLocal 实现请求级别的安全上下文隔离

  • UsernamePasswordAuthenticationFilter 是认证的入口,负责创建初始的 Authentication 对象

  • FilterSecurityInterceptor 是权限决策的最终执行者,调用 AccessDecisionManager 进行投票

  • ExceptionTranslationFilter 是异常处理的统一出口,将技术异常转换为用户友好的响应

后续修炼方向(你的下一篇博客主题):

  • 自定义登录页面: 替换默认的登录页,设计符合宗门风格的登录界面。

  • 记住我功能: 实现"记住我"功能,让弟子们一段时间内无需重复登录。

  • 登录成功处理: 自定义登录成功后的跳转逻辑,根据用户角色跳转到不同页面。

  • 退出登录: 实现安全的退出登录功能,清理用户会话。

  • 探索 JWT: 为你的前后端分离架构,打造无状态的令牌安全机制。

相关推荐
摇滚侠4 小时前
Spring Boot3零基础教程,JVM 编译原理,笔记87
jvm·spring boot·笔记
NEU-UUN4 小时前
C语言 . 第二章第二节 . 分支结构
c语言·开发语言
千里镜宵烛4 小时前
Lua-function的常见表现形式
开发语言·junit·lua
阿巴~阿巴~4 小时前
线程局部存储(Thread-Local Storage, TLS)
linux·服务器·开发语言·c++·线程·虚拟地址空间·线程局部存储
初见无风4 小时前
2.4 Lua代码中table常用API
开发语言·lua·lua5.4
初见无风4 小时前
2.6 Lua代码中function的常见用法
开发语言·lua·lua5.4
BAGAE4 小时前
MQTT 与 HTTP 协议对比
java·linux·http·https·硬件工程
oak隔壁找我4 小时前
Spring框架中的跨域CORS配置详解
java·后端
摇滚侠4 小时前
Spring Boot3零基础教程,配置 GraalVM 环境,笔记88
java·spring boot·笔记