
作为互联网软件开发人员,你是否曾在项目中遇到过这些问题?用户密码明文存储导致数据泄露,不同角色的用户能随意访问敏感接口,跨域请求时认证信息失效…… 这些安全隐患不仅会影响用户体验,更可能给项目带来严重的安全风险。而 Spring Security,作为 Java 生态中最主流的安全框架,正是解决这些问题的 “利器”。今天这篇文章,我们就从实战角度出发,手把手教你搞定 Spring Security 的认证与授权,让你的接口防护能力直接拉满。
为什么必须掌握 Spring Security?先看 3 个真实开发场景
在开始技术讲解前,我们先聊聊为什么 Spring Security 是后端开发者的 “必修课”。我曾接触过三个典型的开发案例,或许能让你更直观地理解它的重大性。
第一个案例是某电商平台的后台管理系统。初期开发时,团队为了赶进度,只简单做了 “用户名 + 密码” 的登录验证,没有做角色权限控制。结果上线后,运营人员通过普通账号竟能访问到订单支付接口,还能修改用户的支付金额 —— 这就是典型的 “授权缺失” 导致的安全漏洞。最后团队紧急引入 Spring Security,花了 3 天时间重构权限体系,才避免了更大的损失。
第二个案例是某 SaaS 系统的 API 开发。开发者在设计认证机制时,自己手写了 Token 生成和验证逻辑,却忽略了 Token 的过期刷新、签名加密等细节。上线后不久,就出现了 Token 被伪造的情况,导致多个企业用户的数据被非法获取。后来改用 Spring Security 的 OAuth2.0 组件,仅用半天就实现了安全合规的认证流程。
第三个案例更常见:许多开发者在集成 Spring Security 时,由于不了解其核心过滤器链的工作原理,盲目自定义配置,导致出现 “登录成功后仍无法访问接口”“权限注解不生效” 等问题,排查了一整天才发现是过滤器顺序配置错误。
实则这些问题的根源,都是对 Spring Security 的认证与授权核心逻辑理解不透彻。接下来,我们就从基础概念入手,一步步搭建实战案例,帮你彻底搞懂这套框架。
认证:搞定 “你是谁” 的核心流程(附代码实战)
第一要明确:认证(Authentication)是确认用户身份的过程,简单说就是回答 “你是谁” 的问题。Spring Security 的认证流程围绕 “Authentication 对象” 和 “AuthenticationManager 接口” 展开,我们先拆解核心组件,再写代码。
2.1 3 个核心组件,搞懂认证底层逻辑
Authentication 对象:存储认证相关信息的载体,包含 3 个关键属性:
- principal:用户身份信息(如用户名、用户实体类对象);
- credentials:用户凭证(如密码,认证成功后会被清空,避免泄露);
- authorities:用户拥有的权限(认证阶段暂不关注,授权阶段会用到)。
认证前,我们会创建一个 “未认证” 的 Authentication 对象,传入用户名和密码;认证成功后,Spring Security 会返回一个 “已认证” 的对象,填充用户的完整信息。
AuthenticationManager:认证的核心接口,只有一个authenticate()方法,负责执行认证逻辑。实际开发中,我们常用它的实现类ProviderManager,它可以管理多个AuthenticationProvider(不同的认证方式,如用户名密码认证、短信验证码认证)。
UserDetailsService:加载用户信息的接口,Spring Security 默认会调用它的loadUserByUsername()方法,根据用户名查询数据库中的用户信息(如密码、角色)。这是我们自定义认证逻辑时最常扩展的接口。
简单理解:认证流程就是 “前端传入用户名密码 → 构建未认证 Authentication 对象 → AuthenticationManager 调用 UserDetailsService 查询用户 → 比对密码是否正确 → 返回已认证对象”。
2.2 实战:10 行代码实现数据库认证(Spring Boot 整合)
接下来我们用 Spring Boot 整合 Spring Security,实现 “从数据库查询用户信息” 的认证功能。假设你已经搭建好 Spring Boot 项目,且有一个sys_user表(包含 id、username、password、role 字段)。
步骤 1:引入依赖(pom.xml)
<!-- Spring Security核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- MyBatis-Plus(用于操作数据库,也可用JPA) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
步骤 2:实现 UserDetailsService 接口(查询用户信息)
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper; // 自定义的Mapper接口
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 根据用户名查询数据库
SysUser user = sysUserMapper.selectOne(new QueryWrapper<SysUser>().eq("username", username));
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 2. 转换为Spring Security需要的UserDetails对象
// 注意:密码必须是加密后的(这里假设数据库中存储的是BCrypt加密后的密码)
Collection<? extends GrantedAuthority> authorities =
Collections.singletonList(new SimpleGrantedAuthority(user.getRole()));
return new User(user.getUsername(), user.getPassword(), authorities);
}
}
步骤 3:配置 Spring Security(核心配置类)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
// 密码加密器(使用BCrypt算法)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 配置认证管理器
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder auth = http.getSharedObject(AuthenticationManagerBuilder.class);
// 关联UserDetailsService和密码加密器
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
return auth.build();
}
// 配置接口访问规则、登录逻辑等
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 关闭CSRF(前后端分离项目常用,若为传统项目需开启)
.csrf(csrf -> csrf.disable())
// 配置接口访问权限
.authorizeHttpRequests(auth -> auth
// 放行登录接口(不需要认证就能访问)
.requestMatchers("/api/login").permitAll()
// 其他所有接口都需要认证
.anyRequest().authenticated()
)
// 配置表单登录(前后端分离可改用JSON登录,这里先演示默认表单)
.formLogin(form -> form
// 自定义登录接口地址(默认是/login)
.loginProcessingUrl("/api/login")
// 登录成功后的返回格式(默认是跳转页面,这里改为返回JSON)
.successHandler((request, response, authentication) -> {
response.setContentType("application/json;charset=utf-8");
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "登录成功");
result.put("data", authentication.getPrincipal());
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
})
// 登录失败后的返回格式
.failureHandler((request, response, exception) -> {
response.setContentType("application/json;charset=utf-8");
Map<String, Object> result = new HashMap<>();
result.put("code", 401);
result.put("message", "登录失败:" + exception.getMessage());
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
})
);
return http.build();
}
}
步骤 4:测试认证功能
此时启动项目,发送 POST 请求到/api/login,传入参数username和password(注意:前端传入的是明文密码,Spring Security 会自动用 BCrypt 加密后与数据库中的加密密码比对)。若用户存在且密码正确,会返回如下 JSON:
{
"code": 200,
"message": "登录成功",
"data": {
"username": "admin",
"authorities": [{"authority": "ROLE_ADMIN"}],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
}
}
至此,我们就完成了最基础的数据库认证功能。但这还不够 —— 如果所有认证通过的用户都能访问所有接口,那管理员和普通用户就没有区别了,这就需要 “授权” 来解决。
授权:控制 “你能做什么” 的 3 种实战方案
授权(Authorization)是确认用户拥有哪些权限的过程,也就是回答 “你能做什么” 的问题。Spring Security 的授权核心是 “基于角色的访问控制(RBAC)”,常用的实现方式有 3 种:接口级授权、方法级授权、动态权限授权。我们逐一讲解实战用法。
3.1 方案 1:接口级授权(在 SecurityConfig 中配置)
这种方式最直接,在securityFilterChain()方法中,通过requestMatchers()指定接口路径,并搭配hasRole()或hasAuthority()设置访问权限。
例如,我们希望:
- /api/admin/**接口只能由 “ADMIN” 角色访问;
- /api/user/**接口只能由 “USER” 角色访问;
- /api/public/**接口无需认证即可访问。
修改 SecurityConfig 中的authorizeHttpRequests()配置:
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll() // 放行公开接口
.requestMatchers("/api/admin/**").hasRole("ADMIN") // ADMIN角色可访问
.requestMatchers("/api/user/**").hasRole("USER") // USER角色可访问
.anyRequest().authenticated()
)
注意:hasRole()会自动给角色名加上 “ROLE_” 前缀,所以如果数据库中存储的角色是 “ROLE_ADMIN”,这里用hasRole(“ADMIN”)即可;若数据库中是 “ADMIN”,则需要用hasAuthority(“ADMIN”)(hasAuthority()不会加前缀)。
3.2 方案 2:方法级授权(用注解控制)
如果接口较多,在配置类中逐个配置会很繁琐,此时可以用注解实现 “方法级授权”。只需两步:
步骤 1:在 SecurityConfig 上添加@EnableMethodSecurity注解
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // 开启方法级授权注解
public class SecurityConfig {
// 其他配置不变
}
步骤 2:在 Controller 方法上添加@PreAuthorize注解
@RestController
@RequestMapping("/api/admin")
public class AdminController {
// 只有ADMIN角色能访问该方法
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/user/list")
public String getUserList() {
return "管理员查看用户列表";
}
// 只有同时拥有ADMIN角色和MANAGE_USER权限的用户能访问
@PreAuthorize("hasRole('ADMIN') and hasAuthority('MANAGE_USER')")
@PostMapping("/user/add")
public String addUser() {
return "管理员添加用户";
}
}
这种方式的优势是 “权限与方法绑定”,代码更直观,后期维护也更方便。
3.3 方案 3:动态权限授权(从数据库加载权限)
前面两种方案的权限都是 “写死” 在代码中的,如果需要动态修改权限(列如在后台管理系统中,管理员可以随时调整角色的权限),就需要 “从数据库加载权限”。实现思路是:自定义Filter,在请求到达时,从数据库查询当前用户的权限,再与请求的接口进行匹配。
核心步骤:
设计权限表结构:新增sys_role(角色表)、sys_permission(权限表)、sys_user_role(用户 – 角色关联表)、sys_role_permission(角色 – 权限关联表);
自定义权限过滤器:继承OncePerRequestFilter,在doFilterInternal()方法中:
- 获取当前登录用户;
- 从数据库查询用户拥有的所有权限(如/api/admin/user/list);
- 构建SecurityContext,将权限设置到 Authentication 对象中;
将过滤器加入 Spring Security 的过滤器链。
这里给出关键代码(自定义过滤器):
@Component
public class DynamicPermissionFilter extends OncePerRequestFilter {
@Autowired
private SysPermissionMapper permissionMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 1. 获取当前登录用户
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
// 2. 获取用户名
String username = authentication.getName();
// 3. 从数据库查询用户拥有的权限(这里需要关联用户-角色-权限表)
List<String> permissions = permissionMapper.getPermissionsByUsername(username);
// 4. 构建权限集合
Collection<GrantedAuthority> authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 5. 重新构建Authentication对象,设置动态权限
Authentication newAuth = new UsernamePasswordAuthenticationToken(
authentication.getPrincipal(),
authentication.getCredentials(),
authorities
);
// 6. 更新SecurityContext
SecurityContextHolder.getContext().setAuthentication(newAuth);
}
// 继续执行过滤器链
filterChain.doFilter(request, response);
}
}
然后在 SecurityConfig 中,将这个过滤器加入过滤器链:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, DynamicPermissionFilter dynamicPermissionFilter) throws Exception {
http
// 其他配置不变
.addFilterBefore(dynamicPermissionFilter, UsernamePasswordAuthenticationFilter.class); // 在用户名密码过滤器前执行
return http.build();
}
这样一来,用户的权限就会从数据库动态加载,后续修改权限时,只需更新数据库即可,无需修改代码。
避坑指南:5 个开发中最容易踩的坑
在集成 Spring Security 的过程中,许多开发者会由于对细节不熟悉而踩坑。我整理了 5 个最常见的问题,帮你少走弯路。
坑 1:密码加密方式不匹配
问题表现:明明输入的密码正确,却始终提示 “Bad credentials”(密码错误)。
缘由:数据库中存储的密码是明文,而 Spring Security 默认使用 BCrypt 加密器,会将前端传入的明文密码加密后与数据库中的明文比对,自然不匹配。
解决方案:确保数据库中存储的是加密后的密码。可以用PasswordEncoder的encode()方法生成加密密码,再存入数据库:
public static void main(String[] args) {
PasswordEncoder encoder = new BCryptPasswordEncoder();
String encodedPassword = encoder.encode("123456"); // 加密明文密码“123456”
System.out.println(encodedPassword); // 输出加密后的密码,如$2a$10$...
}
坑 2:CSRF 保护导致接口无法访问
问题表现:前后端分离项目中,发送 POST 请求时,提示 “403 Forbidden”,且响应信息包含 “CSRF token”。
缘由:Spring Security 默认开启 CSRF 保护,会要求请求中携带 CSRF Token,而前后端分离项目中,前端一般不会处理这个 Token。
解决方案:前后端分离项目中,直接关闭 CSRF 保护(如前面的配置中csrf(csrf -> csrf.disable()));若为传统服务器渲染项目(如 JSP),则需要在表单中添加 CSRF Token:
<form action="/login" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
<!-- 其他表单元素 -->
</form>
坑 3:过滤器顺序配置错误
问题表现:自定义的过滤器不生效,或者权限判断逻辑出现异常。
缘由:Spring Security 的过滤器链有严格的执行顺序,若自定义过滤器的位置放错,会导致逻辑混乱。例如,将动态权限过滤器放在
UsernamePasswordAuthenticationFilter之后,会导致权限判断时,用户还未完成认证。
解决方案:牢记核心过滤器的执行顺序(从先到后):
- CsrfFilter(CSRF 保护过滤器)
- UsernamePasswordAuthenticationFilter(用户名密码认证过滤器)
- RememberMeAuthenticationFilter(记住我过滤器)
- FilterSecurityInterceptor(权限判断过滤器)
自定义过滤器时,需根据功能确定位置:列如动态权限过滤器需要在 “认证后、权限判断前” 执行,所以放在
UsernamePasswordAuthenticationFilter之后、FilterSecurityInterceptor之前;而验证码过滤器则需要在
UsernamePasswordAuthenticationFilter之前执行(先验证验证码,再进行账号密码认证)。
坑 4:@PreAuthorize 注解不生效
问题表现:在 Controller 方法上添加了@PreAuthorize(“hasRole(‘ADMIN’)”),但普通用户仍能访问该接口。
缘由:常见有 3 种缘由:
未在 SecurityConfig 上添加@EnableMethodSecurity(prePostEnabled = true)注解,导致方法级授权注解未开启;
注解中的表达式错误,列如角色名大小写不匹配(hasRole(‘admin’)与数据库中的 “ADMIN” 不匹配);
Spring Security 版本问题:Spring Boot 3.x 后,默认使用@EnableMethodSecurity,而 Spring Boot 2.x 使用@
EnableGlobalMethodSecurity,若版本与注解不匹配会导致失效。
解决方案:
确认注解开启:Spring Boot 3.x 用@EnableMethodSecurity(prePostEnabled = true),Spring Boot 2.x 用@
EnableGlobalMethodSecurity(prePostEnabled = true);
检查表达式:角色名严格区分大小写,若用hasRole()需确认数据库中角色是否带 “ROLE_” 前缀;
排除版本冲突:在 pom.xml 中指定 Spring Security 的版本,与 Spring Boot 版本匹配(如 Spring Boot 3.2.x 对应 Spring Security 6.2.x)。
坑 5:跨域请求时认证信息丢失
问题表现:前后端分离项目中,前端发送跨域请求(如 Vue 项目部署在localhost:8080,后端在localhost:8081),登录成功后,后续请求仍提示 “未认证”。
缘由:跨域请求默认不会携带 Cookie,而 Spring Security 的 Session 认证依赖 Cookie 中的 JSESSIONID,导致后续请求无法识别用户身份;若用 Token 认证(如 JWT),则可能是前端未在请求头中携带 Token,或后端未配置跨域允许携带请求头。
解决方案:
若用 Session 认证:
- 后端配置跨域允许携带 Cookie:
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080") // 允许的前端域名
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true) // 允许携带Cookie
.maxAge(3600);
}
};
}
- 前端请求时开启 withCredentials:
// Axios示例
axios.post("http://localhost:8081/api/user/info", {}, {
withCredentials: true // 携带Cookie
});
若用 Token 认证:
- 前端在请求头中携带 Token:
axios.post("http://localhost:8081/api/user/info", {}, {
headers: {
"Authorization": "Bearer " + localStorage.getItem("token") // Bearer + Token格式
}
});
- 后端配置跨域允许请求头:
在addCorsMappings()中添加.allowedHeaders(“Authorization”),允许携带 Authorization 头。
实战扩展:Token 认证(JWT)与记住我功能
前面我们讲的是基于 Session 的认证,但在分布式系统中(如多台后端服务器部署),Session 认证会出现 “Session 共享” 问题,此时更推荐用 Token 认证(如 JWT)。下面我们补充 JWT 认证的实战实现,以及常用的 “记住我” 功能。
5.1 JWT 认证:实现无状态的身份验证
JWT(JSON Web Token)是一种无状态的认证方式,核心是将用户信息加密到 Token 中,后端无需存储 Session,只需验证 Token 的有效性即可。
步骤 1:引入 JWT 依赖
<!-- JJWT(JWT的Java实现库) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
步骤 2:编写 JWT 工具类(生成 Token、验证 Token)
@Component
public class JwtUtils {
// 密钥(实际项目中需放在配置文件,避免硬编码)
@Value("${jwt.secret}")
private String secret;
// Token过期时间(1小时,单位:毫秒)
@Value("${jwt.expiration}")
private long expiration;
// 生成Token
public String generateToken(String username) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(username) // 设置用户名
.setIssuedAt(now) // 签发时间
.setExpiration(expireDate) // 过期时间
.signWith(SignatureAlgorithm.HS512, secret) // 签名算法
.compact();
}
// 从Token中获取用户名
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
// 验证Token是否有效(未过期、签名正确)
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (Exception e) {
// Token过期、签名错误等都会抛出异常
return false;
}
}
}
步骤 3:自定义 JWT 认证过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 1. 从请求头中获取Token
String token = getTokenFromRequest(request);
// 2. 验证Token有效性
if (token != null && jwtUtils.validateToken(token)) {
// 3. 从Token中获取用户名
String username = jwtUtils.getUsernameFromToken(token);
// 4. 加载用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 5. 构建Authentication对象,设置到SecurityContext
Authentication authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// 继续执行过滤器链
filterChain.doFilter(request, response);
}
// 从Authorization头中获取Token(格式:Bearer <token>)
private String getTokenFromRequest(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7); // 截取“Bearer ”后的Token
}
return null;
}
}
步骤 4:更新 SecurityConfig,集成 JWT 过滤器
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
// 省略其他注入...
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
// 关闭Session(JWT是无状态认证,无需Session)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/login").permitAll()
.anyRequest().authenticated()
)
// 登录成功后生成Token并返回
.formLogin(form -> form
.loginProcessingUrl("/api/login")
.successHandler((request, response, authentication) -> {
response.setContentType("application/json;charset=utf-8");
Map<String, Object> result = new HashMap<>();
String username = authentication.getName();
String token = jwtUtils.generateToken(username); // 生成JWT Token
result.put("code", 200);
result.put("message", "登录成功");
result.put("data", token); // 返回Token给前端
response.getWriter().write(new ObjectMapper().writeValueAsString(result));
})
.failureHandler(/* 省略失败处理,同之前 */)
)
// 添加JWT过滤器(在用户名密码过滤器之前执行)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
步骤 5:测试 JWT 认证
- 发送 POST 请求到/api/login,获取返回的 Token(如eyJhbGciOiJIUzUxMiJ9…);
- 发送后续请求(如/api/user/info)时,在请求头中添加Authorization: Bearer eyJhbGciOiJIUzUxMiJ9…;
- 后端会通过 JWT 过滤器验证 Token,若有效则允许访问接口。
5.2 “记住我” 功能:实现自动登录
“记住我” 功能允许用户在关闭浏览器后,再次访问时无需重新登录。Spring Security 默认基于数据库存储 “记住我” 令牌,实现步骤如下:
步骤 1:创建 “记住我” 令牌表(MySQL)
CREATE TABLE persistent_logins (
username VARCHAR(64) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL
);
步骤 2:配置 “记住我” 功能
在 SecurityConfig 中添加rememberMe()配置:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 省略其他配置...
.rememberMe(rememberMe -> rememberMe
.tokenRepository(jdbcTokenRepository()) // 基于数据库存储令牌
.tokenValiditySeconds(60 * 60 * 24 * 7) // 令牌有效期(7天)
.userDetailsService(userDetailsService) // 加载用户信息
);
return http.build();
}
// 配置JDBC令牌仓库
@Bean
public JdbcTokenRepositoryImpl jdbcTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource); // 注入数据源
// 首次启动时自动创建persistent_logins表(若已手动创建,注释掉这行)
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
步骤 3:前端添加 “记住我” 复选框
在登录表单中添加复选框,参数名固定为remember-me:
<form action="/api/login" method="post">
<input type="text" name="username" placeholder="用户名">
<input type="password" name="password" placeholder="密码">
<input type="checkbox" name="remember-me" value="true"> 记住我
<button type="submit">登录</button>
</form>
若为前后端分离项目,前端需在登录请求中携带remember-me参数(布尔值)。
步骤 4:测试 “记住我” 功能
- 勾选 “记住我” 并登录;
- 关闭浏览器,再次打开并访问需要认证的接口(如/api/user/info);
- 无需重新登录即可正常访问,说明 “记住我” 功能生效。
总结:Spring Security 认证与授权核心流程
看到这里,信任你已经对 Spring Security 的认证与授权有了清晰的理解。最后我们用一张流程图,总结核心逻辑:
认证流程:
前端传入认证信息(用户名密码 / Token)→ 后端过滤器(如
UsernamePasswordAuthenticationFilter/JwtAuthenticationFilter)接收请求 → 调用 AuthenticationManager 执行认证 → 通过 UserDetailsService 加载用户信息 → 比对认证信息(密码 / Token 有效性)→ 认证成功后,将 Authentication 对象存入 SecurityContext。
授权流程:
请求到达 FilterSecurityInterceptor → 从 SecurityContext 获取 Authentication 对象 → 调用 AccessDecisionManager 判断用户权限是否满足接口要求 → 权限足够则允许访问,不足则返回 403。
掌握这套流程后,无论遇到简单的单体项目,还是复杂的分布式系统,你都能快速搭建安全可靠的接口防护体系。如果在实际开发中遇到具体问题,欢迎在评论区留言讨论,我们一起解决!



向你学习👍
收藏了,感谢分享
请受我一拜👍