1. 项目背景:一个让我决定把权限压进选择屏幕的报表需求
1.1 接手报表时的现状
年初接到一个销售销售报表的改造需求,客户说得很直接:现在这个ZRPT_SALES_001,只要有菜单权限的账号,一进来能把全公司的销售订单都查出来。销售一组的人只要手一贱按下回车,别的事业部报价、客户信息全看见了。业务根本不要求做复杂的分析,只提了一句话:你们这个系统的权限边界,能不能在用户点执行之前就帮我卡死。
我打开程序一瞧,果不其然,这是一个典型的“裸奔报表”——结构清爽,几段SQL干到底,SELECT-OPTIONS里放了销售组织、客户、日期范围,然后没有任何AUTHORITY-CHECK。执行键一按,所有数据直接怼到ALV上。说实话,在很多内部报表里这并不稀奇,尤其是那些从很早之前一直传到现在的老旧Z程序,权限基本都是靠菜单和角色在管,程序内部几乎不设防。可一旦用户权限范围逐渐扩大、账号交叠,菜单能挡住的只是入口,挡不住数据本身。
1.2 为什么“查询后再过滤”不行
很多同行第一反应是:数据查出来之后在ALV输出前做一次过滤不就行了?逻辑上看似可行,但你会遇到三个麻烦。
第一个麻烦是性能。用户没有输入销售组织筛选条件时,程序会捞出全公司所有销售组织的数据,几百万条记录在网络和数据库中跑一遍,然后再在应用层把没权限的行删掉。为了几个无权访问的销售组织,白付了全量查询的成本,这种事情在SAP里属于典型的“可以跑但非常不优雅”,而且随着数据量增长迟早会炸。
第二个麻烦是数据泄露窗口。查询结果已经进入应用服务器了,哪怕ALV上没显示,内存里仍然有完整的全量数据集。如果后续这段数据被传递给其他模块、接口或用于批处理,权限边界在技术上已经失效——任何一次后续处理路径的疏漏,都可能把数据带出去。
第三个麻烦是用户体验。如果用户A输入了销售组织1000,但他其实只有2000的权限,你是等到结果出来后才告诉他“抱歉,这行你没权限”,还是在他一按回车、还没开始干活的时候就告诉他“兄弟,这个组织不在你的授权范围内”?很明显,后者才是一款成熟报表该有的反应。
1.3 最终取舍:在 SELECT-OPTIONS 环节做权限边界
于是我把改造方案放在了一个最容易被忽略的位置:选择屏幕事件,也就是AT SELECTION-SCREEN这一段。在用户点了执行按钮之后、任何SQL运行之前,把用户输入的SELECT-OPTIONS逐一和权限对象做AUTHORITY-CHECK。不合法就直接弹消息、删值、或者用用户自己的权限集合生成一个默认范围。这样做的好处是,从一开始进到程序的数据就是合法的,权限边界被“前置”到了查询入口,整个执行阶段不需要再为权限操心了。
这篇文章就是把这次改造的完整思路、代码细节和踩坑过程整理出来。你可以直接照着这个思路去改造自己的Z报表,也可以把其中的封装部分抽出来,变成你们团队内部通用的权限校验工具。
2. 方案核心设计:AUTHORITY-CHECK 如何与 SELECT-OPTIONS 联动
2.1 先搞清楚权限对象的几个成员
要把AUTHORITY-CHECK用在SELECT-OPTIONS上,第一步是确认你手里的权限对象是什么结构。SAP的权限对象一般由Authorization Object(权限对象)、Authorization Fields(权限字段),以及字段上的Activity(活动)组成。以我这次为销售报表建的权限对象ZSO_SALES为例,它包含两个字段:
| 权限对象 | 字段 | 含义 |
|---|---|---|
| ZSO_SALES | VKORG | 销售组织 |
| ZSO_SALES | VTWEG | 分销渠道 |
权限对象是给业务角色做权限分配用的。在SU01维护用户角色的时候,管理员会为ZSO_SALES分配VKORG=1000、VTWEG=10等值。程序里的AUTHORITY-CHECK目的就是判断当前登录用户是否具备这些值对应的操作权限。
需要特别注意的是,AUTHORITY-CHECK语句里ID后面的字段值,必须和权限对象真正定义的字段保持一致,少一个ID、多一个ID,或者使用了DUMMY占位导致某些字段没有参与检查,都可能让整个权限形同虚设。关于这一点,后面会专门讲坑。
2.2 选屏事件家族:AT SELECTION-SCREEN 到底有哪些关键时刻
SAP选择屏幕的事件比很多人想象的丰富,这里挑几个和权限相关的关键节点说明:
- AT SELECTION-SCREEN:整个选择屏幕执行时触发,在后面加ON BLOCK可以限定某个BLOCK,加ON field可以只在某个字段变化时触发。
- AT SELECTION-SCREEN ON VALUE-REQUEST FOR field:对应F4帮助事件,可以在这里为SELECT-OPTIONS自定义值列表,非常适合做权限范围的输入提示。
- AT SELECTION-SCREEN OUTPUT:屏幕输出之前触发,可以在这里修改屏幕属性、隐藏字段、修改输入状态。
- START-OF-SELECTION:程序正式执行的开始,所有选屏输入完成之后才触发。
真正适合放AUTHORITY-CHECK的位置有两个:AT SELECTION-SCREEN和START-OF-SELECTION。我推荐在AT SELECTION-SCREEN阶段拦截,因为这样可以拿到用户刚输入的内容,第一时间反馈错误,而不用等SQL逻辑跑起来之后再打断。
2.3 同一块权限,三种拦截位置的区别
我之前用一张设计对比表帮项目组理清了方案选型,现在也分享出来:
| 位置 | 触发时机 | 能拦截非法输入 | 能修改用户输入 | 性能开销 | 推荐度 |
|---|---|---|---|---|---|
| AT SELECTION-SCREEN ON field | 字段输入离开时 | 是 | 是 | 极低 | 高 |
| AT SELECTION-SCREEN(整体) | 点执行按钮时 | 是 | 是 | 极低 | 高 |
| START-OF-SELECTION | 数据查询开始前 | 是(不及时) | 可以但别扭 | 极低 | 中 |
| 数据读取后ALV前过滤 | 数据已经查出后 | 否,有泄露窗口 | 否 | 高 | 不推荐 |
最终我选的是“AT SELECTION-SCREEN + 字段事件”的组合:在字段级事件里做输入的逐值校验,让用户在输入销售组织的时候马上得到反馈;在整体执行事件里做兜底检查,防止某些绕过字段事件的情况。
3. 代码落地:从权限值收集到筛选条件封锁
3.1 收集用户有权限的值集合
方案的第一块基石,是把当前用户“有权限的那个值集”提前收进来。很多人到这里就卡住了,原因是SAP没有一个开箱即用、直接返回某个权限对象所有授权值列表的标准函数,直接去读USR12授权表又太复杂,且不同profile、复合角色会拼出一堆重复值,很难维护。
我采用了一个更可控的方案:从业务主数据表中,把可能作为权限范围值的记录全部列出来,然后逐一用AUTHORITY-CHECK做验证,通过的就收集到一个RANGE表里。说白了,就是拿业务数据作为候选池,用权限对象当筛子。这个思路在绝大多数业务场景下都成立,因为报表的销售组织范围,无论如何都不会超出组织架构主数据里存在的范围。
代码示意如下:
FORM collect_auth_vkorg CHANGING ct_range TYPE vkorg_tab. DATA: ls_range LIKE LINE OF ct_range. REFRESH ct_range. SELECT vkorg FROM tvko UP TO 1000 ROWS INTO @DATA(lv_vkorg). AUTHORITY-CHECK OBJECT 'ZSO_SALES' ID 'VKORG' FIELD lv_vkorg ID 'VTWEG' FIELD '***'. IF sy-subrc = 0. ls_range-sign = 'I'. ls_range-option = 'EQ'. ls_range-low = lv_vkorg. APPEND ls_range TO ct_range. ENDIF. ENDSELECT. ENDFORM.这里有一个容易混淆的写法需要解释:字段VTWEG在检查时用的是通配符***。ABAP中AUTHORITY-CHECK对字段值进行匹配时,SAP的权限对象内部对字段值支持泛化匹配,***代表“任意值”。换句话说,这次检查只看VKORG维度,不限制分销渠道。这个技巧很常用,但前提是权限对象的设计者允许该字段通配。如果业务上要求销售组织权限必须同时绑分销渠道,那你就要从另一个销售渠道的SELECT-OPTIONS里也取值做同样处理。
3.2 在 AT SELECTION-SCREEN 中做逐项校验
值集合收好之后,接下来是在选择屏幕上处理用户输入。下面是报表中实际使用的核心代码框架:
REPORT zrpt_sales_001. TABLES: vkorg. SELECT-OPTIONS: s_vkorg FOR vkorg-vkorg, s_vtweg FOR vtweg-vtweg, s_date FOR vbak-erdat. DATA: gt_auth_vkorg TYPE RANGE OF vkorg-vkorg. INITIALIZATION. " 提前收集权限值,这里只做一次,后续校验反复使用 PERFORM collect_auth_vkorg CHANGING gt_auth_vkorg. AT SELECTION-SCREEN ON s_vkorg. PERFORM check_vkorg_auth CHANGING s_vkorg[]. AT SELECTION-SCREEN. " 兜底检查:如果用户什么都没填,自动放入权限范围 IF s_vkorg[] IS INITIAL. s_vkorg[] = gt_auth_vkorg[]. IF s_vkorg[] IS INITIAL. MESSAGE e000(zmsg) WITH '当前用户没有销售组织权限,请联系管理员'. ENDIF. ENDIF. FORM check_vkorg_auth CHANGING ct_vkorg TYPE vkorg_seltab. DATA: lv_index TYPE sy-tabix. FIELD-SYMBOLS: <ls_vkorg> LIKE LINE OF ct_vkorg. LOOP AT ct_vkorg ASSIGNING <ls_vkorg>. " 只对单个等值条件做精细校验,区间和排除逻辑走特殊分支 IF <ls_vkorg>-option = 'EQ' AND <ls_vkorg>-sign = 'I'. AUTHORITY-CHECK OBJECT 'ZSO_SALES' ID 'VKORG' FIELD <ls_vkorg>-low ID 'VTWEG' FIELD '***'. IF sy-subrc <> 0. lv_index = sy-tabix. MESSAGE e000(zmsg) WITH '销售组织' <ls_vkorg>-low '不在你的权限范围内'. ENDIF. ELSEIF <ls_vkorg>-option = 'BT' AND <ls_vkorg>-sign = 'I'. " 区间处理:两个端点都要有权限才放行 AUTHORITY-CHECK OBJECT 'ZSO_SALES' ID 'VKORG' FIELD <ls_vkorg>-low ID 'VTWEG' FIELD '***'. IF sy-subrc = 0. AUTHORITY-CHECK OBJECT 'ZSO_SALES' ID 'VKORG' FIELD <ls_vkorg>-high ID 'VTWEG' FIELD '***'. ENDIF. IF sy-subrc <> 0. MESSAGE e000(zmsg) WITH '权限范围外的销售组织区间,无法执行'. ENDIF. ENDIF. ENDLOOP. ENDFORM.这段代码要解决的核心问题,是怎样把AUTHORITY-CHECK的返回码和选择屏幕的字段状态捆绑起来。逻辑很直白:用户输入的每个等值条件,如果不在授权范围内,直接抛出E类消息,程序在选屏阶段就暂停,SQL根本不会执行。
有一点需要提醒:AUTHORITY-CHECK的返回码不止0和非0这么简单。通常0代表有权限,4代表无权限,12代表权限对象或字段不存在,这个时候建议引用官方说明做进一步诊断。很多新手在12这个码上吃了大亏,以为自己写错了,其实是对象的激活状态问题。后面的章节我详细展开。
3.3 没有输入筛选条件时的默认权限兜底
很多报表的SELECT-OPTIONS是可选输入,用户不填就代表“全部”,这个习惯在内部报表里根深蒂固。但在有权限控制的场景下,“不填=全部”是一个非常危险的默认行为。
我在这次改造里的策略是:用户不填销售组织,系统就把收集到的、他有权限的销售组织集合放进去,相当于把他的查询范围自动收敛到权限边界内。如果收集到的集合也为空,就直接报错中止程序。这个逻辑很容易理解:不给销售组织,就给权限范围内的组织;连权限都没有,就不让查。
要注意的是,这个兜底逻辑不能放在AT SELECTION-SCREEN ON s_vkorg里面,因为那个事件只在字段发生变化时触发,用户压根没动这个字段就不会执行。要放在不带ON字段限定的AT SELECTION-SCREEN里,确保用户点击执行按钮时,一定会走到这一段判断。
3.4 与 F4 帮助联动:别让用户看到不该看的值
只做执行前校验还不够,用户能不能通过F4输入帮助看到一个下拉列表,然后把无权销售组织选进去,这是另一个体验问题。我的方案是同时在AT SELECTION-SCREEN ON VALUE-REQUEST FOR s_vkorg-low里做自定义的F4值列表,直接把用户输入的销售组织筛选范围限定在权限值集合内。
实现方式很简单,在值列表请求事件里,调用标准函数F4IF_INT_TABLE_VALUE_REQUEST,把要展示的表数据换成之前收集到的gt_auth_vkorg:
AT SELECTION-SCREEN ON VALUE-REQUEST FOR s_vkorg-low. PERFORM f4_vkorg CHANGING s_vkorg. FORM f4_vkorg CHANGING ct_vkorg TYPE vkorg_seltab. DATA: lt_return TYPE TABLE OF ddshretval, ls_return LIKE LINE OF lt_return. IF gt_auth_vkorg[] IS INITIAL. PERFORM collect_auth_vkorg CHANGING gt_auth_vkorg. ENDIF. " 将权限范围中的 low 值整理为去重后的值列表 SELECT DISTINCT low FROM @gt_auth_vkorg AS a INTO @DATA(lv_value) ORDER BY low. " 这里可以直接使用 F4IF_INT_TABLE_VALUE_REQUEST 展示值列表 ENDSELECT. ENDFORM.实际上,F4帮助配合权限值集合这个方案只花几十分钟就搞定了,但效果非常明显。用户输入的时候自动弹出可访问的销售组织,不需要靠背编码,也不会选到无权数据。从业务侧反馈来看,这一步带来的直接感官提升比后台的权限校验还大。
4. 避坑实录:权限校验在选屏上最容易翻车的四个地方
4.1 AUTHORITY-CHECK 返回 12 不代表你没有权限
AUTHORITY-CHECK的错误码很有迷惑性。我在测试的时候,用SAP_ALL账号执行,按道理SAP_ALL是全权限,任何检查都应该通过,但代码一梭子跑完,某些行却报了4和12。最开始我怀疑是权限对象激活有问题,后来SU53一查才发现,问题出在权限对象本身没有给SAP_ALL这个用户分配“完整”的授权,或者是对象中某些ID字段在角色里根本没维护值。
这里有个很重要的认知:SAP_ALL虽然涵盖绝大多数标准权限对象,但企业自建的Z开头对象,默认不会自动包含在SAP_ALL里,除非管理员专门给SAP_ALL的Profile加了这条对象。所以测试时要留个心眼:不要看到ZSO_SALES就假设SAP_ALL一定有权限,否则你会得出“权限检查失效”的错误结论。
返回12通常表示某个权限字段在权限对象里不存在,或者字段没有被正确传递。排查思路是先SU53看最近权限检查的字段值,再用SUIM查看权限对象的激活状态。字段值如果传递的是空值或者不存在的值,12就会冒出来。很多情况下不是代码的锅,而是权限对象定义和角色维护不同步。
4.2 区间与 EXCLUDE 的组合怎么处理
SELECT-OPTIONS不是一个简单的单值输入框,它是一个标准内表,每行有SIGN、OPTION、LOW、HIGH四个字段。用户完全可以输入“1000到2000”“排除3000”这种复杂条件。如果只用“逐行EQ校验”的逻辑去处理,区间一多就会漏过边界。
我在项目里用的是“收紧+拦截”的组合策略。对于SIGN=I、OPTION=EQ,逐值校验;对于SIGN=I、OPTION=BT,检查区间两个端点都有权限;对于SIGN=E(排除)的条件,理论上“排除一个无权值的请求”等于不想看那个值,这并不会造成越权,所以可以放行,但最好提示用户该值本身不在授权范围内;至于OPTION=CP这种模式匹配,一旦发现用户用了模糊搜索且匹配范围可能超出权限,最简单的做法是直接拒绝,要求用户改成精确值。
如果你维护的权限边界本身是“精确到组织”,那这种组合策略基本够用。如果权限边界是“大区能看多个组织”,你还是先想清楚业务规则,别指望一段通用代码能覆盖所有权限语义。
4.3 测试时永远全权限:SAP_ALL 掩盖问题
这次项目最大的测试难点,不是代码本身,而是测试账号的权限组合。开发环境里大多数开发者的账号都带着SAP_ALL,导致任何AUTHORITY-CHECK都返回0,看起来一切正常。一旦上了生产,用户的角色是由业务分配的,结果就不一样了。
所以我建议在回归测试时专门建一个“受限测试用户”,只授予报表本身的可执行权限,以及ZSO_SALES销售组织的部分值权限,不带SAP_ALL。手工创建用户时,角色只放一个Z_RPT_TEST,这个角色只含执行权限对象和销售组织1000、1010两个值。测试时的预期行为是:输入1000放行,输入2000报错,不输入自动变成1000和1010。
有条件的团队还可以做一个自动遍历的ABAP测试程序,循环不同权限组合,断言SELECTION-SCREEN的校验结果,但这通常需要用到ABAP单元测试框架和MOCK权限上下文,投入产出比略低。至少手工场景要覆盖全。
4.4 别在 SQL 里只依赖一次校验
我们最终把权限校验放在了选屏阶段,SQL里没有了权限子句,但如果你维护的是老程序,有些代码是在START-OF-SELECTION之后才拼动态SQL的,这时候要小心:动态OPEN SQL里千万不要只依赖外层的一次校验,然后在内部子查询、多个报表页签或者跳转目标里忘了把权限范围带过去。
我处理这类问题的原则是:凡是需要跨程序跳转、或者用SUBMIT把报表值传出去的场景,都要把权限范围E.XPORT到内存,接收方在初始化时做强校验,而不是无条件信任传入的SELECT-OPTIONS。你可以把权限集合通过ABAP内存传递,但接收端至少再做一次AUTHORITY-CHECK,否则其他人写个报表直接把内存里塞满值,就能绕过选屏逻辑。
5. 延伸封装:把权限范围做成可复用模块
5.1 封装成类:后续报表直接复用
这次改造做完后,我发现将权限收集和校验逻辑直接写在报表里虽然直观,但一旦有七八个报表都要做同样的权限控制,复制粘贴就会变成灾难。于是我把它抽成了一个可复用的权限处理类。
CLASS zcl_sales_auth DEFINITION PUBLIC FINAL CREATE PUBLIC. PUBLIC SECTION. CLASS-METHODS: collect_vkorg_range RETURNING VALUE(rt_range) TYPE vkorg_seltab, check_vkorg IMPORTING iv_vkorg TYPE vkorg RETURNING VALUE(rv_result) TYPE abap_bool, check_vkorg_range IMPORTING it_range TYPE vkorg_seltab RAISING cx_static_check. ENDCLASS.类的实现把权限对象、字段和查询源封装在一起,报表里只需要调用ZCL_SALES_AUTH=>CHECK_VKORG_RANGE,把用户输入的SELECT-OPTIONS传进去,无论报错还是自动生成默认范围,都交给类来管。类似权限校验类可以推广到销售、采购、财务等不同域,每个域维护自己的权限对象字段。
封装好的另一个好处是,如果以后权限对象从ZSO_SALES换成标准的S_SALES,或者业务上要在VTWEG维度上收紧权限,只需要改类内部逻辑,所有调用方自动生效。每次权限模型变更时,不需要满项目去找报表逐个改。
5.2 与 F4 帮助联动可以做成配置表
如果你觉得“循环业务主数据+AUTHORITY-CHECK”的方式在大数据量下效率不够理想,可以考虑在权限值收集层加入配置表。我们维护了一张ZORG_AUTH_MAP,包含权限对象名、字段名、候选数据源表名、取值条件等元数据,然后写一个通用读取函数来解析这张配置表,再把候选值交给AUTHORITY-CHECK过滤。
这种方式虽然前期投入多一点,但胜在灵活。业务新增一个报表,只要在配置表加一条记录,不需要写ABAP代码,权限范围就自动被收集和校验,适合已有几十张权限相关报表的团队做规模化推广。
如果数据量确实特别大,比如候选销售组织有一两千个、用户权限却很分散,逐个AUTHORITY-CHECK的开销也不算小。这时候可以在类里加一层静态缓存,同一个用户在一次执行生命周期内只做一次收集,后面所有报表段共用同一个内存变量,显著减少重复校验。实测中,销售组织维度一两百个值的情况下几乎无感,但如果候选池上万,建议还是把候选源换成“按组织维度优先过滤”的查询条件,提前缩小操作范围。
6. 复盘:这个设计解决了什么,还留下了哪些思考
这次改造从头到尾经历了两周,代码量并不大,但方案讨论的时间比写代码多得多。最后沉淀下来的核心思路很简单:把AUTHORITY-CHECK放到SELECT-OPTIONS的入口,让权限边界在SQL执行之前就生效。这条原则适用于绝大多数权限敏感型报表,能同时解决性能、体验和数据泄露三个层面的问题。
有一点没有在代码里体现,但我想特别提出来:权限对象虽然能校验“用户能不能访问某个值”,但它并不知道业务报表的筛选逻辑是否合理。比如一个销售大区的用户,明明只有大区组织的权限,却可以输入一个日期范围查询历史归档数据,权限上每个组织都合法,但数据汇总逻辑仍然可能出现预期之外的结果。所以权限边界是一个漏斗,AUTHORITY-CHECK只是其中一道网,更上层的查询条件设计、报表展示粒度、导出脱敏,仍然需要结合业务规则一起设计。
站在我的角度,这个方案最值得推广的部分,不是特定代码片段本身,而是“在用户输入阶段就建立信任”的思路。一个报表如果等到数据都跑到内存里才开始考虑谁该看到什么,那它永远都在打补丁。把权限前置到SELECT-OPTIONS,本质上是在数据流入口处就塑造了安全的默认行为,这对习惯了“反正后台有权限,前端随便试试”的老系统来说,是一个不大但非常关键的扭转。
如果再给我一次机会,我会在项目一开始就把权限对象的设计文档补全,而不是一边写代码一边纠结ZSO_SALES到底要不要加VTWEG字段。权限模型一旦稳定,后面的所有校验逻辑都只是流程问题。希望这篇分享,能给正在被权限报表折磨的ABAP同行一个可落地的参考。