做ABAP开发到第三年,我才真正被“权限校验不对”这个问题逼着把权限对象完整研究了一遍。之前写代码时,遇到需要判断用户能不能操作某个事务、能不能看某个工厂的数据,我都是条件反射一样写上AUTHORITY-CHECK,然后看一眼SY-SUBRC就完事。直到做一个接口平台项目,需要把当前用户在V_VBAK_AUG上的销售组织、分销渠道、产品组授权值全部拉出来,动态生成第三方界面的数据过滤条件,才发现自己对“怎么拿权限对象的值”这件事的理解完全是碎片化的:AUTHORITY-CHECK只能告诉你“这一个值能不能过”,但要拿到用户在一组字段上被授予的所有值清单,得去读授权表、合并配置文件和直接授权、再处理低值高值范围,这里面坑特别多。
这篇文章我把整套东西做一个系统梳理:先从权限对象的结构讲清楚值到底是什么,再把AUTHORITY-CHECK的返回值一一拆开,接着给出直接读取USR12/UST12授权表拿到全部授权值清单的写法,最后用BAPI_SALESORDER_CREATEFROMDAT2调用前预校验、ME55审批增强校验、报表结果集过滤三个实际场景说明怎么用。同时会穿插我在项目里踩过的坑和调试技巧。适合刚接触ABAP权限开发的同事,也适合已经写过两年ABAP、但一直靠试错来猜权限逻辑的开发者。
1. 权限对象值到底是个什么东西
1.1 字段 + 值:权限对象的最小组成单元
SAP里的权限对象,本质是一个容器,里面装着若干个“字段”。最经典的S_TCODE对象,只有一个字段TCD,值就是事务代码,比如VA01、MM01。而像M_MSEG_WWA这种物料过账的权限对象,字段就多了:WERKS(工厂)、MBWWA(移动类型),可能还有LGORT(库存地点)。所以“获取用户权限对象的值”,准确说应该是:获取用户在某一个权限对象的某一个字段上,被分配了哪些具体值,或者被分配了哪个范围的值。
这里有个新手很容易混淆的点:角色(Role)和权限值不是一回事。角色是在PFCG里维护的,里面挂了菜单、属性、权限对象;而“权限对象的值”是角色下每一个对象每一个字段上具体填的授权值,比如S_TCODE字段TCD填了VA01,或者M_MSEG_WWA的WERKS填了1000~2000。用户通过SU01把角色分配给账号,最终生效的是账号在系统里经过权限聚合后的值,而不是角色名本身。所以当你需要判断用户能不能使用某个事物代码、能不能操作某个工厂的数据时,本质上要拿到的是“权限对象的值”。
理解到这一层,后面的方案选择才有基础。如果你只需要判断“当前传进来的这个值有没有被授权”,跑一次AUTHORITY-CHECK就完了;但如果你想知道“用户在这个对象上到底有哪些值”,那就得去读授权表。
1.2 开发中必须读权限值的三类场景
并不是所有权限需求都能靠AUTHORITY-CHECK应付。我总结下来,至少有三类场景必须把权限对象的值“拉出来”:
第一类是权限管理和维护工具。比如做一个批量复制用户权限的功能,需要把源用户在某个对象上的字段值读出来,然后写进目标用户的临时授权数据里。这时AUTHORITY-CHECK完全没用,你得直接读授权表。
第二类是接口和增强的预校验。标准BAPI内部通常不做权限检查,比如BAPI_SALESORDER_CREATEFROMDAT2,你用一个没有销售订单创建权限的账号通过RFC调用它,它一样能创建订单,因为权限检查是在SAP GUI的事务里由界面代码控制的,BAPI本身不管。所以我做项目时,凡是对外提供API或者做增强,都会在调用之前自己先校验一遍权限。这时候用AUTHORITY-CHECK可以,但如果你想在预校验后给调用方返回“你在哪几个工厂没有权限”这样友好的提示,还是需要把授权值清单拿出来对比。
第三类是数据和界面过滤。有些报表希望选择画面上默认只显示用户有权限的工厂;有些外部系统通过接口查询数据,要求只返回当前用户权限范围内的记录。这种场景如果对每一行数据都跑一次AUTHORITY-CHECK,性能会很差,正确做法是先把用户在某字段上的授权值清单读出来,生成一个范围表,再在SELECT里用FOR ALL ENTRIES或者RANGES去过滤。
2. 运行时权限判断的首选姿势:AUTHORITY-CHECK
2.1 语法剖析:ID、FIELD 和 DUMMY
先看最基础的写法:
AUTHORITY-CHECK OBJECT 'S_TCODE' ID 'TCD' FIELD lv_tcode.这段代码的意思很直白:检查当前用户(SY-UNAME)在对象S_TCODE的字段TCD上,是否对值lv_tcode拥有权限。对象名和字段名必须和SE11里权限对象定义完全一致,大小写无所谓,但拼写绝对不能错。
如果一个对象有多个字段,就逐个ID写:
AUTHORITY-CHECK OBJECT 'M_MSEG_WWA' ID 'WERKS' FIELD lv_werks ID 'MBWWA' FIELD lv_bwart.这里每个ID后的FIELD值可以是变量,也可以直接写字面值。多个ID之间的关系是AND,也就是说,任何一个字段的值不在授权范围内,整体就算不通过。
还有一种写法是DUMMY:
AUTHORITY-CHECK OBJECT 'M_MSEG_WWA' ID 'WERKS' DUMMY ID 'MBWWA' FIELD lv_bwart.DUMMY的含义是“不检查这个字段”。注意它和“这个字段值等于*,所以放行”不是一回事。DUMMY是直接跳过该ID的比较逻辑。这种写法在有些场景下很好用,比如某个字段的授权已经在另一个权限对象里更细粒度地检查过了,再这里查一遍会有大量误报,就用DUMMY跳过。
2.2 SY-SUBRC 返回值查表与实战解读
AUTHORITY-CHECK执行完,结果放在SY-SUBRC里。常见就四个值,很多老开发闭着眼都知道,但新手的典型问题是不知道4和12的区别。
| SY-SUBRC | 含义 | 常见原因 |
|---|---|---|
| 0 | 校验通过 | 传入值完全匹配用户授权范围 |
| 4 | 校验不通过 | 用户有该对象授权,但传入值不在授权范围内 |
| 8 | 无法执行校验 | 对象不存在、字段名拼错、FOR USER指定用户不存在 |
| 12 | 值无效 | 传入字段值为空或非法值,无法与授权范围比较 |
为什么重点强调4和12的区别?因为排错方向完全不同。4说明授权对象本身是存在的,用户也确实有这个对象,只是值不对,比如授权给工厂1000,你传了2000。12则往往是代码bug,比如调用前忘了给字段赋值,传了个初始值进去,系统认为这个比较没有意义,直接返回12。我在代码评审时经常看到有人把12当成“用户没有权限”处理,然后给用户报错,结果实际是程序自己传了空值。
还有一个坑:如果一次AUTHORITY-CHECK里写了多个ID,SY-SUBRC只返回第一个失败ID的状态码,不会告诉你具体是哪一个字段挂的。尤其当几个ID的传入值都比较复杂时,很容易让人蒙圈。所以我建议不要在复杂业务逻辑里写一次包含四五个ID的AUTHORITY-CHECK,而是拆开写,每个对象一个判断,失败后把对象名、字段名、传入值全部记到日志里。
2.3 多个对象的组合校验怎么组织
一个真实业务操作往往涉及多个权限对象。拿创建销售订单举例,V_VBAK_AUG管销售组织/分销渠道/产品组,可能还需要对订单类型的相关对象做校验。这些对象之间有先后,也有依赖,不能简单连写几个AUTHORITY-CHECK然后取最后一次的SY-SUBRC。
我在项目里习惯封装一个小方法,逐个对象检查,并且把失败上下文返回给调用方:
METHOD check_create_sales_auth. DATA: ls_result TYPE ty_auth_result. AUTHORITY-CHECK OBJECT 'V_VBAK_AUG' ID 'VKORG' FIELD iv_vkorg ID 'VTWEG' FIELD iv_vtweg ID 'SPART' FIELD iv_spart. IF sy-subrc <> 0. ls_result-object = 'V_VBAK_AUG'. ls_result-subrc = sy-subrc. rs_result = ls_result. RETURN. ENDIF. AUTHORITY-CHECK OBJECT 'S_TCODE' ID 'TCD' FIELD 'VA01'. IF sy-subrc <> 0. ls_result-object = 'S_TCODE'. ls_result-subrc = sy-subrc. rs_result = ls_result. RETURN. ENDIF. rs_result-subrc = 0. ENDMETHOD.这样的好处是调用方能明确看到失败对象和返回码,对外部系统或审批流可以给出具体提示:“你在销售组织1000上没有V_VBAK_AUG权限”,而不是冷冰冰的一句“没有权限”。
顺带说下ABAP新语法的现状。新版ABAP里,内联声明、CONV、CORRESPONDING这些新语法确实让代码短了不少,但AUTHORITY-CHECK这块我仍然推荐传统写法,因为它本身是语句不是函数,新语法提供不了额外能力,反而把可读性搞乱。我见过有人把AUTHORITY-CHECK包在一个函数里再判断返回值,做得过分了。新语法的优势要在字符串判断、内表操作那些场景才能真正体现,后面会提到。
3. 把某个用户的权限对象值全部捞出来
3.1 授权存储模型:直接授权与配置文件授权
如果你想去读表,必须先搞清楚SAP权限在这表里到底怎么存的。权限值其实分散在两套体系里。
第一套是用户直接授权。就是管理员在SU01的“权限”页签里,直接给用户添加的个人授权。这些数据存在USR10(授权对象头)和USR12(对象字段值)里。USR12是核心,它每个字段存储着一行“对象-字段-低值-高值”的记录。
第二套是配置文件授权。用户通过角色拿到的权限,在系统底层其实转化成了配置文件(Profile)。用户和配置文件之间的关系在UST04表里,配置文件的权限值在UST12表里。我们通过PFCG创建角色、分配事务代码和权限对象,生成的就是这一类。
所以当你需要获取一个用户的完整权限值清单时,不能只查USR12,因为大部分用户的权限其实来自于角色,而角色的值在UST12。正确做法是两边都查,然后合并去重。
USR12和UST12的字段结构基本一致,核心列是OBJCT(权限对象)、FIELD(字段)、LOW(低值)、HIGH(高值)。部分内核版本里还有VON等辅助列,但对我们开发来说,取前四个就够了。实际开发前建议先在SE11里打开这两张表看一眼字段说明,不要凭我这个记忆硬写。
3.2 用标准函数读取用户授权
SAP其实提供了几个读取用户授权的标准函数,比如SUSR_USER_READ、SUSR_USER_READ_OBJECTS,它们能把用户主记录、参数文件、授权对象都读出来。但这些函数在不同版本下签名变化比较大,对老项目的老内核兼容性差,而且返回结构很重,很多字段根本用不上。我早年在项目里用过一次,后来换系统版本后函数挂了,从此在自定义工具里我更偏向直接查表。
如果你确实想用标准函数,建议先在SE37里看函数签名和返回结构,确认你的系统版本支持,再做封装。别指望一套代码十年不变。
3.3 直接查授权表构造值清单
下面这个例子,是读当前用户对某个权限对象在某个字段上的所有授权值,合并了USR12和UST12两边:
FORM get_field_auth_values USING iv_user TYPE uname iv_object TYPE xfeld iv_field TYPE fieldname CHANGING ct_low TYPE STANDARD TABLE. DATA: lt_direct TYPE TABLE OF usr12, lt_profile_val TYPE TABLE OF ust12, lt_profiles TYPE TABLE OF ust04. " 1) 用户直接授权 SELECT objct field low high FROM usr12 INTO CORRESPONDING FIELDS OF TABLE lt_direct WHERE bname = iv_user AND objct = iv_object AND field = iv_field. " 2) 用户通过角色/配置文件获得的授权 SELECT profile FROM ust04 INTO TABLE lt_profiles WHERE bname = iv_user. IF lt_profiles IS NOT INITIAL. SELECT objct field low high FROM ust12 INTO CORRESPONDING FIELDS OF TABLE lt_profile_val FOR ALL ENTRIES IN lt_profiles WHERE profile = lt_profiles-profile AND objct = iv_object AND field = iv_field. ENDIF. " 3) 合并去重到输出表 LOOP AT lt_direct INTO DATA(ls_direct). APPEND ls_direct-low TO ct_low. ENDLOOP. LOOP AT lt_profile_val INTO DATA(ls_prof). APPEND ls_prof-low TO ct_low. ENDLOOP. SORT ct_low. DELETE ADJACENT DUPLICATES FROM ct_low. ENDFORM.这里我简化了一点:真实场景里权限值经常不是单一值,而是范围,比如LOW=1000,HIGH=2000。如果你要拿这批范围去过滤一个大数据集,建议做成RANGES表,而不是把所有值展开成单值列表。RANGES表在ABAP里可以直接用在SELECT的WHERE条件中,效率比单值展开好得多。
3.4 做成业务友好的权限值清单
读表拿到的是原始数据,直接丢给业务界面是不行的。我在工具类里会进一步组装成三层结构:对象 -> 字段 -> 值列表。这样在界面上展示时,可以清晰看到用户对S_TCODE有哪些事务代码权限,对M_MSEG_WWA在工厂字段上有哪几个工厂。
TYPES: BEGIN OF ty_auth_tree, objct TYPE xfeld, field TYPE fieldname, values TYPE STANDARD TABLE OF string WITH EMPTY KEY, END OF ty_auth_tree.这里用到了新版ABAP的表类型声明语法。新语法在组装这种嵌套结构时非常方便,老语法的话你得定义层层叠叠的内表,代码量至少多一倍。我建议新项目里能用新语法的地方尽量用,但权限校验语句本身还是传统写法为主。
4. 三个实战场景,把权限值用起来
4.1 BAPI_SALESORDER_CREATEFROMDAT2 调用前的权限预校验
在项目里对接外部电商平台时,对方通过RFC调用BAPI_SALESORDER_CREATEFROMDAT2创建销售订单。BAPI本身不检查权限,所以我必须在自己封装的BAPI包装器里把权限校验补上,否则会造成越权创建订单的严重问题。
我的做法是,在调用BAPI之前,先根据订单抬头里的销售组织、分销渠道、产品组做权限校验:
METHOD create_order_wrapper. " 先校验用户对销售区域的权限 AUTHORITY-CHECK OBJECT 'V_VBAK_AUG' ID 'VKORG' FIELD is_header-vkorg ID 'VTWEG' FIELD is_header-vtwerg ID 'SPART' FIELD is_header-spart. IF sy-subrc <> 0. " 记录日志并返回友好的错误信息 rv_error = abap_true. RETURN. ENDIF. " 再调用标准BAPI CALL FUNCTION 'BAPI_SALESORDER_CREATEFROMDAT2' EXPORTING order_header_in = is_header IMPORTING salesdocument = lv_salesdoc TABLES return = lt_return. ENDMETHOD.这里有个细节:权限校验不通过时不要直接丢异常,而是要把失败上下文返回给调用方,因为很多外部系统需要知道具体是什么原因拒绝的。如果外部系统能拿到“你在分销渠道10上没有权限”,对方的运维就能直接找SU01去改权限,效率高很多。所以我前面强调要拆开记录SY-SUBRC,而不是笼统判断非0即错。
4.2 ME55审批增强里按工厂和金额范围校验
最近一段时间很多人搜“abap me55审批增强校验”,这其实是采购申请集中审批的增强点。ME55审批时,审批人可能在不同工厂有不同的审批权限,甚至还有金额边界。审批增强里经常要做的事情就是:拿到待审批单据的工厂和金额,去判断当前审批人是否有权限审批。
这里我建议用AUTHORITY-CHECK按单据的工厂逐条校验,因为审批的待处理量一般不大,性能无忧。如果把所有工厂权限值一次性读出来再对比,反而要处理范围和去重逻辑,性价比低。但有一种情况必须要读值:审批按钮要按审批人可审批的工厂范围去动态控制状态,比如一张申请单跨越两个工厂,其中一个工厂不在审批人权限内,这时就要提示先转给其他审批人。这种场景下,把用户在某权限对象工厂字段上的授权值读取出来,再跟单据工厂比对,才能给出准确提示。这也是热词里“me55审批增强校验”的实际落地需求。
我写过一个增强片段,先读当前用户在采购申请审批相关权限对象上的工厂授权值,再用待审批单据的工厂去做匹配:
SELECT DISTINCT low FROM usr12 INTO TABLE @DATA(lt_auth_werks) WHERE bname = @sy-uname AND objct = 'M_BANF_FRG' AND field = 'WERKS'.注意,权限对象的具体名字在不同项目里可能不一样,有的是标准对象,有的是客户自定义对象,写之前一定要去SE11里确认,别直接抄网上的对象名。这也是一个很常见的坑。
4.3 报表结果集按权限对象值动态过滤
做物资库存报表时,领导要求每个用户只能看到自己有权查看的工厂数据。如果跑完报表之后每行都做权限检查,数据量大时用户会等到抓狂。正确做法是,在查询之前把用户在当前工厂字段上的授权值取出来,拼成RANGES表,直接在SQL查询里过滤。
DATA: lt_factory TYPE RANGE OF werks. " 读取用户在工厂字段上的授权值,拼RANGES lt_factory = build_auth_range( iv_user = sy-uname iv_object = 'M_MSEG_WWA' iv_field = 'WERKS' ). IF lt_factory IS INITIAL. " 没有工厂授权,直接返回空结果 RETURN. ENDIF. SELECT werks matnr lgort meins FROM mchb INTO CORRESPONDING FIELDS OF TABLE lt_result WHERE werks IN lt_factory.这里有个关键判断:如果用户只有单个工厂的授权,拼出来就是等值条件;如果授权是范围,比如1000到1999,那就要用BETWEEN,或者干脆在SQL里用IN。RANGES表只支持符号(SIGN)加选项(OPTION),把LOW-HIGH值填进去最省事。拼的时候要设置OPTION:
ls_factory-sign = 'I'. ls_factory-option = 'EQ'. ls_factory-low = ls_auth-low.如果读表时发现HIGH不为空,就把OPTION改成‘BT’。这种做法代码量不大,但能显著降低数据库查询压力。
4.4 顺手写一个纯数字判断小函数
最后一个很实用的小工具,来自经常搜到的“abap判断字符串是否是数字”。为什么在权限主题里提到它?因为在批量导入权限数据或者外部系统传参时,权限值有时是数字,有时是字母开头的组合,比如工厂号一般是数字,但移动类型也是纯数字。你在校验用户输入或者解析上传文件时,要先把字符串是否是数字判断出来,再决定用数字型还是字符型逻辑处理。
新版ABAP里最简单的写法是:
TRY. DATA(lv_num) = CONV int4( iv_value ). rv_is_number = abap_true. CATCH cx_root. rv_is_number = abap_false. ENDTRY.也可以用一个更正则的方式:
DATA(lv_is_number) = xsdbool( cl_abap_matcher=>matches( pattern = '^\d+$' text = iv_value ) ).两种写法各有好处:TRY的方式比较直接,适合做数值转换前的预校验;正则的方式更灵活,比如你只想要非负整数,直接改正则表达式就行。在权限导入工具里,我一般是先判断是否数字,再用CONV转成数值型字段去查工厂主数据存在性,这样能避免类型冲突。
5. 常见坑与排查技巧实录
5.1 坑一:权限对象字段名拼错,SY-SUBRC=8
AUTHORITY-CHECK返回8,最常见的原因就是权限对象名或字段名拼错了。尤其一些冷门对象,字段名缩写规则很怪,比如MBWWA来自“Bewegungsart Ware”这类德语缩写,靠猜很容易猜错。我再三强调:写权限相关代码之前,先去SE11输入对象名,双击字段列表,把字段名复制出来用。开发环境没有权限检查问题不大,上到生产环境后发现所有用户全部返回8,排查起来非常尴尬。
5.2 坑二:读表时忽略LOW/HIGH范围对应关系
直接读USR12或UST12时,最开始我犯过一个错误:只取了LOW字段,把HIGH有值的范围授权当成单值处理。比如某用户工厂字段授权是1000到2000,我读出来却只有1000,导致报表只查了1000一个工厂。这个问题尤其容易出现在角色通过范围授权的情况下。所以读表的逻辑必须处理HIGH非空的情况:要么转成BT选项,要么在代码里把HIGH也带出来参与比较。
5.3 坑三:别在生产上裸查授权表
在大批量接口或者频繁联机操作里,每次都去查USR12/UST12,性能会很差。我的经验是,能用AUTHORITY-CHECK解决的就别读表;必须读表的,缓存结果到内存或者共享内存,权限值很少变,一次登录会话内完全没必要反复查。另外,查询条件一定要带用户和对象字段,千万别全表扫描然后再代码过滤,那在几百万行的UST12上会让数据库直接冒出压力告警。
5.4 调试技巧:SU53、ST01和断点组合
定位权限问题,我常用的三件套:SU53、ST01和DEBUG断点。
最实用的是SU53。如果一个事务在运行过程中权限检查失败,系统会把失败信息保存在内存里,你回到SE38或任意项目里执行事务代码SU53,就能看到最近一次权限检查失败的详细信息:哪个权限对象、哪个字段、传入值是多少、授权值范围是什么。这比瞎猜SY-SUBRC快得多。
DEBUG也有一个技巧:在AUTHORITY-CHECK之后那一行程序上设置断点,断点停住后看SY-SUBRC是不准确的,因为SY-SUBRC在后续语句执行时会被覆盖。正确做法是把返回值立刻存到自定义变量里,再去看变量值。我见过太多人断点看SY-SUBRC,看到的是后面某条语句的结果,误判成权限检查通过。
最后再补充一个排查权限读表问题的小技巧:当你从USR12/UST04拼出来权限值数量和你用SU01看到的授权明显对不上时,先检查该用户是否有通过继承或复合角色传递的权限,这种情况下单纯查UST04可能拿不全。我一般会用事务代码SUIM的权限对比功能,把系统计算的用户实际权限值和代码里读出来的做对照,很快能定位到底是哪一层漏了。
这套权限读取和校验的方法,我在项目里已经反复用过很多轮,从最初只会写一个AUTHORITY-CHECK,到后来能够熟练处理范围授权、多配置文件合并、批量导入场景,过程中踩过的坑基本都写在上面了。以后你要是再遇到权限相关的开发任务,先问自己一个问题:我是只需要判断一个值能不能过,还是需要把授权值全部拿出来用?答案不同,技术路线完全不同,别一上来就写AUTHORITY-CHECK或者一上来就查表。