之前在业务系统里接入 Spring Security 做登录认证和权限控制时,说实话踩了不少坑。网上资料虽然多,但很多是零散片段,要么只讲入门 Demo,要么直接甩一堆配置让人照着抄,出了问题也不知道怎么排查。这篇文章打算围绕 Spring Security 在 Spring Boot 项目中的落地,整理一套完整的实操方案,从核心概念、环境搭建、认证流程、权限配置到常见报错和最佳实践,尽量做到让新手能看懂原理、让有基础的开发者能快速复用代码。
文章里涉及到的代码和配置都基于一个最简单的 RBAC 权限模型来设计,没有引入太复杂的微服务、网关、分布式会话等概念,方便把注意力集中到 Spring Security 本身。
1. 认清 Spring Security 在项目中到底解决什么问题
1.1 认证和授权是两个不同的问题
很多初学者刚接触 Spring Security 的时候,容易把“认证”和“授权”混在一起。实际上这是两件事:
- 认证(Authentication)解决的是“你是谁”的问题,也就是说系统需要确认当前访问的人确实是某个合法用户。
- 授权(Authorization)解决的是“你能干什么”的问题,也就是确认这个用户有没有权限访问某个接口、操作某个资源。
Spring Security 的底层是一组过滤器链,请求进入应用后,会先经过认证相关的过滤器,确认用户身份;然后再根据配置的权限规则判断当前用户能否访问目标资源。理解这一点很重要,因为很多配置问题、权限不生效的问题,本质上是没有把这两条链路拆开看。
1.2 Spring Security 的核心组件
我们在项目中接触最多的几个组件包括:
- SecurityFilterChain:负责定义哪些请求需要认证、哪些请求放行、使用什么认证方式。
- AuthenticationManager:认证管理器,负责协调具体的认证逻辑。
- UserDetailsService:负责根据用户名加载用户信息。
- PasswordEncoder:负责密码加密和校验。
- SecurityContextHolder:存储当前登录用户信息的容器。
这些组件之间的关系可以这样理解:请求进来之后,过滤器把用户名和密码封装成一个 Authentication 对象,交给 AuthenticationManager 去校验,校验过程中会通过 UserDetailsService 查询用户信息,再用 PasswordEncoder 比对密码。校验通过后,Authentication 对象会被放到 SecurityContextHolder 中,后续的业务代码就能获取当前登录用户了。
1.3 为什么需要掌握 Spring Security
只要项目里涉及到用户登录、后台管理、接口权限控制,就绕不开 Spring Security。虽然也可以自己写拦截器、写注解、写 Session 校验,但 Spring Security 提供了一套完整的、经过大量生产环境验证的方案,尤其在密码加密、会话管理、CSRF 防护、方法级权限控制等方面,比自己造轮子要可靠得多。
另外,Spring Security 也是 Spring Boot 生态中与 Spring 家族集成最顺畅的安全框架,和 Spring Boot、Spring Cloud 搭配使用时,各方面的支持都比较完善。
2. 环境准备与项目初始化
2.1 版本说明
本文示例使用以下环境:
- JDK 1.8 或更高版本
- Spring Boot 2.7.x
- Spring Security 5.7.x(Spring Boot 2.7 默认引入的版本)
- Maven 3.6+
- IDE:IntelliJ IDEA
需要说明的是,Spring Security 5.7 之后配置方式有一些调整,WebSecurityConfigurerAdapter 已经废弃,官方推荐直接使用 SecurityFilterChain Bean 的方式进行配置。本文采用这种新写法。
如果你使用的是 Spring Boot 3.x,对应的 Spring Security 是 6.x,部分 API 有所变化,建议参考官方文档调整。
2.2 创建项目并引入依赖
先创建一个 Spring Boot 项目,在pom.xml中引入 Web 和 Spring Security 依赖。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <!-- Web 依赖 --> <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> <!-- Lombok,简化实体类代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 测试依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>引入spring-boot-starter-security之后,再启动项目,控制台会输出一个默认密码。此时访问任意接口,都会弹出登录页面,默认用户名是user,密码就是控制台打印的那串随机 UUID。这就是 Spring Security 的默认保护机制生效了。
2.3 项目结构
为了方便后续扩展,建议把代码按职责分层。本文的完整项目结构如下:
src/main/java/com/example/securitydemo ├── SecurityDemoApplication.java ├── config │ └── SecurityConfig.java ├── controller │ └── UserController.java ├── entity │ └── User.java ├── mapper │ └── UserMapper.java ├── service │ ├── UserService.java │ └── impl │ └── UserServiceImpl.java └── vo ├── LoginRequest.java └── Result.java下面会按照这个结构依次创建各个文件。为了让示例尽量简单,用户数据暂时存在内存中,不连接数据库,等理解核心流程之后,再替换为 MyBatis 或 JPA 实现。
3. 核心概念与配置拆解
3.1 密码加密:PasswordEncoder 不能省略
在实际项目中,用户密码绝对不能以明文形式存储。Spring Security 提供了PasswordEncoder接口,常用的实现有:
BCryptPasswordEncoder:推荐使用,BCrypt 算法会自动加盐,每次加密结果都不一样。DelegatingPasswordEncoder:Spring Boot 默认的编码器,支持多种密码格式,格式类似{bcrypt}密文。
这里推荐直接使用BCryptPasswordEncoder。它的特点是:同一个明文密码,每次加密得到的密文不同,但是校验方法matches可以正确判断。这样即使数据库泄露,攻击者也很难通过彩虹表反推明文密码。
我们先把密码编码器定义为一个 Bean:
@Configuration public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }3.2 核心配置:SecurityFilterChain
接下来编写核心的安全配置类。在 Spring Security 5.7 之后的写法中,我们通过定义一个SecurityFilterChainBean 来完成过滤链配置。
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeRequests(auth -> auth // 放行登录接口和静态资源 .antMatchers("/api/login", "/css/**", "/js/**").permitAll() // 其他请求都需要认证 .anyRequest().authenticated() ) .formLogin(form -> form .loginProcessingUrl("/api/login") .permitAll() ) .logout(logout -> logout .logoutUrl("/api/logout") .permitAll() ) .csrf(csrf -> csrf.disable()); return http.build(); } }这段配置的含义如下:
authorizeRequests:定义请求的访问规则。antMatchers("/api/login", "/css/**", "/js/**"):这些路径不拦截,可以直接访问。anyRequest().authenticated():除了上面放行的路径,其他请求都要求登录后才能访问。formLogin:启用表单登录,并指定登录处理地址为/api/login。logout:配置退出登录地址。csrf(csrf -> csrf.disable()):关闭 CSRF。如果是在前后端分离的项目中,通常后端不维护页面表单,CSRF 防护意义不大;如果是服务端渲染的传统项目,建议保留 CSRF 防护。
3.3 用户信息加载:UserDetailsService
Spring Security 需要知道如何根据用户名查询用户信息,这个逻辑放在UserDetailsService中。我们在内存中模拟两个用户:
@Service public class UserServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; private final Map<String, User> userMap = new HashMap<>(); @PostConstruct public void init() { // 模拟两个用户 User admin = new User("admin", passwordEncoder.encode("123456"), "ADMIN"); User user = new User("zhangsan", passwordEncoder.encode("123456"), "USER"); userMap.put(admin.getUsername(), admin); userMap.put(user.getUsername(), user); } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMap.get(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) .password(user.getPassword()) .roles(user.getRole()) .build(); } }这里的User是自定义实体类,包含username、password、role三个字段。最终通过User.withUsername()构建 Spring Security 的UserDetails对象,并设置角色。
注意角色命名的时候,Spring Security 会在hasRole()判断时自动加上ROLE_前缀。所以这里设置角色为ADMIN,实际对应的权限标识是ROLE_ADMIN。
3.4 认证入口:AuthenticationManager
如果使用自定义登录接口,我们需要让 Spring Security 暴露AuthenticationManager,然后手动执行认证逻辑。
首先在 SecurityConfig 中注入并暴露AuthenticationManager:
@Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private UserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 这里需要把自定义的 UserDetailsService 设置进去 http .userDetailsService(userDetailsService) .authorizeRequests(auth -> auth .antMatchers("/api/login").permitAll() .antMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(form -> form .loginProcessingUrl("/api/login") .successHandler((req, res, auth) -> { res.setContentType("application/json;charset=utf-8"); res.getWriter().write("{\"code\":200,\"msg\":\"登录成功\"}"); }) .failureHandler((req, res, ex) -> { res.setContentType("application/json;charset=utf-8"); res.getWriter().write("{\"code\":401,\"msg\":\"用户名或密码错误\"}"); }) .permitAll() ) .logout(logout -> logout.logoutUrl("/api/logout")) .csrf(csrf -> csrf.disable()); return http.build(); } }hasRole("ADMIN")表示访问/api/admin/**路径时,当前用户必须拥有ROLE_ADMIN权限。这是最简单的授权方式,适合角色固定的后台系统。
3.5 自定义登录接口
虽然可以通过表单登录处理器来实现认证,但在前后端分离项目中,更常用的方式是自己写一个登录 Controller,调用AuthenticationManager完成认证。这样做的好处是登录逻辑更可控,比如可以自定义参数格式、增加验证码校验等。
@RestController public class AuthController { @Autowired private AuthenticationManager authenticationManager; @PostMapping("/api/login") public Result login(@RequestBody LoginRequest loginRequest) { // 1. 创建认证信息 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); // 2. 执行认证 Authentication authenticate = authenticationManager.authenticate(authenticationToken); // 3. 认证成功后,把身份信息放入 SecurityContext SecurityContextHolder.getContext().setAuthentication(authenticate); return Result.success("登录成功", authenticate.getAuthorities()); } @GetMapping("/api/me") public Result me() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return Result.success(authentication.getName()); } }这里有几个细节需要说明:
UsernamePasswordAuthenticationToken是 Spring Security 提供的最常用的认证凭据对象。authenticationManager.authenticate()方法会调用我们之前配置的UserDetailsService和PasswordEncoder完成校验。- 如果用户名或密码错误,会抛出
BadCredentialsException,建议在全局异常处理器中统一捕获。
3.6 方法级权限控制
除了在 URL 层面做授权,Spring Security 还支持在方法上通过注解控制权限。这种方式更灵活,适合对 Service 层或 Controller 层的方法做细粒度控制。
首先在配置类上开启方法安全:
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig { // 配置代码... }然后在方法上使用@PreAuthorize:
@RestController @RequestMapping("/api/admin") public class AdminController { @GetMapping("/list") @PreAuthorize("hasRole('ADMIN')") public Result list() { return Result.success("管理员列表数据"); } }@PreAuthorize在方法执行前检查权限,不满足会抛出AccessDeniedException。通过这种方式,我们可以在同一个 Controller 中,针对不同方法设置不同的权限要求。
4. 完整实战:基于内存用户的管理系统登录与权限控制
现在我们把上面的内容整合成一个完整的可运行示例,覆盖用户登录、获取当前用户、管理员接口、普通用户接口这几个场景。
4.1 创建实体类
package com.example.securitydemo.entity; import lombok.Data; @Data public class User { private String username; private String password; private String role; public User(String username, String password, String role) { this.username = username; this.password = password; this.role = role; } }4.2 创建统一返回结果类
package com.example.securitydemo.vo; import lombok.Data; @Data public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("成功"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String msg) { Result<T> result = new Result<>(); result.setCode(code); result.setMsg(msg); return result; } }4.3 创建登录请求体
package com.example.securitydemo.vo; import lombok.Data; @Data public class LoginRequest { private String username; private String password; }4.4 配置 SecurityConfig
package com.example.securitydemo.config; import com.example.securitydemo.service.UserServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig { @Autowired private UserServiceImpl userServiceImpl; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .userDetailsService(userServiceImpl) // 使用无状态会话,适合前后端分离 .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) .authorizeRequests(auth -> auth .antMatchers("/api/login").permitAll() .antMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(form -> form.disable()) .httpBasic(basic -> basic.disable()) .csrf(csrf -> csrf.disable()); return http.build(); } }这里做了两个调整:
- 关闭了默认的
formLogin和httpBasic,因为前后端分离场景中不需要这些默认入口。 - 设置了会话策略为
IF_REQUIRED,表示如果需要会话就创建 Session,默认情况下登录成功后会创建 Session,后续请求通过 Session 识别用户。如果想改成真正的无状态 JWT 方案,可以在这里设置为STATELESS。
4.5 编写用户服务
package com.example.securitydemo.service; import com.example.securitydemo.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.util.HashMap; import java.util.Map; @Service public class UserServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; private final Map<String, User> userMap = new HashMap<>(); @PostConstruct public void init() { userMap.put("admin", new User("admin", passwordEncoder.encode("123456"), "ADMIN")); userMap.put("zhangsan", new User("zhangsan", passwordEncoder.encode("123456"), "USER")); } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMap.get(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) .password(user.getPassword()) .roles(user.getRole()) .build(); } }4.6 编写登录接口和用户接口
package com.example.securitydemo.controller; import com.example.securitydemo.vo.LoginRequest; import com.example.securitydemo.vo.Result; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; @RestController public class AuthController { @Autowired private AuthenticationManager authenticationManager; @PostMapping("/api/login") public Result login(@RequestBody LoginRequest loginRequest) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); SecurityContextHolder.getContext().setAuthentication(authenticate); return Result.success(authenticate.getName()); } @GetMapping("/api/me") public Result me() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated()) { return Result.error(401, "未登录"); } return Result.success(authentication.getName()); } @GetMapping("/api/user/info") public Result userInfo() { return Result.success("普通用户可访问"); } }package com.example.securitydemo.controller; import com.example.securitydemo.vo.Result; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/admin") public class AdminController { @GetMapping("/list") @PreAuthorize("hasRole('ADMIN')") public Result list() { return Result.success("管理员接口数据"); } }4.7 运行与验证
启动SecurityDemoApplication,使用 Postman 或 curl 验证接口。
第一步,访问普通接口,未登录时返回 401 或 403:
GET http://localhost:8080/api/user/info第二步,调用登录接口:
POST http://localhost:8080/api/login Content-Type: application/json { "username": "admin", "password": "123456" }成功后会返回当前用户名,同时浏览器或客户端会记录 Session Cookie。之后访问受保护接口时会带着 Cookie 自动认证。
第三步,使用 zhangsan 登录后访问管理员接口:
GET http://localhost:8080/api/admin/list因为 zhangsan 的角色是USER,没有ROLE_ADMIN权限,此时接口会返回 403,这说明方法级别权限控制生效了。
5. 常见的坑与排查思路
5.1 自定义登录接口一直 401
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| POST /api/login 返回 401 | 自定义的登录接口没有放行,被过滤链拦截 | 在 SecurityConfig 的antMatchers中加入/api/login并调用permitAll() |
| 返回 403 | CSRF 防护开启,请求头缺少 Token | 前后端分离项目中可以显式关闭 CSRF |
| 返回 XML 或网页格式错误 | 默认的认证失败处理器返回的是重定向 | 自定义 AuthenticationEntryPoint 并返回 JSON |
需要注意的是,permitAll()只代表该路径不需要认证就能访问,不代表该路径不会经过过滤器。如果自定义登录接口依赖AuthenticationManager手动认证,那么登录接口本身放行是没有问题的。
5.2 密码加密后登录始终失败
常见的原因有两个:
UserDetails中的密码使用了明文,而PasswordEncoder使用的是 BCrypt。- 初始化用户时,数据库中存的密码不是 BCrypt 编码后的值。
解决方案是:在用户初始化时通过passwordEncoder.encode()存储密码,不要直接存明文。
5.3 hasRole 和 hasAuthority 混淆
hasRole("ADMIN")实际检查的是ROLE_ADMIN权限,hasAuthority("ROLE_ADMIN")直接检查ROLE_ADMIN权限。两者在设置权限时写法略有不同:
// 方式一:设置角色 .roles("ADMIN") // 对应 ROLE_ADMIN // 方式二:设置权限 .authorities("ROLE_ADMIN")如果设置了角色ADMIN,但用了hasAuthority("ADMIN"),就会一直返回 403。这是非常常见的配置问题。
5.4 引入 Security 后静态资源被拦截
如果在传统模板项目中引入了 Spring Security,CSS、JS、图片等静态资源也会被拦截。解决方法是放行静态资源路径:
.authorizeRequests(auth -> auth .antMatchers("/static/**", "/css/**", "/js/**", "/images/**").permitAll() ... )5.5 方法级权限注解不生效
如果加了@PreAuthorize但没有生效,检查两件事:
- 是否在配置类上加了
@EnableGlobalMethodSecurity(prePostEnabled = true)。 - 当前请求是否已经通过认证,
@PreAuthorize是在认证之后才执行权限校验的。
5.6 会话失效或每次都要求重新登录
如果部署在多实例环境中,默认的 Session 无法跨实例共享。常见的解决方案是:
- 使用 Spring Session + Redis 实现会话共享。
- 改用 JWT 无状态认证,每次请求携带 Token。
如果只是单机环境,默认的 Session 机制是够用的。
6. 生产环境落地建议
前面讲的都是基础功能,真正要把 Spring Security 用到生产环境,还需要关注下面几个方面。
6.1 用户数据接入数据库
内存用户的写法只是为了演示,生产环境应该把用户信息放在数据库中,通过 MyBatis 或 JPA 查询。核心变化点在UserDetailsService的实现中,原来是操作 Map,现在改成操作数据库表。
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = userMapper.selectByUsername(username); if (sysUser == null) { throw new UsernameNotFoundException("用户不存在"); } return org.springframework.security.core.userdetails.User .withUsername(sysUser.getUsername()) .password(sysUser.getPassword()) .roles(sysUser.getRole()) .build(); }如果后续需要接入用户状态禁用、密码过期等功能,可以在UserDetails的实现类中对应设置enabled、accountNonExpired、credentialsNonExpired、accountNonLocked这几个属性。
6.2 JWT 无状态认证改造
前后端分离 + 多实例部署的场景下,JWT 是比较常见的方案。核心思路是:
- 登录成功后生成 JWT Token 返回给前端。
- 前端每次请求在 Header 中携带
Authorization: Bearer <token>。 - 后端写一个过滤器,解析 Token 得到用户信息,放入 SecurityContext。
实现时需要注意:
- JWT 的密钥必须放到配置中心或环境变量中,不能硬编码在代码里。
- Token 需要设置过期时间,建议不要过长。
- 网关或认证中心统一处理 Token 解析,避免业务服务各自实现一遍。
- 在
SecurityFilterChain中把自定义的 JWT 过滤器加入过滤器链,并设置为无状态会话。
http .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);6.3 权限模型设计
本文使用的是最简单的基于角色的权限模型。实际项目中角色和权限往往需要拆分,例如:用户属于多个角色,角色关联多个权限点。这种情况下,UserDetails中的authorities存储的是权限点标识,而不是角色名称。
推荐的做法是:
- 用户表、角色表、权限表、用户角色关联表、角色权限关联表。
UserDetailsService加载用户时,查询出该用户的所有角色和权限点。- 接口授权时使用
hasAuthority("system:user:list")或@PreAuthorize("hasAuthority('system:user:list')")。
这种权限点字符串的设计便于后续做按钮级权限控制和动态菜单,比单纯用角色控制更精细。
6.4 统一异常处理
Spring Security 在认证和授权失败时,默认返回的响应不一定适合前端解析。建议自定义AuthenticationEntryPoint(未登录时进入)和AccessDeniedHandler(已登录但无权限时进入),统一返回 JSON 格式的错误信息。
@Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); } }然后把这两个处理器配置到 SecurityConfig 中:
.exceptionHandling(exception -> exception .authenticationEntryPoint(restAuthenticationEntryPoint) .accessDeniedHandler(restAccessDeniedHandler) )6.5 密码策略和账号安全
生产环境中建议关注:
- 密码必须使用 BCrypt 或更高强度的算法加密,禁止明文存储。
- 增加登录失败次数限制,防止暴力破解。
- 管理后台开启验证码。
- 关键操作(如删除数据、修改权限)增加操作审计日志。
- 敏感接口增加 IP 白名单或频率限制。
7. 总结
Spring Security 的内容远不止本文提到的这些,但它解决的核心问题始终是认证和授权。对初学者来说,先理解过滤器链、AuthenticationManager、UserDetailsService、PasswordEncoder 之间的关系,再动手写一个完整的登录实例,比直接堆配置要有效得多。
本文主要掌握了以下知识点:
- Spring Security 中认证和授权的区别。
- 基于内存用户的完整登录认证流程。
SecurityFilterChain的基本配置方式。- 方法级权限控制
@PreAuthorize的使用。 - 常见的 401、403、密码校验失败、权限不生效等问题的排查方法。
下一步可以继续学习 JWT 无状态认证、Spring Security OAuth2 客户端、基于数据库的 RBAC 权限模型、方法级权限和动态权限控制,这些都是实际项目中经常用到的方向。你可以在本文示例的基础上,先把用户数据切换到数据库,再加上一个简单的 JWT Token 生成与校验,就是一个非常接近生产环境的基础框架了。