前言 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 if (token == null ) { chain.doFilter(request, response); return ; } try { validate(token); } catch (Exception e) { chain.doFilter(request, response); return ; } @Override protected boolean shouldNotFilter (HttpServletRequest request) { return request.getServletPath().startsWith("/api" ); }
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) ; void postHandle (req, res, handler, ModelAndView) ; 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去认证
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" ))) { 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 @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 @Component public class AuthAspect { @Around("@annotation(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 @DeleteUser @RequestMapping("/deleteuser") public String deleteUser (@RequestParam Long userId) { userDao.delete(userId); 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写得比较清楚,所以一下就看出来了
actuator 这是Spring内置的一个检测器,内置很多路由可以看当前系统信息
所有路由绑定的controller 位于/mappings中,从这些json可以提取出所有的路由
Filter和拦截器的配置 位于/beans中
Spring-boot鉴权审计.skill 总结在仓库里
jujubooom/spring-boot-auth-audit.SKILL: spring-boot鉴权审计SKILL