Spring Security入门实战:从零构建Java Web安全权限控制

📅 2026/8/11 15:12:18
Spring Security入门实战:从零构建Java Web安全权限控制
在 Java Web 开发中权限控制是一个绕不开的核心话题。很多开发者初次接触时往往只关注如何实现一个简单的登录拦截认为在过滤器里检查 Session 就算完成了。然而当项目规模扩大需要区分不同角色、不同资源的访问权限并妥善处理认证失败、授权失败、会话超时、CSRF 攻击等一系列安全问题时一个简单的手写过滤器很快就会变得臃肿且难以维护。这时一个成熟、强大且可扩展的权限框架就显得至关重要。Spring Security 正是为此而生它提供了一套完整的企业级安全解决方案但其学习曲线也相对陡峭复杂的配置和抽象概念常常让初学者望而却步。本文旨在为有一定 Spring Boot 基础的开发者提供一个清晰、可复现的 Spring Security 入门实战指南。我们将从零开始搭建一个 Spring Boot 项目集成 Spring Security并逐步实现一个包含用户登录、角色权限控制、自定义登录页、记住我等核心功能的 Web 应用。文章将重点解释 Spring Security 的核心工作机制如过滤器链、认证管理器、投票器等并详细说明每一步配置的目的和常见陷阱。通过本文你将能够理解 Spring Security 的基本架构并具备将其应用于实际项目的能力。1. 理解 Spring Security 的核心过滤器链与安全上下文在开始写代码之前必须先理解 Spring Security 是如何工作的。它本质上是一个基于 Servlet 过滤器的安全框架。当 HTTP 请求到达你的应用时它首先会经过 Spring Security 构建的一条过滤器链。1.1 过滤器链的职责这条过滤器链由多个Filter组成每个Filter负责一项特定的安全任务。例如UsernamePasswordAuthenticationFilter: 处理表单登录从请求中提取用户名和密码。BasicAuthenticationFilter: 处理 HTTP Basic 认证。RememberMeAuthenticationFilter: 处理“记住我”功能。AnonymousAuthenticationFilter: 为未登录的请求分配一个匿名身份。ExceptionTranslationFilter: 处理认证和授权过程中抛出的异常并将其转换为合适的 HTTP 响应如重定向到登录页或返回 403。FilterSecurityInterceptor: 这是授权决策的最终守卫根据配置的访问规则如hasRole(ADMIN)决定是否允许访问资源。这些过滤器协同工作共同完成了从认证到授权的全过程。理解这一点至关重要因为后续的很多配置本质上都是在定制这条过滤器链。1.2 SecurityContext 与 Authentication认证成功后用户的身份信息如用户名、权限列表会被封装在一个Authentication对象中。这个对象会被存储在线程绑定的SecurityContext中。这意味着在同一个请求线程的任何地方如 Controller、Service 层你都可以通过SecurityContextHolder.getContext().getAuthentication()获取到当前登录用户的信息。这种设计使得业务代码无需关心用户信息是如何传递的。2. 环境准备与项目初始化我们将使用 Spring Boot 来简化 Spring Security 的集成和配置。这是目前最主流和高效的方式。2.1 技术栈与版本JDK: 8 或 11推荐 11构建工具: Maven 3.6 或 Gradle 6.xSpring Boot: 2.7.x本文基于 2.7.18与 Spring Security 5.7.x 兼容IDE: IntelliJ IDEA 或 VS Code注意Spring Boot 3.x 与 Spring Security 6.x 有较大变化本文为降低入门门槛选择更稳定、资料更丰富的 2.7.x 版本。生产环境请根据实际情况选择版本。2.2 创建 Spring Boot 项目使用 Spring Initializr 创建项目是最快的方式。访问start.spring.io 。选择项目参数Project: Maven ProjectLanguage: JavaSpring Boot: 2.7.18Group:com.exampleArtifact:security-demoPackaging: JarJava: 11添加依赖在Dependencies中搜索并添加Spring Web(用于构建 Web 应用)Spring Security(核心安全框架)Thymeleaf(可选用于渲染 HTML 模板本文示例会用到)Spring Boot DevTools(可选用于热部署)点击Generate下载项目压缩包解压后用 IDE 打开。你的pom.xml关键依赖部分应该类似这样dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-devtools/artifactId scoperuntime/scope optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency dependency groupIdorg.springframework.security/groupId artifactIdspring-security-test/artifactId scopetest/scope /dependency /dependencies3. 初体验Spring Security 的默认行为项目创建完成后无需任何配置直接启动主类SecurityDemoApplication。Spring Security 已经自动生效。3.1 访问应用并观察启动应用控制台会打印类似Tomcat started on port(s): 8080的信息。打开浏览器访问http://localhost:8080。你会被自动重定向到一个登录页面 (http://localhost:8080/login)。这个页面是 Spring Security 默认提供的。控制台日志中会有一行类似下面的信息Using generated security password: 78fa095d-3f4c-48b1-ad50-e24c31d5cf35这个就是默认用户的密码。用户名为user。使用user和打印的密码登录成功后你会看到一个Whitelabel Error Page因为还没有定义首页。但这证明你已经成功登录。3.2 默认配置分析这个简单的体验揭示了 Spring Security 的默认行为自动保护所有端点 (/**) 默认都需要认证。自动生成用户提供了一个内存中的用户user密码随机生成。默认登录页提供了一个基础的/login页面。默认登出提供了/logout端点。CSRF 保护默认启用防止跨站请求伪造攻击。对于快速原型或内部工具这可能足够了。但对于实际项目我们几乎总是需要自定义用户来源、登录逻辑和访问规则。4. 核心配置自定义用户、密码与访问规则接下来我们将通过编写一个配置类来覆盖默认行为。这是 Spring Security 配置的核心。4.1 创建安全配置类在src/main/java/com/example/securitydemo下创建config包然后创建SecurityConfig类。package com.example.securitydemo.config; 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.EnableWebSecurity; 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; Configuration EnableWebSecurity // 启用 Spring Security Web 安全支持 public class SecurityConfig { /** * 配置安全过滤器链这是最核心的配置方法。 * 定义了URL的访问规则、登录/登出行为、异常处理等。 */ Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz - authz // 定义访问规则匹配模式 - 所需权限 .antMatchers(/, /home).permitAll() // 首页允许所有人访问 .antMatchers(/admin/**).hasRole(ADMIN) // /admin 下的所有路径需要 ADMIN 角色 .antMatchers(/user/**).hasRole(USER) // /user 下的所有路径需要 USER 角色 .anyRequest().authenticated() // 其他所有请求都需要认证登录 ) .formLogin(form - form .loginPage(/login) // 指定自定义登录页的路径 .permitAll() // 允许所有人访问登录页 ) .logout(logout - logout .permitAll() // 允许所有人访问登出端点 ); return http.build(); } /** * 配置用户详情服务。这里使用内存存储生产环境需连接数据库。 */ Bean public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { UserDetails admin User.builder() .username(admin) .password(passwordEncoder.encode(admin123)) // 密码必须加密 .roles(ADMIN, USER) // 拥有 ADMIN 和 USER 角色 .build(); UserDetails user User.builder() .username(user) .password(passwordEncoder.encode(user123)) .roles(USER) // 只有 USER 角色 .build(); // 返回一个内存用户管理器其中包含了我们定义的用户 return new InMemoryUserDetailsManager(admin, user); } /** * 配置密码编码器。用于对密码进行加密和验证。 * BCrypt 是目前推荐的强哈希算法。 */ Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }4.2 关键代码解释EnableWebSecurity这个注解至关重要它引入了 Spring Security 的 Web 安全配置支持。没有它你的配置类不会生效。SecurityFilterChainBean这是配置的入口。通过HttpSecurity对象来定制安全行为。authorizeHttpRequests: 定义请求的授权规则。规则按顺序匹配所以更具体的规则要放在前面。antMatchers(“/admin/**”).hasRole(“ADMIN”): 表示匹配/admin及其子路径的请求需要用户拥有ROLE_ADMIN权限注意配置时写ADMIN框架会自动加上ROLE_前缀。anyRequest().authenticated(): 这是一个兜底规则确保所有未被前面规则匹配的请求都需要认证。UserDetailsServiceBean提供用户信息的来源。这里使用了内存存储 (InMemoryUserDetailsManager)方便演示。生产环境需要实现从数据库加载用户。PasswordEncoderBean绝对不要以明文存储密码BCryptPasswordEncoder会对密码进行单向哈希加密。在创建用户和登录验证时框架会自动使用它。4.3 创建测试 Controller 和页面为了测试配置我们需要创建一些端点。创建HomeController:package com.example.securitydemo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; Controller public class HomeController { GetMapping({/, /home}) public String home() { return home; // 对应 templates/home.html } GetMapping(/user/dashboard) public String userDashboard() { return user/dashboard; } GetMapping(/admin/dashboard) public String adminDashboard() { return admin/dashboard; } GetMapping(/login) public String login() { return login; // 对应 templates/login.html } }创建 Thymeleaf 模板: 在src/main/resources/templates下创建以下 HTML 文件。home.html(允许匿名访问):!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head title首页/title /head body h1欢迎来到首页/h1 p这个页面所有人都可以访问。/p a th:href{/user/dashboard}用户仪表盘/a | a th:href{/admin/dashboard}管理员仪表盘/a | a th:href{/login}登录/a /body /htmllogin.html(自定义登录页):!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head title登录/title /head body div th:if${param.error} p stylecolor:red;用户名或密码错误/p /div div th:if${param.logout} p stylecolor:green;你已成功登出。/p /div form th:action{/login} methodpost div label用户名: input typetext nameusername//label /div div label密码: input typepassword namepassword//label /div div input typesubmit value登录/ /div /form pa th:href{/home}返回首页/a/p /body /html在templates下创建user和admin文件夹分别创建dashboard.html。user/dashboard.html:h1用户仪表盘/h1p只有 USER 角色可以访问。/padmin/dashboard.html:h1管理员仪表盘/h1p只有 ADMIN 角色可以访问。/p4.4 运行与验证重启应用进行以下测试访问http://localhost:8080/home应能直接看到首页无需登录。点击“用户仪表盘”链接会被重定向到/login页面因为我们配置了/user/**需要USER角色。使用user/user123登录。成功登录后应能访问/user/dashboard。尝试访问/admin/dashboard会得到403 Forbidden错误页因为user没有ADMIN角色。登出访问http://localhost:8080/logout这是 Spring Security 默认提供的端点。使用admin/admin123登录。应能同时访问/user/dashboard和/admin/dashboard因为admin用户同时拥有USER和ADMIN角色。至此一个基于角色进行 URL 权限控制的基本系统已经完成。5. 深入功能记住我、方法级安全与数据库集成基础配置跑通后我们来完善几个生产环境中常见的功能。5.1 实现“记住我”功能“记住我”功能允许用户在关闭浏览器后一段时间内再次访问网站时无需重新登录。Spring Security 通过 Cookie 实现。修改SecurityConfig.filterChain方法在formLogin配置后添加Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz - authz .antMatchers(/, /home).permitAll() .antMatchers(/admin/**).hasRole(ADMIN) .antMatchers(/user/**).hasRole(USER) .anyRequest().authenticated() ) .formLogin(form - form .loginPage(/login) .permitAll() ) .logout(logout - logout .permitAll() ) // 启用“记住我”功能 .rememberMe(remember - remember .key(uniqueAndSecretKey) // 必须设置一个密钥用于生成 Token .tokenValiditySeconds(86400) // Token 有效期单位秒这里设置 24 小时 ); return http.build(); }同时修改login.html在表单中添加一个复选框form th:action{/login} methodpost !-- ... 用户名密码输入框 ... -- div labelinput typecheckbox nameremember-me/ 记住我/label /div div input typesubmit value登录/ /div /form关键点key是用于签名和验证记住我 Token 的密钥生产环境应使用强随机字符串并从配置文件中读取。tokenValiditySeconds设置 Cookie 的有效期。前端复选框的name属性必须是remember-me这是 Spring Security 默认的参数名。5.2 启用方法级安全控制除了在HttpSecurity中配置 URL 规则我们还可以在 Service 层或 Controller 层的方法上使用注解进行更细粒度的控制。在SecurityConfig类上添加EnableGlobalMethodSecurity注解Configuration EnableWebSecurity EnableGlobalMethodSecurity(prePostEnabled true) // 启用 PreAuthorize 等注解 public class SecurityConfig { // ... 其他配置不变 }在 Controller 或 Service 方法上使用注解import org.springframework.security.access.prepost.PreAuthorize; RestController RequestMapping(/api) public class ApiController { GetMapping(/user-info) PreAuthorize(hasRole(USER)) // 只有 USER 角色可以访问 public String getUserInfo() { return User Info; } GetMapping(/admin-info) PreAuthorize(hasRole(ADMIN)) // 只有 ADMIN 角色可以访问 public String getAdminInfo() { return Admin Info; } // 更复杂的 SpEL 表达式 GetMapping(/profile/{username}) PreAuthorize(#username authentication.name or hasRole(ADMIN)) // 允许用户查看自己的资料或者管理员查看任何人的资料 public String getProfile(PathVariable String username) { return Profile of username; } }方法级安全提供了更大的灵活性特别是当权限逻辑与业务数据紧密相关时。5.3 集成数据库用户存储使用 JPA内存用户仅用于演示。实际项目用户信息必然存储在数据库中。这里演示如何集成 Spring Data JPA。添加依赖(pom.xml):dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope !-- 使用 H2 内存数据库方便演示 -- /dependency !-- 如果使用 MySQL -- !-- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency --创建用户实体User和角色实体Role:package com.example.securitydemo.entity; import javax.persistence.*; import java.util.Collection; Entity Table(name users) // 避免使用 SQL 关键字 user public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String username; private String password; private boolean enabled; ManyToMany(fetch FetchType.EAGER) JoinTable( name users_roles, joinColumns JoinColumn(name user_id), inverseJoinColumns JoinColumn(name role_id) ) private CollectionRole roles; // 构造方法、Getter、Setter 省略... }package com.example.securitydemo.entity; import javax.persistence.*; Entity Table(name roles) public class Role { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String name; // 例如ROLE_USER, ROLE_ADMIN // 构造方法、Getter、Setter 省略... }创建 Repository:package com.example.securitydemo.repository; import com.example.securitydemo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface UserRepository extends JpaRepositoryUser, Long { OptionalUser findByUsername(String username); }实现自定义的UserDetailsService: Spring Security 需要UserDetailsService来加载用户。我们需要实现它从数据库查询用户信息。package com.example.securitydemo.service; import com.example.securitydemo.entity.Role; import com.example.securitydemo.entity.User; import com.example.securitydemo.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import javax.transaction.Transactional; import java.util.Collection; import java.util.stream.Collectors; Service public class CustomUserDetailsService implements UserDetailsService { Autowired private UserRepository userRepository; Override Transactional public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user userRepository.findByUsername(username) .orElseThrow(() - new UsernameNotFoundException(用户不存在: username)); // 将数据库中的 User 实体转换为 Spring Security 认识的 UserDetails return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), user.isEnabled(), true, // accountNonExpired true, // credentialsNonExpired true, // accountNonLocked mapRolesToAuthorities(user.getRoles()) ); } private Collection? extends GrantedAuthority mapRolesToAuthorities(CollectionRole roles) { return roles.stream() .map(role - new SimpleGrantedAuthority(role.getName())) .collect(Collectors.toList()); } }修改SecurityConfig: 移除之前内存存储的userDetailsServiceBean。Spring Security 会自动发现我们实现的CustomUserDetailsService并注入。 同时需要配置密码编码器确保注册时密码被正确加密登录时能正确比对。Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // 不再需要显式定义 userDetailsService Bean初始化数据(可选用于测试): 可以在src/main/resources下创建data.sqlSpring Boot 启动时会自动执行。INSERT INTO roles (name) VALUES (ROLE_USER); INSERT INTO roles (name) VALUES (ROLE_ADMIN); -- 密码是 admin123 经过 BCrypt 加密后的结果 INSERT INTO users (username, password, enabled) VALUES (admin, $2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTV6UiC, 1); INSERT INTO users (username, password, enabled) VALUES (user, $2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTV6UiC, 1); INSERT INTO users_roles (user_id, role_id) VALUES (1, 1); -- admin has ROLE_USER INSERT INTO users_roles (user_id, role_id) VALUES (1, 2); -- admin has ROLE_ADMIN INSERT INTO users_roles (user_id, role_id) VALUES (2, 1); -- user has ROLE_USER注意$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTV6UiC是字符串admin123经过 BCrypt 加密后的结果。你可以写一个简单的 Java 程序或使用在线工具生成。现在应用的用户和角色信息完全来自数据库具备了生产环境的基本形态。6. 常见问题排查与最佳实践在实际集成 Spring Security 时你几乎一定会遇到一些问题。下面是一些常见问题及其排查路径。6.1 常见问题排查表问题现象可能原因检查方式处理建议登录失败无错误提示1. CSRF 保护未禁用且表单未包含 CSRF Token。2. 登录请求的 URL 或参数名不对。3. 密码编码器不匹配。1. 查看浏览器开发者工具 Network 标签确认请求是否包含_csrf参数。2. 确认表单action是/login(POST)用户名参数是username密码参数是password。3. 检查数据库中的密码是否与配置的PasswordEncoder匹配。1. 确保表单包含 CSRF Token (input type”hidden” th:name”${_csrf.parameterName}” th:value”${_csrf.token}”/)。2. 核对请求参数。3. 确保注册/初始化数据时使用了相同的PasswordEncoder。403 Forbidden (权限不足)1. 用户角色与访问规则不匹配。2. 方法级安全注解 (PreAuthorize) 生效但权限不足。3. CSRF Token 无效或缺失。1. 检查SecurityConfig中的antMatchers规则和用户的角色。2. 检查 Controller/Service 方法上的注解。3. 检查 POST/PUT/DELETE 请求是否携带了有效的 CSRF Token。1. 调整访问规则或为用户分配正确角色。2. 检查EnableGlobalMethodSecurity是否已启用。3. 对于 API 接口可以考虑在HttpSecurity配置中.csrf().disable()但需评估安全风险。重定向循环1. 登录页 (/login) 本身也需要认证但规则配置错误。2. 成功登录后的默认跳转路径 (defaultSuccessUrl) 也需要认证。1. 检查SecurityConfig确保.loginPage(“/login”).permitAll()已配置。2. 检查登录成功后是否跳转到了一个仍需认证的页面。1. 确保登录页、静态资源等路径在permitAll()规则中。2. 明确设置.defaultSuccessUrl(“/home”, true)(第二个参数true表示总是跳转到此URL)。“记住我”功能无效1. 前端复选框name不是remember-me。2. 配置中未设置key或key太简单。3. 浏览器禁用了 Cookie。1. 检查 HTML 表单。2. 检查SecurityConfig中.rememberMe().key(“…”)配置。3. 检查浏览器 Cookie 设置。1. 确保前端参数名正确。2. 设置一个复杂且唯一的key。3. 确保浏览器允许 Cookie。自定义UserDetailsService不生效1. 实现类未被 Spring 扫描到缺少Service注解。2. 存在多个UserDetailsServiceBean 造成冲突。1. 检查类是否在组件扫描路径下是否有Service。2. 检查SecurityConfig中是否还定义了其他UserDetailsServiceBean。1. 添加正确的注解。2. 移除冲突的 Bean 定义让 Spring 自动注入唯一的实现。6.2 生产环境最佳实践清单密码安全永远使用强哈希算法如 BCrypt存储密码禁止明文。考虑密码复杂度策略和定期更换。会话管理设置合理的会话超时时间。对于敏感操作使用二次验证。考虑防止会话固定攻击。CSRF 保护对于传统 Web 应用服务端渲染保持 CSRF 保护开启并在所有表单和 AJAX 请求中包含 Token。对于纯 API 后端如 SPA RESTful API可以考虑禁用 CSRF (csrf().disable())但必须使用其他机制如 JWT并妥善处理 CORS。CORS 配置如果前端与后端分离部署必须在SecurityConfig或全局配置中正确设置 CORS 策略避免跨域问题。细粒度授权结合 URL 规则 (antMatchers) 和方法级注解 (PreAuthorize)实现灵活的权限控制。对于复杂的业务规则可以考虑实现自定义的AccessDecisionVoter或PermissionEvaluator。日志与监控记录重要的安全事件如登录成功/失败、权限拒绝、敏感操作。集成监控关注异常登录行为如频繁失败、非常用地点。配置外置将敏感信息如数据库密码、Remember-Me Key、JWT Secret放在配置文件如application.yml或配置中心不要硬编码。定期更新关注 Spring Security 的版本更新及时修复安全漏洞。7. 扩展方向与总结通过本文的步骤你已经成功搭建了一个具备基础认证和授权功能的 Spring Boot 应用。但这仅仅是 Spring Security 能力的冰山一角。根据项目需求你可以继续深入以下方向OAuth2 / OIDC 集成实现第三方登录如微信、GitHub、Google或构建统一的单点登录系统。JWT (JSON Web Token)为无状态 API 设计认证方案替代传统的 Session-Cookie 模式。多因素认证集成短信、邮件或 TOTP 验证码提升账户安全等级。LDAP / Active Directory 集成与企业现有的目录服务对接。自定义登录逻辑实现图形验证码、限流登录、根据设备或IP限制等复杂需求。Spring Security 的强大在于其高度可配置和可扩展的架构。理解其核心的过滤器链、SecurityContext和Authentication机制是驾驭它的关键。在遇到问题时多查看官方文档和调试日志理解请求在过滤器链中的流转过程大部分难题都能迎刃而解。从本文的最小可运行案例出发逐步增加复杂度是掌握 Spring Security 最稳妥的路径。