Spring-boot鉴权审计SOP

前言

Spring-boot的鉴权逻辑写法千奇百怪,有各式各样的AOP操作,注册Filter的方式,Filter链,DAO数据层比较复杂,但是把这个框架信息梳理一遍建立起好的反馈环境给LLM,加上LLM强大的语义理解应该能比较好的完成自动化

整体框架

我们不先从各种Filter,securityConfig,userid,sqlbind说起

我们先从spring-boot中一个请求->返回结果的流程来看,看看需要鉴权的地方一般出现在哪

Tomcat接收 -> Filter链

Spring-boot是内置的Tomcat

执行顺序:请求进入 Tomcat 容器,按 order 值从小到大依次执行 Filter 的 doFilter()。

chain.doFilter(req, res) 将请求传给下一个 Filter
最后一个 Filter 调用 chain.doFilter 后,请求进入 DispatcherServlet

那么怎么去寻找这个Tomcat的Filter链的具体逻辑和执行顺序呢,我们可以通过下面方法找到所有Filter

四条搜索,各自对应一种注册方式:

1
2
3
4
5
6
7
8
# 方式1:@WebFilter 注解
grep -rn "@WebFilter" --include="*.java"

# 方式2:FilterRegistrationBean 手动注册
grep -rn "FilterRegistrationBean" --include="*.java"

# 方式3:@Component + implements Filter
grep -rn "implements Filter\|extends.*Filter" --include="*.java"

还有一种更快的方法就是直接运行项目加入--debug参数在运行的时候就会自动把Filter链信息打印出来

1
2
3
4
5
6
7
8
9
--debug 启动时 Tomcat 打印了完整顺序:

Mapping filter: 'metricsFilter' to: [/*] ← 第1
Mapping filter: 'characterEncodingFilter' to: [/*] ← 第2
Mapping filter: 'hiddenHttpMethodFilter' to: [/*] ← 第3
Mapping filter: 'httpPutFormContentFilter' to: [/*] ← 第4
Mapping filter: 'requestContextFilter' to: [/*] ← 第5
Mapping filter: 'webRequestLoggingFilter' to: [/*] ← 第6
Mapping filter: 'applicationContextIdFilter' to: [/*] ← 第7

打开每个鉴权 Filter 的 doFilter() 方法,画控制流图。核心问题只有一个:

“有没有条件分支走到了 chain.doFilter()(放行)但本不该放行?”

找法:在 doFilter 里搜所有 chain.doFilter 调用点,逐个往前推——什么条件能走到这里?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 危险1:条件不满足反而放行
if (token == null) {
chain.doFilter(request, response); // ← 没 token 也放行?
return;
}

// 危险2:异常被吞后放行
try {
validate(token);
} catch (Exception e) {
chain.doFilter(request, response); // ← 校验失败也放行?
return;
}

// 危险3:shouldNotFilter 排除了太多路径
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return request.getServletPath().startsWith("/api"); // ← /api/admin 也被排除了
}

Spring Security Filter 链

这是Spring可以自己配置的安全配置,它原理是在Tomcat注册了一个Filter DelegatingFilterProxy

但是它的doFilter()是可以拿到在Spring中注册的bean

  • Tomcat 只感知到一个 Filter:DelegatingFilterProxy
  • 它的 doFilter() 内部拿到了 Spring 容器中的 FilterChainProxy Bean
  • FilterChainProxy 维护了一条完全独立的虚拟 Filter 链
  • 这条链里的 Filter 虽然也实现了 javax.servlet.Filter 接口,但不是注册给 Tomcat 的,是 Spring Security 自己 new
    出来、自己迭代调用的

这一类比较好找,主要是找项目中的配置类一般就叫securityConfig.java继承自WebSecurityConfigurerAdapter

需要按照不同版本的官方api写法进行审计

Interceptor

有些鉴权逻辑会写到拦截器上,本质是包裹在 Controller 外面的三层钩子:

1
2
3
4
5
6
7
8
public interface HandlerInterceptor {

boolean preHandle(req, res, handler); // Controller 执行前

void postHandle(req, res, handler, ModelAndView); // Controller 执行后,视图渲染前

void afterCompletion(req, res, handler, Exception); // 视图渲染后
}

注册方式都是通过addInterceptor注册,其实也是在securityConfig那个类中配置的,但是有的项目是分开写的,本质都一样

1
2
3
4
5
6
7
8
//配置拦截器
@Configuration
public class Interceptorconfig extends WebMvcConfigurerAdapter{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new recordInterceptor()).addPathPatterns("/**").excludePathPatterns("/logins").excludePathPatterns("/captcha");//意思就是除了logins和captcha两个路由都要被拦截器拦截
}
}

这里是一个项目的越权实例,这里原本是想作一个检测你登录状态的鉴权,如果没登录就要去logins去认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {

HttpSession session = request.getSession();
if (!StringUtils.isEmpty(session.getAttribute("userId"))) {
//导入dao类
UserDao udao = tool.getBean(UserDao.class, request);
RolepowerlistDao rpdao = tool.getBean(RolepowerlistDao.class, request);
Long uid = Long.parseLong(session.getAttribute("userId") + "");
User user = udao.findOne(uid);
List<Rolemenu> oneMenuAll = rpdao.findbyparentxianall(0L, user.getRole().getRoleId(), true, false);
List<Rolemenu> twoMenuAll = rpdao.findbyparentsxian(0L, user.getRole().getRoleId(), true, false);
List<Rolemenu> all = new ArrayList<>();
//获取当前访问的路径
String url = request.getRequestURL().toString();
String zhuan = "notlimit";

if (oneMenuAll.size() > 0) {
all.addAll(oneMenuAll);
}
if (twoMenuAll.size() > 0) {
all.addAll(twoMenuAll);
}
for (Rolemenu rolemenu : all) {
if (!rolemenu.getMenuUrl().equals(url)) {

return true;
} else {
request.getRequestDispatcher(zhuan).forward(request, response);
}

}

} else {
response.sendRedirect("/logins");
return false;
}

return super.preHandle(request, response, handler);
}

虽然单看这里看不出什么问题,但是实际上因为这个拦截得不够细导致的垂直越权,典型的只检测了登录状态,没检测用户权限,导致低权限用户可以使用高权限用户的路由

Controller方法体与注解AOP

这里也是审计的大头,因为Controller很多,上游有filter的interceptor,下游有DAO数据绑定,可能写错一个if/else判断条件就有可能造成越权

这类的逻辑写法比较复杂,但是核心审计流程思想如下

  • 从哪获取用户数据?session.getAttribute("userId")or@RequestParam("userid")
  • 对用户标识符干了什么?比如直接传入sql,或者有没有进行校验

除此之外还要注意AOP切面,也就是我们的注解,因为为了减少耦合,一般鉴权逻辑会通过AOP的方式来鉴权,而不是直接写在controller代码里面,不污染业务代码

一个简单的例子如下

假设有一个删除用户的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
// === Controller ===
@DeleteUser // ← 自定义注解
@RequestMapping("/deleteuser")
public String deleteUser(@RequestParam Long userId) {
// 方法体鉴权:写在这里
User currentUser = (User) session.getAttribute("user");
if (!"ADMIN".equals(currentUser.getRole())) {
throw new UnauthorizedException();
}

userDao.delete(userId); // 执行业务逻辑
return "success";
}

同样的鉴权用 AOP 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// === 独立的 Aspect 类,可能在另一个文件甚至另一个包 ===
@Aspect
@Component
public class AuthAspect {

@Around("@annotation(DeleteUser)") // 拦截所有标了 @DeleteUser 的方法
public Object checkAdmin(ProceedingJoinPoint joinPoint) throws Throwable {
User currentUser = getCurrentUser();
if (!"ADMIN".equals(currentUser.getRole())) {
throw new UnauthorizedException();
}
return joinPoint.proceed(); // 鉴权通过,执行原方法
}
}

此时 Controller 里就没有任何鉴权代码了:

1
2
3
4
5
6
7
// Controller 里只有业务逻辑,干干净净
@DeleteUser
@RequestMapping("/deleteuser")
public String deleteUser(@RequestParam Long userId) {
userDao.delete(userId); // 鉴权已经在 AOP 层做过了
return "success";
}

DAO层

前面几层(Filter、Interceptor、AOP、Controller)解决的是”能不能访问这个功能”。DAO 层解决的是”能看到多少数据”。

即使一个用户通过了所有前面的鉴权进入了功能页面,他能在里面看到什么数据,取决于 DAO 的查询有没有做隔离。

这一类比较需要通过语义和SQL语句来审计,需要深入了解系统的权限控制中有哪些角色,不同角色直接能不能互查

如果有MyBatis Mapper的话就比较方便,比如

1
2
3
4
5
6
7
8
9
<select id="allDirector" resultType="java.util.Map">
SELECT d.*, u.*
FROM aoa_director_users AS u
LEFT JOIN aoa_director AS d ON d.director_id = u.director_id
WHERE u.user_id=#{userId} <!-- ← 有用户隔离 -->
AND u.director_id is NOT null
AND u.is_handle=1
...
</select>

反馈环境建立for大模型

角色层级图

首先必须要建立一个分明的角色层级图,这样LLM才能理解哪里是真的越权了,哪里没有越权

这个角色信息一般是要去sql表结构与代码中获取用户的id结合查看才能建立

我们这里以oasys_cms为例,因为它本身sql写得比较清楚,所以一下就看出来了

image-20260531162041945

actuator

这是Spring内置的一个检测器,内置很多路由可以看当前系统信息

所有路由绑定的controller

位于/mappings中,从这些json可以提取出所有的路由

image-20260531161521143

Filter和拦截器的配置

位于/beans中

image-20260531161622174

Spring-boot鉴权审计.skill

总结在仓库里

jujubooom/spring-boot-auth-audit.SKILL: spring-boot鉴权审计SKILL