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. 解释 AuthenticationSecurityContextSecurityContextHolderUserDetailsServiceAuthenticationProvider 的关系。
  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、统一异常、审计日志和数据权限。