# security.action

用户登录、初始化权限,以及维护用户、机构、权限等事件时调用此脚本中的钩子函数。

  1. 此钩子脚本全局唯一,在sysdata项目中,位于/sysdata/settings/hooks/security.action
  2. 可以直接编辑security.action文件,也可以在SuccIDE或元数据项目设置中通过脚本编辑器直接编辑 ts语法的脚本文件security.action.ts,编辑器会自动编译并生成security.action

# 脚本模版

下面的钩子函数按需实现,需要实现哪个就把它的注释去掉即可。

/**
 * 此脚本定义系统登录、权限初始化相关的一些事件脚本。
 * 
 * 1. 此脚本全局唯一,在`sysdata`项目中,位于`/sysdata/settings/hooks/security.action`。
 * 2. 推荐直接编辑`security.action.ts`,可以有语法提示,系统会自动编译生成`security.action`。
 * 3. 当【系统设置】-【单点登录】-【脚本定制】启用后这里还可以实现sso扩展接口中所有定义的接口,
 * 	具体见: https://docs.succbi.com/dev/extension-points/sso/

 */
/**
 * 当一个用户使用用户名密码方式登录时校验账号密码时候调用。
 *
 * 注意:
 * 	1. 当执行此回调函数时,用户还未登录成功,系统会根据此函数返回的结果决定后续的操作
 * 	2. 用户ID为 `admin` 的用户登录时不受这个脚本函数的影响
 * 
 * @param request
 * @param response 这个属性不再使用
 * @param userId 用户输入的用户ID或者手机号
 * @param password 明文密码
 * @param userDirectory 用户目录。用于标识用户的来源,系统内置的区分有:`sys`表示系统用户,`external`表示外部用户
 * @returns
 * 	1. 如果明确返回`UserInfo`对象,那么将绕过系统默认的登录验证逻辑,直接使用返回的用户信息登录。  
 * 	2. 如果抛出异常,那么将导致用户登录失败,异常信息会显示给用户,可用于进行额外的登录验证判断。  
 * 	3. 如果没有返回值,那么将进行系统默认的登录校验 
 */
// function verifyLoginPassword(request: HttpServletRequest, userId: string, password: string, userDirectory: string): UserInfo | void {
// }

/**
 * 根据用户ID获取登录用户
 *
 * 调用位置说明:只用于无密码登录的时候获取用户信息
 *
 * 广义需求说明: 将系统集成到第三方系统的时候,为了避免在多个地方维护用户信息,经常需要只在第三方系统存储用户,提供这个函数用于从第三方系统获取用户信息
 *
 * 业务使用场景:
 * 1. 使用数字证书(https://docs.succbi.com/dev/ca-token/)方式登录系统的时候,会将用户ID作为待加密内容,系统解密出用户ID后,将调用本方法用于查找用户
 *
 * @param userId 用户ID
 * @param userDirectory 用户目录。用于标识用户的来源,系统内置的区分有:`sys`表示系统用户,`external`表示外部用户
 * @since 4.19.3
 * @returns 返回用户信息
 * 		如果没有返回值, 则会尝试从系统用户库中读取
 * 		如果返回null, 则代表没有查找到用户
 * 		如果抛出异常,则视为返回null,并在控制台打印 error级别错误日志(在控制台日志界面,将登录日志级别调整为error即可查看异常详情)
 */
// function verifyLoginUser(request: HttpServletRequest, userId: string, userDirectory: string): UserInfo | void {
// }

/**
 * 用于在登录过程中动态决定一个用户属于哪些用户组。
 * 
 * 1. 此函数执行的时机是,用户完成登录校验后,并初始化用户的权限时
 * 2. 如果此函数存在,那么系统将完全信任此函数返回的信息,默认不读取用户组用户关系表中的用户组信息
 * 3. 用户ID为 `admin` 的用户登录时不受这个脚本函数的影响
 * 
 * @param userInfo 用来登录的用户信息
 * @returns 
 * 	1. 用户组ID数组(是一个字符串数组),代表当前用户将被分配到这些用户组里面去
 *  2. 字符串,表示用户属于哪个用户组
  * 3. undefined,或者抛出异常:表示使用系统默认的用户组逻辑
 *  3. 其它值将被忽略,系统完全信任此函数返回的信息,默认不读取用户组用户关系表中的用户组信息
 */
// function resolveUserGroups(userInfo: UserInfo): Array<string> | string {
// }
/**
 * 当一个用户登录成功后,执行一次此钩子函数。可以通过此钩子函数修改当前用户的登录信息(包括权限列表等)。
 *
 * 如果抛出异常,那么将导致用户登录失败,可用于进行额外的登录验证判断。
 * 
 * 属于用户组`admin`的用户在登录时候,不受此函数影响
 */
// function onDidSignin(request: HttpServletRequest, response: HttpServletResponse, user: CurrentUser): void {
// }

/**
 * 当使用url上带access_token参数进行登录操作时用于校验access_token
 * 
 * 这个函数会在系统默认access_token库中没有找到对应access_token的情况下进行调用
 * 
 * 若没有返回值,则视为脚本没有对access_token进行校验
 * @param access_token 
 * @returns
 */
// function onAccessTokenSignin(access_token: string): {
// /** 是否校验成功 */
// 	valid: boolean;
// /** 
//  * 与 access_token 绑定的用户,用户信息请尽可能的完整,以方便系统中使用
//  * 
//  * 需要说明的是,校验成功的情况下必须返回用户信息,否则仍然视为校验失败
//  */ 
// 	user?: UserInfo;
// 	/** 校验错误码
// 	 * 系统内置错误码有:
// 	 *  tokennotexists: 代表系统中没有找到对应的access_token 
// 	 *  used: 代表 access_token 已经被使用过了
// 	 *  invalid: 代表 access_token 的使用方式不对,不能用来登录
// 	 *  expired : 代表 access_token 过期了
// 	 *  usernotexists: 代表 access_token绑定的用户在不存在
// 	 *  exception:代表 access_token 校验脚本出现问题
// 	 */
// 	errorCode?: string;
// /** 错误消息,这个将会打印在系统的控制台中,但需要设置控制台的日志查看级别为debug */
// 	errorMessage?: string;
// } {
// }

/**
 * 用于加密用户的明文密码
 * 
 * 返回值约定:
 * 	- 在字符串的开头用 `{Alogrithm}` 用于指定所使用的加密算法,
 * 	- 如果不指定,则代表使用的是系统默认的加密算法(sha1)
 * 	- 也可以指定 `{script} `作为前缀来指定是使用此脚本函数进行加密
 * 	- 如果指定的加密算法为系统自带的算法(`sha1`;`md5`;`sm3`),进行校验的时候,则会使用系统内部的对应算法实现进行校验,其他的算法都会调用此脚本函数来进行校验
 * 	比如 :
 * 		- `{md5}afhasiohfo` 代表用 `md5` 算法加密后的字符串
 * 		- `{sm3}iugbasdfjiojda` 代表用 `sm3` 算法加密后的字符串
 * 		- `iugbasdfjiojda` 和 `{sha1}iugbasdfjiojda` 都代表用 `sha1` 算法加密后的字符串
 * 		- `{script}sjiogjs` 代表用此脚本函数的实现来进行加密
 * 		- `{sm4}wergtyhsdfg` 代表用 `sm4` 算法加密,并且实现之后的脚本
 * 
 * @param str 待进行加密的秘钥字符串
 * @returns 加密后的字符串
 */
// function encryptUserPassword(str: string): string{
// }

/**
 * 用于对比输入的明文密码是否为数据库记录密码
 * 
 * 当校验密码检查到记录的用户密码使用加密方式不为系统自带的加密方式( `sm3` , `sha1` , `md5` )时,就会调用本函数来进行判断
 * @param plainPassword 未加密前的用户密码字符串或者密码加盐字符串
 * @param encryptedPassword 期望加密后的字符串
 */
// function compareUserPassword(plainPassword: string, encryptedPassword: string):boolean {
// }

/**
 * 用于使用 dwapi 向数据表中更新或者插入数据的时候加密数据
 *
 * 注意:
 * 1. 脚本中可以实现多个加密算法,请通过后缀名的方式进行区分,比如
 *      encryptAlgorithm_DES 代表使用 DES加密算法实现
 *      encryptAlgorithm_3DES 代表使用 3DES加密算法实现
 *
 * @param plainText 待加密的字符串
 * @return 加密后的字符串
 */
// function encryptAlgorithm_xxx(plainText: string): string {
// }

/**
 * 用于使用dwapi查询数据的时候,解密根据{@link encryptAlgorithm_xxx(string)}函数加密数据
 *
 *  注意:
 * 1. 脚本中可以实现多个加密算法的解密实现,请通过后缀名的方式进行区分,比如
 *      encryptAlgorithm_DES 代表使用 DES加密算法的解密实现
 *      encryptAlgorithm_3DES 代表使用 3DES加密算法的解密实现
 * 2. 请保证与 {@link encryptAlgorithm_xxx(string)} 成对出现,避免数据被加密后,无法正确被解密,导致数据无法正常使用
 * @param encryptedText 被加密的字符串
 * @return 解密后的字符串
 */
// function decryptAlgorithm_xxx(encryptedText: string): string {
// }

/**
 * 接收通知,处理支付订单已经完成支付
 *
 * 注意:
 * 1. 系统会自动根据`支付场景`去修改订单支付状态,内部实现时也就没有必要再修改状态
 * @param orderId 支付订单
 * @param appid 发起支付对应的appid
 * @param scenarioId 应用中所配置支付场景id
 * @param receiptMessage 来自第三方系统支付成功后的回调信息
 */
// function notifyOrderPaid(orderId: string, scenarioId: string, receiptMessage?: JSONObject): void {
// }

/**
 * 判断权限
 *
 * 调用场景:
 * 1. 判断用户是否有对应权限
 * 2. 获取用户对一个资源可用的数据范围
 *
 * 使用场景:
 * 1. 当登录时候无法确定用户已经对哪些用户进行了权限分配的情况,需要开发者根据业务来判断权限,比如:对一个新添加帖子,即时邀请在线人员参与审核和编辑
 *
 * 不受此钩子函数影响的范围有:
 * 1. 当前登录为系统管理员用户:可操作任何系统资源
 * 2. 公开目录下的资源完全公开:查看时不需要任何权限判断
 *
 * 使用注意:
 * 1. 只有已经有登录用户的情况,才会调用到到这个函数
 * 2. 对于返回数据范围JSON的说明:
 *    此方法不受项目设置中关于数据范围维表的限制(https://docs.succbi.com/devops/project-manage/data-permission-settings/)
 * 		具体体现在如果这里返回指定的维表为地区维度,限定访问湖北省的数据,即使项目数据范围维表中没有设置这个地区维度,这个数据范围仍旧会作用在查询结果中
 * 3. 此方法仅适用于直接查看或者操作一个资源时的权限判断,对于系统提供的默认元数据界面中展示的资源结构,都只会受到用户本身在系统中分配的权限影响
 * @param path 资源的路径(元数据路径)
 * @param operation 权限操作,参考:https://docs.succbi.com/permission/operations
 * @returns
 * 		返回 true , 代表有权限,并且没有数据范围的限制
 * 		返回 false: 代表没有权限
 * 		没有返回值或者null: 代表需要使用系统默认的权限判断逻辑
 * 		返回数据范围结构: 代表用户有对应的权限,并且针对这个权限操作存在对应的数据范围限制,
 * 				数据范围结构为数组的原因是多个条件为 OR 关系 可组成: 可查看`湖北省-食品行业`和`湖南省-计算机行业`
*/
// function checkPermission(path: string, operation: string): boolean | undefined | Array<{
// 	/**
// 	 * 键为维表对应的元数据ID
// 	 * 值为维表的取值, 比如:["420000"] 代表可以取到湖北省的数据
// 	 *
// 	 * 多个维表共同作用标识 AND 关系:
// 	 *  比如 行业维度和地区维度可以组成: 查看 `湖北省-食品行业`
// 	 */
// 	[dim: string]: Array<string>;
// }> {
// 	return
// }

# 示例

# 1. 与SuccezBI3.x单点登录

详见:与SuccezBI3.x单点登录

# 2. 自定义用户组权限

根据用户的某些特殊属性(如用户表的某个字段、或其他数据库表中的记录)在用户登录的时候动态决定用户的用户组。

此时可能无法使用系统的用户组自动匹配条件,因为匹配规则比较复杂(如需要取其他表的数据)或者用户表的数据随时可能修改,此时可以使用此钩子脚本中的resolveUserGroups扩展函数,在登录时自定义溶解用户组权限。

示例代码如下:
/**
 * 扩展点说明:
 *  用于在登录过程中动态决定一个用户属于哪些用户组。
 * 
 *      1. 此函数执行的时机是,用户完成登录校验后,并初始化用户的权限时
 *      2. 如果此函数存在,那么系统将完全信任此函数返回的信息,默认不读取用户组用户关系表中的用户组信息
 * 
 *  @param userInfo 用来登录的用户信息
 *  @returns 
 * 	    1. 用户组ID数组(是一个字符串数组),代表当前用户将被分配到这些用户组里面去
 *      2. 字符串,表示用户属于哪个用户组
 *      3. undefined,或者抛出异常:表示使用系统默认的用户组逻辑
 *      4. 其它值将被忽略,系统完全信任此函数返回的信息,默认不读取用户组用户关系表中的用户组信息
 * 
 * 业务说明:
 *      1. 角色在用户表里,字段:`YHZID`,为逗号分隔多值
 *      2. 需检查YHZID里的数据,避免越权:1)排除`admin`;2)根据用户表上所属机构类型(`YHLX`,单值,不可多选)判断,用于筛选合法用户组
 *      3. 目前用户组关系是固定的,为了避免性能损耗,登录要快,判断条件在代码中写死,不去数据库查映射关系,关系见`GROUPS`
 */
function resolveUserGroups(userInfo: UserInfo): Array<string> | string {
    //用户角色属性,逗号分隔多值
    let yhzids = userInfo.yhzid;
    //用户类型或所属机构类型属性
    let yhlx = userInfo.yhlx;
    //如果所属机构类型或用户组id存在空值,或者不在`GROUPS`关系里,则直接返回,走系统默认逻辑,避免后台设置的用户组掉了,比如开发管理员用户
    if (!yhlx || !yhzids || !GROUPS[yhlx]) {
        return;
    }
    //根据`GROUPS`关系,添加符合对应关系的用户组,对于不符合条件(如:`admin`)则全部排查掉,避免越权
    let groups = GROUPS[yhlx];
    let yhzidItems: string[] = yhzids.split(',');
    let result: string[] = [];
    for (let i = 0; i < yhzidItems.length; i++) {
        let yhzid = yhzidItems[i];
        if (groups.indexOf(yhzid) > -1) {
            result.push(yhzid);
        }
    }
    return result;
}

/**
 * 12个用户组与所属机构类型关系
 * 注意:此处场景固定,可以在代码写死关系,以加快速度,其他场景可根据实际情况选择查数据库表还是代码写死
 */
const GROUPS = {
    /** 采样单位 */
    '1': ['hsjc_sampling_sampling', 'hsjc_sampling_management'],
    /** 检测机构 */
    '2': ['hsjc_detection_detection', 'hsjc_detection_management'],
    /** 疾控中心 */
    '3': ['hsjc_center_management', 'hsjc_center_others', 'hsjc_center_detection'],
    /** 卫健委 */
    '4': ['hsjc_ad_management', 'hsjc_ad_ province_management','hsjc_ad_approval'],
    /** 重点场所 */
    '5': ['hsjc_important_management']
}

# 3. 为每个登录用户自动分配并创建个人文件夹

在用户第一次登录时,在指定的目录下创建以用户登录ID命名的文件夹,并对这个文件夹有完全控制权限。

需要提前准备的事项:

  1. 指定自动创建目录的文件夹,并提前创建好根目录:例如分析目录下提前创建根目录,名称为个人目录
  2. 限定某个用户组的用户登录,才会触发自动创建目录的功能:例如名称为内部用户的用户组,当用户登录时,在个人目录下自动创建目录,规则:
    • 目录名称:以登录用户的ID命名
    • 目录描述:登录用户的名称
    • 目录创建:同名目录已存在时不重复创建
    • 权限:登录用户对【个人目录】下,以登录ID命名的目录,有完全控制权限
示例代码如下:
/**
 * 当一个用户登录成功后,执行一次此钩子函数。可以通过此钩子函数修改当前用户的登录信息(包括权限列表等)。
 *
 * 如果抛出异常,那么将导致用户登录失败,可用于进行额外的登录验证判断。
 *
 * 属于用户组`admin`的用户在登录时候,不受此函数影响
 */
import meta from "svr-api/metadata";
function onDidSignin(request: HttpServletRequest, response: HttpServletResponse, user: CurrentUser): void {
    let groups: any = user.getGroups();

    let flag = false;
    if (groups) {
        let len = groups.size();
        for (let i = 0; i < len; ++i) {
            if ((<UserGroupInfo>groups.get(i)).groupId === '') {//1、限定指定用户组才能自动创建目录:去掉该判断条件,则所有用户登录都可以自动创建目录。若希望指定的用户组的用户登录时,才能自动创建目录,则在''中输入对应用户组的ID,不指定时则不对任何用户组生效
                flag = true;
                break;
            }
        }
    }
    if (flag) {
         let userInfo = user.userInfo;
        let folderName = userInfo.userId;// 创建的目录名称:这里取用户的登录ID作为目录名称,若需要拼接等,可以替换这里的内容
        let desc = userInfo.userName;// 创建的目录名称:这里取用户的登录ID作为目录名称,若需要拼接等,可以替换这里的内容
        let projectName = ''; // 2、需要自动创建目录的项目:若希望指定项目自动创建目录,则在''中输入对应项目ID,不指定时则不对任何项目生效
        let project = meta.getFile('/' + projectName);
        let pm = user.getPermissions();
        if (project != null) {
           //***********创建数据模块文件夹代码开始***********
           let path = '/' + projectName + '/data/tables/' + folderName;// 3、限定指定目录下创建文件夹:如果不想要直接放到 data(数据模块) 目录中,请修改/data/tables/为自定义目录,目录路径可右键文件夹属性中查看
            let dataFolder = meta.getFile(path, false);
            if (dataFolder != null) {
                meta.createFile({
                    name: folderName,
                    desc: desc,
                    isFolder: true,
                    path: path
                });
            }
            pm.addMgrPermission(path); 
           //***********创建数据模块文件夹代码结束***********
           //***********创建分析模块文件夹代码开始***********
            path = '/' + projectName + '/ana/' + folderName;// 4、限定指定目录下创建文件夹:如果不想要直接放到 ana(分析模块) 目录中,请修改/ana/为自定义目录,目录路径可右键文件夹属性中查看
            let anaFolder = meta.getFile(path, false);
            if (anaFolder == null) {
                meta.createFile({
                     name: folderName,
                    desc: desc,
                    isFolder: true,
                    path: path
                });
            }
            pm.addMgrPermission(path); 
        }
        user.setPermissions(pm); 
           //***********创建分析模块文件夹代码结束***********
    }
}
是否有帮助?
0条评论
评论