基于 spring security 6.5.8 认证与授权全流程实战(前后端分离项目)
前言
写下这篇博客,核心目的是为自己记录 Spring Security 6.x 认证与授权的学习过程、梳理核心知识点,同时作为后续复盘回顾的笔记。作为 Spring Boot 开发者,之前被 Spring Security 的各类组件、配置逻辑搞得一头雾水,踩了不少入门坑,所以决定把整个学习过程、可落地的实战步骤,一步步整理清楚——既方便自己以后忘记时快速回顾、查漏补缺,也希望如果有和我一样刚入门的小伙伴看到,能少走一些弯路。
本文不堆砌复杂的底层原理,只聚焦“怎么学、怎么用、怎么落地”,用最通俗的语言讲清认证(登录)和授权(权限控制)的全流程,每一步都附上可直接运行的代码和测试场景,既是我的学习总结,也是一份可复用的复盘笔记,后续再接触相关需求时,能快速唤醒记忆、高效复用知识点。
一 先搞懂2个核心概念(大白话版)
在动手前,先明确两个词,不然越学越懵:
- **认证(Authentication)**:验证“你是谁”——比如登录时输入用户名密码,系统确认你是合法用户。
- **授权(Authorization)**:验证“你能做什么”——比如普通用户只能看数据,管理员能删数据,这就是权限控制。
Spring Security 6.x 就是帮我们把这两件事“标准化”实现的框架,不用自己重复造轮子写登录和权限。
二 实战准备:环境与依赖
2.1 环境要求
- JDK 17+(Spring Boot 3.x/Security 6.x 最低要求)
- Spring Boot 3.x(和 Security 6.x 配套)
- IDE(IntelliJ IDEA/Eclipse 都行)
ps: 作者在本文用Spring Boot 3.5.11(Security 6.5.8)
2.2 pom文件依赖(Maven)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.28</version>
</dependency>
三 实现“认证”——让系统认识“谁在登录”
认证的核心是:用户输入用户名密码 → 系统查数据库→ 验证密码是否正确 → 确认用户身份
3.1 自定义“用户信息加载器”(UserDetailsService)
这是认证的核心:告诉 Spring Security “去哪里找用户信息”,本文默认使用密码为123456(输入值的为加密密文)
public UserDetailsService userDetailsService() {
return username -> {
//设置默认密码为123456,实际项目中可通过数据库查询
return User
//用户名
.withUsername(username)
//密码默认123456(此处设置的是BCryptPasswordEncoder加密过的密码)
.password("$2a$10$ceN4C9rbaETneDRoxYjYaeLtm55WMZBGp2qOnAtvHB81QLkTsjiKa")
//默认用户存在test1权限
.authorities("test1").build();
};
}
3.2 配置AuthenticationManager 与 AuthenticationProvider
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(userDetailsService());
daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
daoAuthenticationProvider.setForcePrincipalAsString(true);
return new ProviderManager(daoAuthenticationProvider);
}
AuthenticationManager 是认证的“总入口”,负责统筹认证逻辑;AuthenticationProvider 是“实际执行者”,真正完成用户名密码的校验,二者是“管理者与执行者”的关系,一个 AuthenticationManager 可对应多个 AuthenticationProvider(本文仅配置一个,默认的账号密码登录),密码加密使用BCryptPasswordEncoder
3.3 配置 SecurityFilterChain
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, CustomizeContextRepository customizeContextRepository) throws Exception {
//根据配置获取无取授权的路径
String[] patterns = {"login"};
//关闭csrf(跨站请求伪造),本文是前后端分离项目,通过设置请求头设置自定义token,靠token保证安全,所以不需要启用csrf防护
http.csrf(AbstractHttpConfigurer::disable)
// 由于本文采用的是自定义登录登出接,因此关闭Spring Security 自带的登录登出功能
// 关闭 Spring Security 自带的 HTTP Basic 认证方式
.httpBasic(AbstractHttpConfigurer::disable)
// 关闭 Spring Security 自带的表单登录
.formLogin(AbstractHttpConfigurer::disable)
// 关闭 Spring Security 自带的登出
.logout(AbstractHttpConfigurer::disable)
// 配置需要放行的路径与需要认证的路径
.authorizeHttpRequests(authorize ->
// 配置公开的路径
authorize.requestMatchers(patterns).permitAll()
// 其他所有请求都要求认证
.anyRequest().authenticated())
// 配置token与用户信息存储的方式( CustomizeContextRepository 是上文中提到的自定义的类)
.securityContext(configurer -> configurer.securityContextRepository(customizeContextRepository)).exceptionHandling(httpSecurityExceptionHandlingConfigurer -> {
//配置认证失败返回结果
AuthenticationEntryPoint authenticationEntryPoint = (request, response, authException) -> {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
JSONObject jsonObject = new JSONObject();
jsonObject.set("code", "A0001");
jsonObject.set("message", "用户未认证");
PrintWriter out = response.getWriter();
out.print(JSONUtil.toJsonStr(jsonObject));
out.flush();
};
httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(authenticationEntryPoint);
//配置权限不足返回结果
AccessDeniedHandler accessDeniedHandler = (request, response, authException) -> {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
JSONObject jsonObject = new JSONObject();
jsonObject.set("code", "A0002");
jsonObject.set("message", "资源无访问权限");
PrintWriter out = response.getWriter();
out.print(JSONUtil.toJsonStr(jsonObject));
out.flush();
};
httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler);
});
return http.build();
}
3.4 配置SecurityContextRepository
package cn.lingna.demo.security.configuration;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* @Author chenzhaoming
* @Date 2026/2/4
* @Description 自定义安全上下文存储仓库,实际项目可以通过redis等进行持久化
*/
@Slf4j
@Component
@ConditionalOnClass(SecurityContextRepository.class)
public class CustomizeContextRepository implements SecurityContextRepository {
private final ConcurrentHashMap<String,SecurityContext> securityContextMap = new ConcurrentHashMap<>();
public CustomizeContextRepository() {
}
/**
* 获取SecurityContext
*
* @param requestResponseHolder 请求响应
* @return SecurityContext
*/
@Deprecated
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
HttpServletRequest request = requestResponseHolder.getRequest();
String token = extractToken(request);
if (StringUtils.hasText(token) && securityContextMap.containsKey(token)) {
securityContext = securityContextMap.get(token);
}
return securityContext;
}
/**
* 保存SecurityContext
*
* @param context SecurityContext
* @param request 请求
* @param response 响应
*/
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
String token = extractToken(response);
securityContextMap.put(token,context);
}
/**
* 是否包含SecurityContext
*
* @param request 请求
* @return true/false
*/
@Override
public boolean containsContext(HttpServletRequest request) {
String token = extractToken(request);
return StringUtils.hasText(token) && securityContextMap.containsKey(token);
}
/**
* 从请求中提取token
*/
private String extractToken(HttpServletRequest request) {
// 1. 从Header中获取
String token = request.getHeader("Authorization");
if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
return token.substring(7);
}
return token;
}
/**
* 从响应中提取token
*/
private String extractToken(HttpServletResponse response) {
// 1. 从Header中获取
String token = response.getHeader("Authorization");
if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
return token.substring(7);
}
return token;
}
/**
* 清除SecurityContext
*
* @param request 响应
*/
public void clearContext(HttpServletRequest request){
String token = extractToken(request);
if (StringUtils.hasText(token)) {
securityContextMap.remove(token);
}
}
}
核心总结
3.4.1. 类的核心价值
- 替代 Spring Security 默认的
HttpSessionSecurityContextRepository,将用户认证信息(SecurityContext)保存到内存,实际项目可以使用redis等永久化存储安全上下文,解决分布式系统(多服务实例)的 Session 共享问题; - 基于 Token 实现无状态认证,适配前后端分离场景。
3.4.2. 核心方法逻辑
| 方法名 | 核心作用 |
|---|---|
loadContext |
请求时从SecurityContextRepository 加载用户认证信息,供框架授权使用 |
saveContext |
登录成功后将认证信息存入SecurityContextRepository, |
containsContext |
快速判断请求是否已认证 |
extractToken |
解析 Bearer Token 格式的凭证,截取有效部分 |
clearContext |
登出时删除 SecurityContextRepository中的认证信息,让 Token 失效 |
3.5 实现自定义登录、注销,
3.5.1 自定义登录
@PostMapping("/login")
public String login(@RequestBody LoginParam loginParam,
HttpServletRequest request,
HttpServletResponse response) {
try {
// 构建 未认证的 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authenticationToken =
UsernamePasswordAuthenticationToken.unauthenticated(loginParam.getUsername(), loginParam.getPassword());
//调用 authenticationManager.authenticate进行认证
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) authenticationManager.authenticate(authenticationToken);
// 创建SecurityContext并设置认证信息
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
// 创建随机token
String token = IdUtil.fastSimpleUUID();
// 设置到响应头
response.addHeader("Authorization", token);
// 保存到自定义的 安全上下文仓库,
customizeContextRepository.saveContext(securityContext, request, response);
return token;
} catch (Exception e) {
log.error("登录失败: {}", loginParam.getUsername(), e);
return "登录失败";
}
}
登录完整流程

3.5.2 自定义注销
/**
* 登出
* @param request http请求内容
*/
@RequestMapping("logout")
public String logout(HttpServletRequest request){
// 清楚保存的上下文
customizeContextRepository.clearContext(request);
return "注销成功";
}
登出的核心就是 “删除 Redis 中的 Token”,无需复杂操作,
3.5.3 添加需要权限可访问接口的
/**
* 需要test1权限才可访问的接口
* @PreAuthorize:方法执行前校验权限,支持SpEL表达式
*/
@GetMapping("/test1")
@PreAuthorize("hasAuthority('test1')")
public String test1() {
return "test1";
}
/**
* 需要test2权限才可访问的接口
* @PreAuthorize:方法执行前校验权限,支持SpEL表达式
*/
@GetMapping("/test2")
@PreAuthorize("hasAuthority('test2')")
public String test2() {
return "test2";
}
四 接口验证
4.1 登录测试
输入username : test password:123 由于密码错误,返回认证失败

输入username : test password:123456 登录正确,登录成功返回token

4.2 测试访问权限为test1的接口
帐号有test1权限,正常返回test1

4.3 测试访问权限为test2的接口
帐号没有test2的访问权限,test2接口抛出了异常,返回了“资源无访问权限”。

4.4 注销测试
接口返回 “注销成功”,用户已正常退出

注销之后再次访问test1接口,提示用户未认证,说明已经注销成功

五 总结
本文围绕 Spring Security 6.x 实现了前后端分离场景下的认证与授权全流程,核心落地思路和关键要点可总结为以下 3 点:
1. 认证核心:基于 Token 实现无状态登录
- 核心逻辑:通过自定义
UserDetailsService加载用户信息,AuthenticationManager统筹认证流程,校验用户名密码合法性;登录成功后生成随机 Token,将认证信息(SecurityContext)存入自定义的CustomizeContextRepository(内存版,生产可替换为 Redis),替代默认的 Session 存储,实现无状态认证; - 关键适配:关闭 Spring Security 自带的表单登录、HTTP Basic 认证等,适配前后端分离场景,通过请求头传递 Token 完成身份校验,登出仅需删除 Token 对应的认证信息即可失效。
2. 授权核心:方法级权限控制 + 异常统一处理
- 权限校验:基于
@PreAuthorize注解(需开启@EnableMethodSecurity)实现细粒度的接口权限控制,通过hasAuthority()校验用户是否具备指定权限; - 异常兜底:自定义
AuthenticationEntryPoint(未认证)和AccessDeniedHandler(权限不足),统一返回标准化的 JSON 提示,替代框架默认的异常页面,提升前后端交互体验。
3. 实战落地关键:简化核心、适配场景
- 环境适配:JDK 17+ 适配 Spring Boot 3.x/Security 6.x 版本要求,关闭 CSRF 防护(前后端分离场景通过 Token 保障安全);
- 可拓展性:核心组件(用户信息加载、上下文存储)均为自定义实现,可快速替换为数据库查用户、Redis 存 Token 等生产级方案,无需大幅修改核心逻辑;
- 流程闭环:从登录认证→权限校验→登出失效,形成完整的用户身份管控流程,测试覆盖了密码错误、权限不足、登出失效等核心场景,确保功能可用。
整体而言,本文避开了复杂的底层原理,聚焦 “落地可用”,通过极简的配置和自定义组件,快速实现了 Spring Security 6.x 在前后端分离项目中的核心能力,既适合新手入门理解认证授权的核心逻辑,也可作为中小型项目的实战模板直接复用。
六 相关参考资料
2. 本文demo配套源码地址:https://gitee.com/chenzhaom/spring-security-demo
https://gitee.com/chenzhaom/spring-security-demo
3. SpringSecurity实现核心流程原理:
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)