Skip to content

Spring Security 从零到生产级掌握 ​

Spring Security 不能只学成“加个依赖就有登录页”或者“写个 JWT 过滤器”。真正的后台系统安全要能解释:请求为什么先进过滤器链,认证对象怎么流转,SecurityContext 保存在哪里,401 和 403 为什么不同,Session 和 JWT 怎么选,权限注解为什么不能解决数据权限,CSRF 和 CORS 分别防什么,Token 泄露、密码泄露、越权访问怎么处理。

一句话建立主线:

Spring Security 是基于 Servlet Filter 链实现的认证、授权、会话、安全上下文和 Web 攻击防护框架。

如果你想检查自己是否真的从零基础学懂到能面试、能落地、能排查,按 Spring Security 从零到精通验收清单 逐项验收。

学习目标 ​

学完这一页,你要能做到:

  1. 解释 Spring Security 为什么把核心逻辑放在过滤器链里。
  2. 解释认证和授权的区别,以及 401、403 的区别。
  3. 解释 Authentication、SecurityContext、SecurityContextHolder、UserDetailsService、AuthenticationProvider 的关系。
  4. 解释账号密码登录、Session 登录、JWT 登录的完整流程。
  5. 解释 JWT 是签名 Token,不是默认加密,为什么不能放敏感数据。
  6. 解释 URL 权限、方法权限、角色权限、权限码和数据权限怎么设计。
  7. 解释 CSRF、CORS、XSS、Token 泄露和密码哈希。
  8. 解释认证异常、授权异常如何统一返回 JSON。
  9. 设计一个后台管理系统的用户、角色、权限、部门、租户、数据范围模型。
  10. 排查登录失败、Token 无效、403、跨域、权限不生效等生产问题。

学习路线 ​

mermaid
flowchart TD
    A["JavaEE Filter 基础"] --> B["Security Filter Chain"]
    B --> C["认证<br/>你是谁"]
    C --> D["SecurityContext<br/>保存当前用户"]
    D --> E["授权<br/>你能访问什么"]
    E --> F["Session/JWT/OAuth2"]
    F --> G["CSRF/CORS/Web安全"]
    G --> H["权限模型和数据权限"]
    H --> I["异常处理和生产排查"]

学 Spring Security 前,最好先理解 JavaEE Filter。Spring Security 的本质就是一组有顺序的 Filter。

第一步:为什么核心是过滤器链 ​

安全逻辑必须发生在 Controller 之前。

如果请求已经进入 Controller 才判断是否登录,很多统一问题会变复杂:

  • 未登录请求可能已经触发业务代码。
  • 权限拒绝无法统一处理。
  • Token 解析散落在各个接口。
  • 日志、审计、上下文设置重复。

过滤器链可以在请求入口统一处理:

mermaid
flowchart TD
    A["HTTP 请求"] --> B["Spring Security Filter Chain"]
    B --> C["解析凭证<br/>Session/JWT/Basic"]
    C --> D["认证用户"]
    D --> E["写入 SecurityContext"]
    E --> F["授权判断"]
    F --> G["进入 Controller"]
    F --> H["拒绝并返回 401/403"]

Spring Security 不是一个 Filter,而是一组 Filter。不同 Filter 负责不同安全步骤。

常见职责:

职责说明
加载安全上下文从 Session 或请求中恢复用户
认证解析用户名密码、Token、Basic 等
异常处理把认证失败、权限不足转成响应
授权判断当前用户是否能访问资源
登出清理上下文、Session、Cookie
CSRF校验防伪 Token

第二步:认证和授权 ​

认证 Authentication:确认你是谁。

授权 Authorization:确认你能做什么。

阶段问题例子
认证用户身份是否可信账号密码正确、JWT 签名有效
授权是否有访问权限是否有 asset:read 权限
数据权限是否能访问这条数据只能看本医院、本部门资产

401 和 403:

状态码含义场景
401未认证或认证失效没带 Token、Token 过期
403已认证但权限不足已登录但没有 admin 权限

前端看到 401 通常跳登录或刷新 Token;看到 403 通常提示无权限。

第三步:核心对象关系 ​

mermaid
flowchart TD
    A["请求凭证<br/>用户名密码/JWT"] --> B["AuthenticationFilter"]
    B --> C["AuthenticationManager"]
    C --> D["AuthenticationProvider"]
    D --> E["UserDetailsService"]
    E --> F["UserDetails<br/>用户、密码、权限"]
    D --> G["PasswordEncoder 或 Token 校验"]
    G --> H["认证成功 Authentication"]
    H --> I["SecurityContext"]
    I --> J["SecurityContextHolder"]

核心对象:

对象作用
Authentication表示认证信息,包含 principal、credentials、authorities、authenticated
SecurityContext保存当前请求的 Authentication
SecurityContextHolder当前线程获取 SecurityContext 的工具
UserDetailsService根据用户名加载用户信息
UserDetailsSpring Security 识别的用户模型
AuthenticationManager认证入口调度器
AuthenticationProvider具体认证逻辑
PasswordEncoder密码哈希和校验

为什么 SecurityContextHolder 通常和线程有关:

Web 请求通常由一个线程处理,Spring Security 会把当前用户放到当前线程上下文里,业务代码才能这样获取:

java
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

注意:如果开启异步线程,新线程默认拿不到原线程的安全上下文,需要显式传递或重新认证。

第四步:账号密码登录流程 ​

典型流程:

mermaid
flowchart TD
    A["用户提交用户名密码"] --> B["认证过滤器读取参数"]
    B --> C["构造未认证 Authentication"]
    C --> D["AuthenticationManager"]
    D --> E["DaoAuthenticationProvider"]
    E --> F["UserDetailsService 查用户"]
    F --> G["PasswordEncoder.matches 校验密码"]
    G --> H{"是否成功"}
    H -- "成功" --> I["生成已认证 Authentication"]
    I --> J["写入 SecurityContext"]
    J --> K["返回登录成功"]
    H -- "失败" --> L["AuthenticationFailureHandler"]

用户查询 Demo:

java
@Service
public class CustomUserDetailsService implements UserDetailsService {
    private final UserMapper userMapper;

    public CustomUserDetailsService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public UserDetails loadUserByUsername(String username) {
        UserAccount account = userMapper.findByUsername(username);
        if (account == null) {
            throw new UsernameNotFoundException("user not found");
        }

        return User.withUsername(account.getUsername())
                .password(account.getPasswordHash())
                .authorities(account.getPermissions().toArray(String[]::new))
                .disabled(!account.isEnabled())
                .build();
    }
}

密码必须用安全哈希:

java
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

保存密码:

java
String hash = passwordEncoder.encode(rawPassword);

校验密码:

java
boolean ok = passwordEncoder.matches(rawPassword, hash);

密码哈希不是加密。加密通常可以解密,哈希不可逆。BCrypt 会加盐并故意变慢,降低撞库和暴力破解风险。

第五步:Session 登录 ​

Session 登录适合传统后台、服务端渲染或同域管理系统。

流程:

mermaid
flowchart TD
    A["登录成功"] --> B["SecurityContext 写入 Session"]
    B --> C["响应 Set-Cookie JSESSIONID"]
    C --> D["浏览器保存 Cookie"]
    D --> E["后续请求带 JSESSIONID"]
    E --> F["SecurityContextPersistenceFilter 恢复上下文"]
    F --> G["当前请求识别为已登录"]

优点:

  • 服务端可主动失效 Session。
  • 权限变更相对容易生效。
  • 适合后台管理系统。

缺点:

  • 多实例要共享 Session。
  • 移动端和跨域场景要处理 Cookie。
  • 服务端保存状态。

多实例方案:

方案说明
Redis Session常见,使用 Spring Session
粘性会话简单但实例故障体验差
Session 复制集群小可以用
改 JWT服务端无状态,但撤销复杂

第六步:JWT 登录 ​

JWT 常用于前后端分离、移动端、网关统一认证。

JWT 结构:

text
header.payload.signature

它默认是签名,不是加密。payload 是 Base64Url 编码,拿到 Token 的人可以解码看到内容,所以不要放手机号、身份证、密码、敏感权限等明文敏感信息。

JWT 登录流程:

mermaid
flowchart TD
    A["用户登录成功"] --> B["服务端生成 JWT"]
    B --> C["返回 access_token"]
    C --> D["前端保存 Token"]
    D --> E["请求头 Authorization: Bearer token"]
    E --> F["JWT Filter 解析和验签"]
    F --> G{"是否有效"}
    G -- "有效" --> H["构造 Authentication"]
    H --> I["写入 SecurityContext"]
    G -- "无效" --> J["返回 401"]

JWT Filter 简化 Demo:

java
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtService jwtService;

    public JwtAuthenticationFilter(JwtService jwtService) {
        this.jwtService = jwtService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String token = header.substring(7);
        JwtUser user = jwtService.parseAndVerify(token);

        Authentication authentication =
                new UsernamePasswordAuthenticationToken(
                        user.userId(),
                        null,
                        user.authorities()
                );
        SecurityContextHolder.getContext().setAuthentication(authentication);

        filterChain.doFilter(request, response);
    }
}

JWT 生产问题:

问题处理
Token 泄露HTTPS、短有效期、刷新 Token、设备管理
无法主动退出Redis 黑名单、Token 版本号、短 access token
权限变更不生效Token 短有效期、每次查权限版本
payload 太大只放 userId、tenantId、少量声明
前端存储风险避免 XSS,谨慎 localStorage

第七步:OAuth2 和单点登录思路 ​

OAuth2 解决的是授权委托,不是简单的“登录协议”。OIDC 在 OAuth2 之上补充身份认证。

常见场景:

场景说明
企业统一登录接入统一身份平台
第三方登录微信、GitHub、企业微信
开放平台授权用户授权第三方应用访问资源

授权码模式简化流程:

mermaid
flowchart TD
    A["用户访问业务系统"] --> B["跳转认证中心"]
    B --> C["用户登录并授权"]
    C --> D["认证中心返回 code"]
    D --> E["业务后端用 code 换 token"]
    E --> F["拉取用户信息或校验 id_token"]
    F --> G["建立本系统登录态"]

商业系统里,接 OAuth2/OIDC 时要关注:

  • 回调地址白名单。
  • state 防 CSRF。
  • token 保存和刷新。
  • 用户绑定关系。
  • 退出登录联动。
  • 权限从统一身份平台同步还是本系统维护。

第八步:授权和权限模型 ​

授权要分层理解:

层次解决什么例子
URL 权限能不能访问接口/admin/** 需要 admin
方法权限能不能调用方法@PreAuthorize
按钮权限前端是否显示操作asset:create
数据权限能不能看这条数据只能看本部门资产

权限码建议稳定命名:

text
asset:read
asset:create
asset:update
asset:delete
collect:task:start
collect:task:stop

典型 RBAC 模型:

mermaid
flowchart TD
    A["用户 User"] --> B["用户角色 UserRole"]
    B --> C["角色 Role"]
    C --> D["角色权限 RolePermission"]
    D --> E["权限 Permission"]

数据库表:

表作用
sys_user用户
sys_role角色
sys_permission权限码和资源
sys_user_role用户和角色关系
sys_role_permission角色和权限关系
sys_department部门组织
sys_user_data_scope用户或角色的数据范围

方法权限示例:

java
@PreAuthorize("hasAuthority('asset:create')")
@PostMapping("/assets")
public AssetVO create(@RequestBody AssetCreateRequest request) {
    return assetService.create(request);
}

只靠注解不够,因为它只能判断“能不能调用创建资产接口”,不能判断“能不能操作这条资产数据”。

数据权限要结合业务字段:

java
public AssetVO getAsset(Long assetId, CurrentUser user) {
    Asset asset = assetRepository.findById(assetId);
    if (!dataScopeService.canReadAsset(user, asset)) {
        throw new AccessDeniedException("no permission to read asset");
    }
    return AssetVO.from(asset);
}

第九步:CSRF、CORS、XSS ​

CSRF ​

CSRF 是跨站请求伪造。攻击者诱导用户访问恶意页面,浏览器自动携带目标站点 Cookie 发请求。

mermaid
flowchart TD
    A["用户已登录系统"] --> B["浏览器保存 Cookie"]
    C["恶意页面"] --> D["发起到系统的请求"]
    D --> E["浏览器自动带 Cookie"]
    E --> F["服务端误以为是用户操作"]

防护:

  • CSRF Token。
  • SameSite Cookie。
  • 关键操作二次确认。
  • 前后端分离 Token 放 Authorization Header 时,CSRF 风险变化,但仍要防 XSS 和 CORS 过宽。

CORS ​

CORS 是浏览器跨域访问控制。它不是服务端之间调用限制,而是浏览器安全策略。

危险配置:

text
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

带凭证时不能随便放开所有来源。

XSS ​

XSS 是脚本注入。攻击者让页面执行恶意 JS,可能窃取 Token、Cookie 或发起操作。

防护:

  • 输出转义。
  • 富文本白名单过滤。
  • Cookie 设置 HttpOnly。
  • Content Security Policy。
  • 前端不要把不可信 HTML 直接渲染。

第十步:统一异常处理 ​

认证失败和权限不足要区分。

异常含义响应
AuthenticationException未登录或认证失败401
AccessDeniedException已登录但权限不足403

配置 Demo:

java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable())
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/login").permitAll()
            .anyRequest().authenticated()
        )
        .exceptionHandling(ex -> ex
            .authenticationEntryPoint((request, response, e) -> {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}");
            })
            .accessDeniedHandler((request, response, e) -> {
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write("{\"code\":403,\"message\":\"无权限\"}");
            })
        );
    return http.build();
}

前后端分离项目不要使用默认登录页跳转,要统一返回 JSON。

第十一步:商业权限系统设计 ​

以医疗数据采集与资产平台为例:

mermaid
flowchart TD
    A["用户"] --> B["角色"]
    B --> C["菜单和按钮权限"]
    B --> D["接口权限"]
    A --> E["部门/医院/租户"]
    E --> F["数据范围"]
    F --> G["资产、采集任务、接口配置"]

权限设计不要只停在角色:

权限类型例子
菜单权限是否能看到“资产管理”菜单
按钮权限是否能点击“新增资产”
接口权限是否能调用 POST /assets
数据权限是否能看某医院资产
字段权限是否能看敏感字段
操作审计谁在什么时候做了什么

数据权限常见维度:

  • 租户。
  • 医院。
  • 部门。
  • 项目。
  • 数据密级。
  • 创建人。
  • 负责范围。

SQL 层面常见做法:

sql
select *
from asset
where hospital_id in (:allowedHospitalIds)
  and deleted = 0

但不要只靠前端隐藏按钮。前端隐藏只能改善体验,真正安全必须由后端校验。

第十二步:生产排查 ​

登录失败 ​

mermaid
flowchart TD
    A["登录失败"] --> B["用户名是否存在"]
    B --> C["用户是否禁用/锁定"]
    C --> D["密码是否 BCrypt matches"]
    D --> E["UserDetailsService 是否返回权限"]
    E --> F["失败处理器返回什么"]

JWT 无效 ​

mermaid
flowchart TD
    A["JWT 无效"] --> B["请求头是否带 Bearer"]
    B --> C["签名密钥是否正确"]
    C --> D["Token 是否过期"]
    D --> E["issuer/audience 是否匹配"]
    E --> F["是否在黑名单"]
    F --> G["过滤器顺序是否正确"]

明明有权限却 403 ​

mermaid
flowchart TD
    A["出现 403"] --> B["是否已认证成功"]
    B --> C["SecurityContext 是否有 Authentication"]
    C --> D["authorities 是否包含权限码"]
    D --> E["hasRole 和 hasAuthority 是否混用"]
    E --> F["方法权限是否开启"]
    F --> G["数据权限是否拒绝"]

跨域失败 ​

mermaid
flowchart TD
    A["跨域失败"] --> B["浏览器控制台看 CORS 报错"]
    B --> C["OPTIONS 预检是否放行"]
    C --> D["Allow-Origin 是否匹配"]
    D --> E["是否带 Credentials"]
    E --> F["Header 和 Method 是否允许"]

常见坑 ​

坑后果正确做法
密码用 MD5容易被撞库BCrypt/Argon2/PBKDF2
JWT 放敏感信息Token 泄露后可直接读payload 只放必要声明
Token 永不过期泄露后长期有效短 access token + refresh
只做前端按钮控制后端接口可被绕过后端必须鉴权
只做接口权限越权访问别人的数据业务层做数据权限
CORS 全放开跨站风险扩大白名单来源
过滤器顺序错Token 不生效或异常不统一明确 addFilterBefore/After
异步线程取不到用户SecurityContext 在线程间不自动传显式传递或重新获取

最小完整 Demo:JWT 前后端分离骨架 ​

配置:

java
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
    private final JwtAuthenticationFilter jwtFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtFilter) {
        this.jwtFilter = jwtFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
            .sessionManagement(session ->
                    session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/login").permitAll()
                    .anyRequest().authenticated())
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

登录接口:

java
@RestController
public class LoginController {
    private final AuthenticationManager authenticationManager;
    private final JwtService jwtService;

    public LoginController(AuthenticationManager authenticationManager, JwtService jwtService) {
        this.authenticationManager = authenticationManager;
        this.jwtService = jwtService;
    }

    @PostMapping("/login")
    public Map<String, String> login(@RequestBody LoginRequest request) {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        request.username(),
                        request.password()
                )
        );
        String token = jwtService.createToken(authentication);
        return Map.of("accessToken", token);
    }
}

权限接口:

java
@PreAuthorize("hasAuthority('asset:read')")
@GetMapping("/assets/{id}")
public AssetVO getAsset(@PathVariable Long id) {
    return assetService.getAsset(id);
}

数据权限仍要在 assetService.getAsset 中检查资源归属。

面试标准回答 ​

Spring Security 请求链路怎么说 ​

请求进入应用后先经过 Spring Security Filter Chain。认证过滤器会解析 Session、JWT 或用户名密码等凭证,认证成功后构造 Authentication 并放入 SecurityContext。授权阶段根据 URL、方法注解或权限规则判断是否能访问资源。未认证返回 401,已认证但权限不足返回 403。业务数据权限还需要在 Service 层结合租户、部门、资源归属继续校验。

JWT 和 Session 怎么选 ​

Session 状态保存在服务端,适合传统后台和需要服务端主动控制会话的场景;JWT 状态主要在客户端 Token 中,适合前后端分离、移动端和网关统一认证。JWT 扩展性好但撤销、权限变更实时生效和泄露处理更复杂,通常要短有效期、refresh token、黑名单或 tokenVersion 机制。

为什么权限注解不能解决所有权限 ​

权限注解只能判断当前用户是否能调用某个接口或方法,例如有没有 asset:read。但它不知道这条资产属于哪个医院、部门或租户。商业系统必须在业务层或数据访问层继续做数据权限校验,防止用户越权访问别人的数据。

关联知识点 ​

本章小结 ​

Spring Security 的学习核心是过滤器链。过滤器链负责在 Controller 前完成认证、上下文恢复、授权判断和异常处理。Session、JWT、OAuth2 只是不同凭证和会话方案;角色、权限码、数据范围才是商业系统的权限设计核心。真正上线时,必须同时处理密码安全、Token 风险、CSRF/CORS、统一异常、审计日志和数据权限。