后端安全
一、认证 (Authentication)
安全设计第一原则:不信任任何来自外部的输入(包括 HTTP 请求参数、Header、Cookie、文件内容、第三方 API 返回数据)。所有安全机制都应该基于这个出发点设计——认证确认身份、授权限制操作、防御机制兜底。安全是纵深防御,不存在单点银弹。
认证解决的核心问题是”你是谁”。在分布式系统中,确认身份不仅是用户态的需求,也是服务间通信的前提——mTLS、API Key 本质上也是认证。认证是安全体系的基石,身份确认有误,后续的授权、审计都会失效。
1.1 Session-Cookie 认证
原理:用户登录后,服务端生成 Session 对象存储在内存/Redis 中,将随机 Session ID 通过 Set-Cookie 返回。后续请求浏览器自动携带 Cookie,服务端通过 Session ID 找回会话状态。
1 | 客户端 服务端 |
优点:实现简单、浏览器原生支持(HttpOnly/Secure 增强安全);可随时踢人(删除 Session 令牌即时失效);敏感数据不落客户端,用户无法篡改 Session 数据。
缺点:水平扩展需共享 Session 存储(Redis Cluster),增加依赖和网络开销;跨域场景需 withCredentials + CORS 精确配置,不如 Authorization header 直观;移动端没有浏览器 Cookie 机制需手动管理;CSRF 攻击面——浏览器对目标域名发任何请求都无条件携带 Cookie,攻击者可在第三方站点诱导用户发起恶意请求(防御见 CSRF 章节)。
面试追问——Session 存在 Redis 的优缺点:优点是一台机器重启不丢会话、多台机器共享状态;缺点是每次请求都要网络 IO 查 Redis(延迟增加 1-2ms),Redis 挂了则所有用户登录态丢失。权衡方案:双写——本地内存一级缓存 + Redis 二级存储,读写都经本地缓存,Redis 只用于写入同步。
面试常问——JWT 的 Signature 用对称(HS256)还是非对称(RS256)? HS256 颁发和验证用同一把密钥——适合单服务场景,密钥泄露后攻击者可以自己签发有效 JWT。RS256 用私钥签名、公钥验证——适合微服务场景:认证中心持有私钥签发,各微服务只需持有公钥即可验证。即使某个微服务沦陷,公钥泄露也无法伪造 JWT。生产环境推荐 RS256/ES256(ECDSA),密钥管理更安全。
📖 独立文章:OAuth2 与 SSO
1.4 SSO 单点登录
CAS 协议——最经典的 SSO 协议,基于票据:
1 | 1. 用户访问 appA.com → 无登录态 → 重定向到 cas.com/login?service=appA.com |
关键设计:TGC 是 CAS 自己的 Cookie(只在 cas.com 域),各子系统拿独立一次性 ST,各自维护 Session 互不干扰。避免了跨域 Cookie 难题。
OIDC(OpenID Connect):基于 OAuth 2.0,增加 ID Token(JWT 格式,携带签名身份信息),让 OAuth 2.0 从”授权协议”扩展为”认证协议”。相比 CAS:OIDC 用 JWT 而非不透明 ST,下游服务可离线验证无需每次回调认证中心;生态极强——Google、Azure AD、GitHub、Keycloak 普遍支持;适合现代化的 SPA/移动端/微服务场景;CAS 的 ST 验证需要认证中心在线,压力集中。但 CAS 在企业内部老旧系统中仍有大量部署,理解其票据机制在面试中仍是加分项。
1.5 双因素认证 (TOTP)
密码是”你知道的东西”(Knowledge),TOTP 增加”你拥有的东西”(Possession)。本质是 HMAC + 时间窗口:
1 | TOTP = Truncate(HMAC-SHA-1(secret, floor(timestamp / 30))) % 10^6 |
注册时用户扫二维码拿到 secret(双方共享),每 30s 独立计算同一口令,无需网络通信。验证时允许 ±1 个时间窗口容忍时钟偏差。
面试要点:密码泄露(钓鱼/撞库/脱库)后攻击者可登录——TOTP 即使密码泄露,没有手机也登不上去。TOTP seed(共享密钥)需安全存储,不能与密码放同一数据库——否则数据库脱库一锅端。TOTP 不能防中间人攻击——攻击者搭建钓鱼代理站点,用户输入密码→攻击者转发到真实服务器,返回 TOTP 输入框→用户输 TOTP→攻击者拿到完整的认证凭据。这就是为什么银行/大型企业开始推广 WebAuthn(公私钥非对称认证,密钥不离开硬件,domain 绑定防钓鱼)。
二、授权 (Authorization)
认证确认”你是谁”,授权决定”你能做什么”——通过认证不代表拥有所有权限,二者必须分离。
2.1 RBAC(基于角色的访问控制)
用户(User) ←N:M→ 角色(Role) ←N:M→ 权限(Permission),经典五表设计(sys_user / sys_role / sys_permission / sys_user_role / sys_role_permission)。
为什么引入角色层? 1000 用户 × 20 权限直接维护是 20000 条关系;通过 5 个角色中转,只需 5 个角色的权限配置 + 1000 条用户-角色关系。角色实现批量管理和语义化。
局限:静态模型,无法感知上下文——“部门经理只能看本部门报表””只能改自己创建的单据””工作日 9-18 点操作”这种需求 RBAC 做不到;微服务下角色爆炸。
2.2 ABAC(基于属性的访问控制)
根据主体属性(部门/职级)、资源属性(密级/所有者)、环境属性(时间/IP/设备)、操作类型(读/写/删),动态计算访问策略。
1 | // 策略表达式: "user.department == resource.department && user.level >= resource.secretLevel" |
实际工程以 “RBAC 为主,ABAC 补充“——角色做粗筛,属性做精判。RBAC 适合 ERP/管理后台,ABAC 适合数据平台/API 网关策略引擎。
面试追问——权限数据如何缓存? 用户权限在认证通过时加载一次,放入 SecurityContext 随请求生命周期存在。对于 RBAC,通常是登录时查一次角色+权限列表,后续请求从缓存/ThreadLocal 获取。对于 ABAC,由于涉及实时属性(时间/位置等),不能完全缓存——需要每次请求实时评估策略。大型系统常见做法是 RBAC 结果缓存(几分钟过期),ABAC 每次实时计算。
2.3 Spring Security 核心原理
Spring Security 本质是责任链模式的 Servlet Filter。
Filter Chain:
1 | 请求 → SecurityContextPersistenceFilter → UsernamePasswordAuthFilter |
SecurityContextPersistenceFilter:请求开始从 HttpSession 恢复 SecurityContext,结束持久化回去。UsernamePasswordAuthenticationFilter:拦截/login,调 AuthenticationManager 认证。ExceptionTranslationFilter:AuthenticationException → 302 登录页;AccessDeniedException → 403。FilterSecurityInterceptor:最后一环,根据 URL 模式+角色做访问决策。
SecurityContextHolder——用 ThreadLocal 存当前请求认证信息。请求结束必须清理防线程池内存泄漏:
1 | Authentication auth = SecurityContextHolder.getContext().getAuthentication(); |
@PreAuthorize——方法级权限:
1 |
|
认证流程:UsernamePasswordAuthenticationFilter 拦截 /login POST → 提取 username/password 构造 UsernamePasswordAuthenticationToken(未认证,authenticated=false)→ 调用 AuthenticationManager.authenticate() → 委托给 ProviderManager 遍历 AuthenticationProvider 列表 → DaoAuthenticationProvider 调用 UserDetailsService.loadUserByUsername() 查数据库/远程服务 → 返回 UserDetails → 调用 PasswordEncoder.matches(rawPassword, encodedPassword) 比对密码 → 认证成功,返回新的 Authentication 对象(authenticated=true)→ 存入 SecurityContextHolder → 请求结束时 SecurityContextPersistenceFilter 将 SecurityContext 持久化到 Session。
面试常问:AuthenticationManager、AuthenticationProvider、UserDetailsService 三者的关系?Manager 是入口(门面),内部遍历 Provider 列表直到某个 Provider 能处理(supports(Authentication) 返回 true)。DaoAuthenticationProvider 是其中最常用的 Provider,它委托给 UserDetailsService 获取用户数据再比对凭据。这种分层设计的好处:你可以自定义 Provider 支持短信验证码/微信扫码等认证方式,无需改动框架核心逻辑。
📖 独立文章:常见 Web 漏洞攻防
四、API 安全
4.1 API 签名
HMAC 签名验证请求完整性和调用方身份,不依赖 Cookie/Session。
1 | 签名参数: Method + Path + Timestamp + Nonce + Body |
防重放三要素:Timestamp(5min 过期窗口)→ Nonce(窗口内唯一,Redis SET NX + TTL 去重)→ 签名包含二者(篡改任一 = 签名不匹配)。
1 | public boolean verify(HttpServletRequest request) { |
签名对比必须用恒定时间比较:String.equals() 首字节不同立即返回(耗时短),攻击者通过测量响应时间可逐字节推测正确签名(时序攻击)。MessageDigest.isEqual() 遍历所有字节耗时恒定。
4.2 接口限流
令牌桶——令牌以速率 r 放入容量 b 的桶,每请求消耗 1 令牌。既能限制平均速率,又允许 b 大小突发。
分布式滑动窗口(Redis + Lua):
1 | String lua = """ |
为什么用 Lua? “删除→计数→判断→添加”四步有竞态条件,Lua 在 Redis 服务端原子执行避免分布式超发。
选型:网关层 Nginx/Kong(按 IP/路径粗粒度);应用层 Guava RateLimiter/Sentinel(单机/集群);分布式 Redis 滑动窗口。限流不仅防 DDoS——秒杀瞬时 QPS 从 200 飙升 20000,无限流直接打挂数据库。
4.3 HTTPS 强制
HTTPS = HTTP + TLS,提供加密性(防窃听)、完整性(防篡改)、身份验证(证书证明身份)。配置要点:
1 | server { listen 80; return 301 https://$host$request_uri; } # 强制跳转 |
HSTS:浏览器在有效期内不尝试 HTTP 连接,从源头杜绝 SSL Strip 降级攻击。
证书管理:生产环境使用 Let’s Encrypt(免费 90 天有效期,需自动续期)或商用证书。证书私钥文件的权限必须是 chmod 600,只有 Web 进程可读——私钥泄露 = 任何人都能冒充你的站点。建议使用 cert-manager(K8s)或 acme.sh 自动管理证书生命周期。
面试常问——TLS 握手做了什么? 客户端发 ClientHello(支持的密码套件+随机数)→ 服务端回 ServerHello(选定密码套件+随机数+证书)→ 客户端验证证书→ 生成 Premaster Secret 用服务端公钥加密发送→ 双方用三个随机数生成 Session Key → 后续通信用 Session Key 对称加密。TLS 1.3 缩减为 1-RTT:ClientHello 就带了密钥交换参数,ServerHello 直接完成密钥协商——速度更快且移除了不安全的算法。
4.4 敏感数据脱敏
日志脱敏(最易被忽视的泄露点):
1 | log.info("用户登录: 手机号={}", PhoneUtils.mask(phone)); // 138****1234 |
返回脱敏:Jackson @JsonSerialize(using = PhoneSerializer.class) 序列化时打星。
分层层策略:前端展示脱敏(138****1234)→ 日志脱敏(密码直接不记)→ 数据库哈希/加密存储(AES + KMS 管密钥)。
五、生产安全实践
5.1 密码存储规范
原则:绝不存明文;不用可逆加密(AES 密钥泄露 = 所有密码泄露);用 Bcrypt/Argon2(cost≥10);不自研密码方案;不把密码当 HMAC key 来”加密”自身。
5.2 日志脱敏规范
1 |
|
禁止记录:密码、Token、卡号、身份证、完整手机号/邮箱。必须记录:用户 ID、操作类型、IP、流水号。定期人工审计日志文件。
5.3 最小权限原则
最小权限原则不是安全理念,是需要落地的工程实践。核心逻辑——不是假设代码 100% 无漏洞,而是假设漏洞存在时限制损害范围。
(1)数据库账号分层:
1 | -- 日常业务账号:仅 CRUD,无 DDL 权限 |
即使 SQL 注入成功,攻击者也无法 DROP TABLE/ALTER TABLE 或创建后门用户。这是纵深防御——不要把所有安全希望押在”代码无漏洞”上。
(2)服务账号权限隔离:订单服务账号只能读写 order 表,用户服务只能读写 user 表,报表服务只读所有表(或只读副本)。每个微服务独立数据库账号,一个服务沦陷不会横向扩散到其他服务的数据。
(3)文件系统权限:Web 进程不以 root 运行(用过 ps aux 确认);上传目录 chmod 644(文件)/ chmod 755(目录)无执行权限;包含数据库密码的配置文件 chmod 600,仅应用进程可读。
(4)最小 API 权限:对外 API 只暴露必要的接口,不要把全套管理接口挂上公网。内部微服务间的 API 调用也遵循同样原则——订单服务不需要调用”删除所有用户”的接口,就不要给它这个权限。
5.4 Spring Security 配置速查
1 |
|
JWT 过滤器:
1 |
|
配置要点:JWT Filter 必须放在认证 Filter 之前执行;401 vs 403 在响应中准确区分(401 跳登录,403 跳无权限页);安全响应头(CSP、HSTS、X-Frame-Options)成本极低务必开启。
六、安全 Checklist
认证:Bcrypt/Argon2 cost ≥ 10 | JWT RS256 或 HS256 + 强 secret | Access Token ≤ 15min + Refresh Token 黑名单 | OAuth 授权码 + PKCE | 关键操作开 MFA/TOTP
授权:每个 API 声明所需权限 | 数据访问以当前用户 ID 过滤(不信任前端传的 ID)| RBAC 为主 ABAC 补充
防漏洞:SQL 100% 参数化(#{},${} 加白名单)| 输出编码 + CSP + HttpOnly 防 XSS | Cookie SameSite=Lax + Secure + HttpOnly | 非 JWT 场景 CSRF Token | SSRF URL 白名单 + 内网 IP 过滤 | 文件上传多维校验(Magic Number/存储隔离/目录无执行权限)| 接口+数据双重权限校验(防越权)
运维安全:全站 HTTPS + HSTS | 敏感数据不入日志(toString() 排除密码)| 数据库无 DDL 账号隔离 | 网关+应用层双重限流 | 对外 API HMAC 签名 + Nonce + 时间戳防重放 | 安全响应头完整配置
面试高频追问速记
1. “JWT 怎么实现退出登录?” → 短 Access Token + Refresh Token 黑名单。本质是拿空间换时间——不让 Access Token 活太久。
2. “OAuth 2.0 隐式模式为什么废弃?” → Token 暴露在 URL fragment 无 client_secret 校验。PKCE 取代,非对称方式确保安全。
3. “MyBatis #{} 和 ${} 的底层区别?” → #{} 走 PreparedStatement(解耦 SQL 与数据),${} 走 Statement(拼接后整条发送无法区分代码与数据)。
4. “CSRF Token 为什么能防 CSRF?” → 攻击者跨站发起请求时,同源策略阻止其读取目标站点页面内容,拿不到 Token 值。
5. “Bcrypt 为什么比 SHA256 适合存密码?” → SHA256 设计目标是快(完整性校验),Bcrypt 故意慢(cost factor),且内存访问模式对硬件加速不友好。
6. “怎么防止水平越权?” → 不信任前端传来的用户 ID,数据查询必须从 SecurityContextHolder 获取当前用户作为过滤条件。
7. “API 签名为什么要加 Nonce?” → Timestamp 只能防过期重放,Nonce 保证同一时间窗口内每条请求唯一。两者配合才能彻底防重放。
8. “为什么 Symmetric(HS256)JWT 不推荐用于微服务?” → 所有服务共享同一把密钥,任何一个服务密钥泄露就能伪造所有服务的 JWT。RS256 用私钥签名公钥验证,即使公钥泄露无法伪造。