背景
最近在做内部系统安全复盘时,遇到一个比较典型的问题:某业务后台已经接入统一登录,但部分管理接口仍然可以在未登录状态下被访问。问题本身不复杂,却很有代表性,涉及路由鉴权、前后端权限边界、接口设计习惯以及代码审计方法。
这类问题在中小型后台、快速迭代的运营系统、历史项目迁移中比较常见。前端页面可能已经做了登录拦截,菜单也按角色隐藏,但后端接口如果没有统一鉴权,实际风险仍然存在。
本文只讨论防御视角下的漏洞分析和修复思路,不涉及针对真实目标的攻击利用过程。示例均为本地测试环境中的抽象代码。
问题场景
系统大致结构如下:
- 前端:Vue 单页应用,通过路由守卫判断是否存在 token。
- 后端:Spring Boot 提供 REST API。
- 登录:统一认证服务签发 JWT。
- 权限:部分接口使用注解校验,部分接口依赖网关或前端控制。
问题接口位于后台管理模块,例如用户列表、配置查询、导出任务状态等接口。页面层面无法直接进入,但接口在缺少登录态时仍返回数据。
典型表现包括:
- 访问管理页面会跳转登录页,但直接访问接口仍返回 200。
- 同一 Controller 中部分方法有权限注解,部分方法没有。
- 新增接口未纳入统一拦截器白名单/黑名单策略。
- 测试环境配置被复制到生产环境,导致部分路径被放行。
技术分析
这类未授权访问通常不是单点代码失误,而是权限模型不清晰导致的工程问题。常见成因有以下几类。
1. 只在前端做登录拦截
前端路由守卫只能提升用户体验,不能作为安全边界。比如常见的写法:
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (!token && to.path !== '/login') {
next('/login')
} else {
next()
}
})这段逻辑只能阻止普通用户通过页面导航访问后台,无法约束后端 API。只要后端没有校验,接口仍然是暴露的。
2. 后端鉴权策略不统一
在 Spring Boot 项目中,经常看到两种混用方式:
- 部分接口通过拦截器校验 token。
- 部分接口通过注解如
@PreAuthorize控制权限。 - 少量历史接口完全没有接入权限体系。
例如:
@RestController
@RequestMapping("/admin/user")
public class AdminUserController {
@PreAuthorize("hasAuthority('admin:user:list')")
@GetMapping("/list")
public List<UserVO> list() {
return userService.listUsers();
}
@GetMapping("/detail")
public UserVO detail(Long id) {
return userService.getUserDetail(id);
}
}上面 /list 有权限校验,但 /detail 没有。如果项目没有全局认证拦截,/detail 就可能成为未授权访问点。
3. 白名单配置过宽
另一个常见问题是认证拦截器或网关白名单配置过于宽泛:
security:
ignore-paths:
- /login
- /captcha
- /actuator/**
- /admin/**/export
- /admin/config/**配置初衷可能是放行登录、验证码、健康检查,或者临时调试导出接口。但随着业务迭代,某些路径下新增了敏感接口,白名单规则没有同步收敛,最终导致过度放行。
尤其需要注意通配符规则,例如 /admin/**、/**/export、/api/public/**。这类配置短期方便,长期风险较高。
4. 网关鉴权和服务鉴权边界不清
有些系统将认证逻辑放在 API Gateway 或 Nginx 层,后端服务默认信任来自内网的请求。这种架构不是不能用,但必须保证:
- 后端服务不能被外部直接访问。
- 网关转发时要传递可信身份信息。
- 服务端仍需对关键操作做权限校验。
- 内部调用也要有明确的认证边界。
如果后端服务端口误暴露,或者测试环境与生产环境网络策略不一致,就容易出现绕过网关直接访问服务接口的问题。
代码审计关键步骤
针对这类问题,建议不要只靠手工点页面,而是结合路由、配置、注解和日志做系统性排查。
1. 梳理接口清单
优先从后端路由入手,导出所有 Controller 映射关系。Spring Boot 可以通过 Actuator 或启动日志辅助查看,生产环境不建议直接开放相关端点。
# 本地测试环境查看接口映射
curl Actuator,也可以用 IDE 搜索:@RequestMapping@GetMapping@PostMapping@PutMapping@DeleteMapping
重点关注路径中包含以下关键词的接口:
admin、manage、systemuser、role、permissionconfig、settingexport、downloadlog、audit
2. 对比鉴权注解覆盖情况
可以用简单脚本或 IDE 结构视图检查 Controller 方法是否缺少鉴权注解。不是所有方法都必须写注解,但管理类接口至少应有统一认证兜底。
常见需要检查的注解包括:
@PreAuthorize@Secured@RolesAllowed- 项目自定义注解,例如
@RequiresPermission、@AuthCheck
一个相对稳妥的做法是:全局先校验登录态,再由方法级注解校验具体权限。也就是认证和授权分层处理。
3. 检查拦截器与白名单
重点看 WebMvcConfigurer、Filter、SecurityConfig、Gateway Filter 等位置。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(
"/login",
"/captcha",
"/health"
);
}
}
这里的原则是:默认拦截,明确放行。放行路径越少越好,且需要定期审查。
不建议使用下面这种配置:
.excludePathPatterns(
"/admin/**",
"/api/**/public/**",
"/**/download/**"
);
如果确实存在公开下载或公开查询接口,建议单独放在明确的 public Controller 下,避免与后台管理接口混用路径。
4. 验证不同身份下的访问结果
测试时至少准备三类请求:
- 无 token 请求。
- 普通用户 token 请求。
- 管理员 token 请求。
预期结果应当清晰:
请求身份 后台列表接口 个人信息接口 公开验证码接口 无 token 401 401 200 普通用户 403 200 200 管理员 200 200 200
这里要区分 401 和 403:
401 Unauthorized:未认证,缺少有效身份。403 Forbidden:已认证,但权限不足。
很多系统统一返回 200 再用业务码表示失败,这种方式不是完全不可行,但会增加网关、监控和自动化测试的判断成本。新项目建议尽量使用标准 HTTP 状态码。
修复建议
1. 建立默认拒绝策略
认证策略建议采用默认拒绝,而不是默认放行。除登录、验证码、静态资源、健康检查等明确公开接口外,其余接口都必须先通过认证。
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/captcha", "/health").permitAll()
.anyRequest().authenticated()
);
如果项目使用 Spring Security,这是比较清晰的基线。对于自研拦截器,也应保持类似逻辑。
2. 关键接口增加方法级授权
只校验登录态还不够,后台管理接口应继续校验角色或权限点。
@PreAuthorize("hasAuthority('system:user:read')")
@GetMapping("/admin/user/detail")
public UserVO detail(@RequestParam Long id) {
return userService.getUserDetail(id);
}
如果权限点过细,也要避免走向另一个极端:权限命名混乱、无人维护。建议按资源和动作组织,例如:
system:user:readsystem:user:updatesystem:role:assignaudit:log:export
3. 对敏感数据做字段级控制
未授权访问的危害往往来自返回数据过多。即使接口权限修好了,也建议检查返回对象,避免把内部字段直接暴露给前端。
例如用户详情接口不应返回:
- 密码哈希
- 盐值
- 重置密码 token
- 内部备注
- 身份证、手机号等非必要敏感字段
推荐使用专门的 VO/DTO,不要直接返回数据库实体。
public class UserVO {
private Long id;
private String username;
private String displayName;
private String status;
}
4. 加入自动化安全回归
这类问题很适合做自动化回归。可以在集成测试中覆盖关键接口的无 token、低权限、高权限访问结果。
@Test
void adminApiShouldRejectAnonymousRequest() throws Exception {
mockMvc.perform(get("/admin/user/list"))
.andExpect(status().isUnauthorized());
}
对历史项目而言,哪怕先覆盖核心管理接口,也比完全依赖人工测试更可靠。
5. 日志与告警
权限拦截失败不是只返回错误就结束了,建议记录必要日志,便于排查异常访问。日志内容应包含:
- 请求路径
- 请求方法
- 用户 ID 或匿名标识
- 客户端 IP
- 失败原因:未登录、token 失效、权限不足
注意不要在日志中记录完整 token、密码、验证码等敏感内容。
注意点
- 不要把前端菜单隐藏当作权限控制。
- 不要长期保留临时白名单。
- 不要在多个地方重复实现互不一致的鉴权逻辑。
- 不要让测试环境安全配置直接进入生产。
- 不要默认信任内网请求,尤其是容器化和微服务环境。
- 不要把实体对象直接作为接口返回值。
一个较实用的排查清单
检查项 建议 接口是否默认需要登录 除明确公开接口外全部认证 后台接口是否有权限点 管理类接口增加方法级授权 白名单是否过宽 避免使用大范围通配符 网关是否是唯一入口 确认后端服务未直接暴露 返回字段是否过多 使用 DTO/VO 控制输出 是否有自动化测试 覆盖匿名、低权、高权访问 是否有审计日志 记录失败原因但避免敏感信息
总结
后台未授权访问通常不是某个接口少写一行代码这么简单,而是权限体系缺少统一边界的表现。修复时不要只补单个接口,更应回到整体设计:默认拒绝、统一认证、细粒度授权、最小化返回字段、自动化回归。
从代码审计角度看,排查这类问题最有效的方法是把接口清单、鉴权配置、权限注解和实际访问结果放在一起对照。只看页面很容易漏,只看代码也可能忽略运行时配置。对于长期维护的业务系统,这套检查方式值得沉淀到发布流程中。

网络请求、日志与边界流量分析示意
推荐意见