在任何一个面向用户的系统中,安全始终是无法绕开的话题。Spring Security 作为 Spring 生态中负责安全的子项目,已经发展为一套功能完备、高度可定制的安全框架。理解它的核心架构,关键在于把握两个根本概念——认证(Authentication) 和 授权(Authorization)——以及它们在框架内部是如何通过一条清晰的过滤器链串联起来的。
15.1.1 两大基石:认证与授权
认证回答的是“你是谁”的问题。系统需要确认当前访问者的身份,常见的方式包括用户名密码登录、手机验证码、OAuth2 社交登录、JWT 令牌等。认证成功后,系统会为当前会话或请求建立一个可信任的身份主体。
授权回答的是“你能做什么”的问题。即便用户已经登录,也不意味着可以访问所有资源。授权机制根据用户的角色、权限或自定义策略,判断是否允许执行某个操作或访问某个 URL。
在实际项目中,两者往往是分离的:认证模块负责建立身份,授权模块负责基于该身份进行访问控制。例如,一个后台管理系统可能先要求用户通过用户名密码登录(认证),然后根据其角色是“管理员”还是“普通编辑”决定是否允许删除文章(授权)。
15.1.2 安全上下文与 SecurityContextHolder
Spring Security 将认证成功后的用户信息存储在一个叫做 SecurityContext 的对象中,而该对象又被保存在 SecurityContextHolder 里。SecurityContextHolder 是整个安全框架的“全局变量”,应用程序的任何位置都可以通过它获取当前登录用户的信息:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName(); // 获取用户名
boolean isAdmin = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
SecurityContextHolder 默认使用 ThreadLocal 存储策略,确保同一个请求线程内各处获取的认证信息一致,处理完毕自动清除,避免内存泄漏。
15.1.3 认证流程的核心组件
当我们发起一个登录请求时,背后是一系列组件的协作。理解这些组件,是排查问题、自定义登录逻辑的基础。
1. Authentication(认证对象)
Authentication 是 Spring Security 中表示认证信息的核心接口。它在认证前和认证后承担不同角色:
- 认证前:通常是一个包含用户名和密码的实例(如
UsernamePasswordAuthenticationToken),此时isAuthenticated()返回false。 - 认证后:认证成功后,会返回一个全新的 Authentication 对象,其中包含用户的主体信息、权限集合,此时
isAuthenticated()返回true。
2. AuthenticationManager
AuthenticationManager 是认证的入口接口,只定义了一个方法:
Authentication authenticate(Authentication authentication)
它会接收未认证的 Authentication 对象,执行认证逻辑,并返回已认证的 Authentication 对象。如果认证失败,则抛出相应的异常(如 BadCredentialsException、LockedException 等)。
在实际配置中,AuthenticationManager 通常是一个 ProviderManager,它内部维护着多个 AuthenticationProvider,依次尝试进行认证。最常见的 Provider 是 DaoAuthenticationProvider,它从数据库或其他数据源加载用户信息,并与输入的凭证比对。
3. UserDetailsService
UserDetailsService 是 Spring Security 与业务用户数据之间的桥梁。它只有一个方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
开发者需要实现这个接口,根据用户名从自己的用户表、缓存或其他存储中加载用户信息,并封装为框架可理解的 UserDetails 对象。UserDetails 包含了用户名、密码(已加密)、账户是否过期、是否锁定、以及授予的权限集合。
4. PasswordEncoder
明文存储密码是安全的大忌。Spring Security 强制要求使用 PasswordEncoder 对密码进行加密和验证。常用实现包括 BCryptPasswordEncoder、SCryptPasswordEncoder 或者委托式的 DelegatingPasswordEncoder。配置一个全局的 PasswordEncoder Bean 后,DaoAuthenticationProvider 会在比对密码时自动调用 matches() 方法,开发者无需手写加密逻辑。
一个典型的认证流程可以这样描述:
- 用户提交用户名密码,封装为
UsernamePasswordAuthenticationToken。 AuthenticationManager调用DaoAuthenticationProvider处理该令牌。- Provider 调用自定义的
UserDetailsService加载用户信息。 - Provider 使用
PasswordEncoder比对提交的密码与数据库中的哈希值。 - 比对成功,返回一个全新的、已认证的
Authentication对象,并存入SecurityContextHolder。
15.1.4 授权流程:访问决策管理器
认证确立了身份,授权则决定了已认证的用户能否访问某个特定的资源。Spring Security 的授权核心是拦截器,具体表现为过滤器链中的 FilterSecurityInterceptor(或方法级安全拦截器)。它的决策过程依赖三个组件:
- ConfigAttribute:安全配置中定义的访问规则,例如
hasRole('ADMIN')或permitAll()。 - Authentication:当前请求的认证对象,包含用户的权限列表。
- AccessDecisionManager:访问决策管理器,根据 ConfigAttribute 和 Authentication 进行投票决策。
默认的实现是 AffirmativeBased,即只要有一个投票器投赞成票,就允许访问;还有一种 ConsensusBased 多数决方式,以及 UnanimousBased 一票否决方式。投票器中最常用的是 RoleVoter 和 WebExpressionVoter,前者检查角色前缀,后者支持 SpEL 表达式。
15.1.5 过滤器链:一切流量的入口
Spring Security 并非通过 Servlet 规范中的某一个过滤器实现安全,而是搭建了一条过滤器链。每个过滤器负责一项具体的安全任务,按顺序执行,共同构成了整个安全模型。核心过滤器包括(按顺序出现):
SecurityContextPersistenceFilter:请求到达时,从存储(如 HttpSession)中恢复 SecurityContext,请求结束时清理。UsernamePasswordAuthenticationFilter:处理表单登录请求,执行认证,成功则重定向到目标页面。BasicAuthenticationFilter:处理 HTTP Basic 方式认证,解析请求头中的 Authorization 信息。ExceptionTranslationFilter:捕获过滤器链中抛出的AuthenticationException和AccessDeniedException,分别引导到登录页或拒绝访问页面。FilterSecurityInterceptor:链的最后一个安全过滤器,根据 URL 配置的访问规则,进行最终授权检查。
每一个 HTTP 请求都必须完整通过这条过滤器链。开发者在调试安全相关问题时,往往需要断开这条链中的某个环节,确认异常究竟是从哪里抛出的。
15.1.6 方法级安全与注解驱动
除了 URL 级别的控制,Spring Security 还支持方法级安全,通过 @EnableGlobalMethodSecurity 开启。常用注解包括:
@PreAuthorize:方法执行前校验,支持 SpEL 表达式,如@PreAuthorize("hasRole('ADMIN') or #user.id == authentication.principal.id")。@PostAuthorize:方法执行后校验,可对返回结果进行过滤。@Secured:早期的角色注解,只支持简单角色字符串。@RolesAllowed:JSR-250 标准注解,同样用于角色控制。
方法级安全是通过 AOP 代理实现的,在方法调用前织入授权逻辑。如果在方法内部再次调用同一类的其他安全方法,代理不会生效,这是 Spring AOP 的固有限制,必要时需要重构方法调用或引入自注入。
15.1.7 实践中的架构最佳理解
在实际开发中,理解核心架构可以帮助我们快速定位问题、定制功能:
- 认证失败:去检查
UserDetailsService是否返回了正确的用户,PasswordEncoder是否匹配,还是密码存储方式不一致。 - 授权失败(403):检查
ConfigAttribute配置是否正确,用户拥有的权限集合里是否真的包含所需角色。 - 匿名用户访问了需要认证的资源:检查过滤器链是否把请求带到了
FilterSecurityInterceptor,是否未抛出认证异常就被判定权限不足。 - 想扩展新的登录方式(如手机验证码):自定义
AuthenticationProvider,注册到AuthenticationManager中即可,不必改动过滤器链。
Spring Security 的架构虽然庞大,但核心只有一条:通过过滤器链识别用户(认证),基于配置好的规则判断用户是否能访问资源(授权)。抓牢这根主线,再复杂的应用安全需求都能被有条不紊地消化掉。接下来我们将通过实际配置,看看如何将这套架构落地为可运行的代码。