前言

写下这篇博客,核心目的是为自己记录 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 在前后端分离项目中的核心能力,既适合新手入门理解认证授权的核心逻辑,也可作为中小型项目的实战模板直接复用。

六 相关参考资料

1. 官方技术文档:https://docs.spring.io/spring-security/reference/6.5/index.htmlhttps://docs.spring.io/spring-security/reference/6.5/index.html

2. 本文demo配套源码地址:https://gitee.com/chenzhaom/spring-security-demohttps://gitee.com/chenzhaom/spring-security-demo

3. SpringSecurity实现核心流程原理:

https://blog.csdn.net/chenzhaom/article/details/158889883?sharetype=blogdetail&sharerId=158889883&sharerefer=PC&sharesource=chenzhaom&spm=1011.2480.3001.8118https://blog.csdn.net/chenzhaom/article/details/158889883?sharetype=blogdetail&sharerId=158889883&sharerefer=PC&sharesource=chenzhaom&spm=1011.2480.3001.8118

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐