安全测评之安全功能整改设计及代码

本文档详细阐述了如何在企业管理系统中实现全面的安全审计功能,包括用户操作记录、异常告警、审计数据存储策略,以及使用TypeHandler进行数据加解密的定制。通过自定义注解和AspectJ实现审计模块,确保数据完整性和安全性。
摘要由CSDN通过智能技术生成

安全功能

安全审计要求:系统应提供覆盖所有用户的安全审计功能,对系统重要安全事件(包括用户和权限的增删改、配置定制、审计日志维护、用户登录和退出、越权访问、连接超时、密码重置、数据的备份和恢复等系统级事件,及业务数据增删改、业务流程定制、交易操作中断等业务级事件)进行审计

1.审计数据产生:

要求:系统审计记录表 日期、时间、事件类型、用户身份(用户名和IP地址,且应具有唯一性标识)、事件描述和事件结果

2.审计查阅:

要求:系统应提供对审计数据进行查询、排序、分类、分析统计的功能,按照行为主体、时间、事件类型等属性

3.异常事件告警:

要求:系统应对异常事件根据严重程度进行等级划分,当异常事件发生时依据安全策略采用弹出告警窗、声光报警、短信通知、邮件通知等方式进行告警;
异常事件包括连续登录失败、越权访问、IP地址异常等

4.审计事件存储:

i 要求:系统应提供对审计数据进行手动或自动备份的功能,自动备份需记录审计日志
ii 要求:系统应保证6个月内的审计记录无法被修改、删除和覆盖,6个月或更早之前的审计记录可依据安全策略进行覆盖
iii 要求:系统应可设置审计记录存储的容量上限,至少为1G,并在容量即将达到上限时应进行告警,弹出告警窗、声光报警、短信通知、邮件通知等方式

审计模块参考

参考:

https://www.cnblogs.com/samlin/archive/2010/02/08/log-operation-management.html

https://blog.csdn.net/t_jindao/article/details/85259145

https://blog.csdn.net/he90227/article/details/44175365

https://www.cnblogs.com/hooray/archive/2012/09/05/2672133.html

系统操作日志设计:

我们在做企业管理系统时,有多多少少都有对数据的完整性有所要求,比如要求系统不能物理删除记录,要求添加每一条数据时都要有系统记录、或者更新某条数据都需要跟踪到变化的内容、或者删除数据时需要记录谁删除了,何时删除了,以便误删后可以通过系统的XXX功能来恢复误删的数据。

为什么要做操作日志?

其实上文也描述了一些,其主要目的就是跟踪到每一个用户在系统的操作行为,如对数据进行查询、新增、编辑或删除甚至是登录等行为。更进一步的理解可以说是对用户使用系统情况的跟踪,对数据的跟踪防止数据意外删除、更改时有所记录,有所依据,以便对数据的还原,从某种程序上可以保护数据的完整性。

3.异步插入数据库日志记录

https://blog.csdn.net/hightrees/article/details/78765580

4.系统错误日志实现

https://blog.csdn.net/weixin_42456466/article/details/89672890

获取客户端ip地址

https://blog.csdn.net/u011521890/article/details/74990338

审计模块实现

  1. 自定义注解标记在handler方法上
@OperationLogger(name = "用户登录", table = OperationTable.USER, operationType = OperationType.SPEC)

@OperationLogger(name = "退出登录", table = OperationTable.USER, operationType = OperationType.SPEC)

@OperationLogger(name = "添加用户", table = OperationTable.USER, operationType = OperationType.ADD)

@OperationLogger(name = "修改密码", table = OperationTable.USER, operationType = OperationType.UPDATE,idRef = 0)

@OperationLogger(name = "批量添加用户", table = OperationTable.USER, operationType = OperationType.UPDATE)  

@OperationLogger(name = "编辑用户信息", table = OperationTable.USER, operationType = OperationType.UPDATE,idRef = 0)

@OperationLogger(name = "删除用户", table = OperationTable.USER, operationType = OperationType.DELETE,idRef = 0)

@OperationLogger(name = "用户注册", table = OperationTable.USER, operationType = OperationType.SPEC)

@OperationLogger(name = "忘记密码", table = OperationTable.USER, operationType = OperationType.SPEC)
@RestController
@RequestMapping("/user")
public class UserController{
    @Anonymous
    @RequestMapping("login.do")
    @OperationLogger(name = "用户登录", table = OperationTable.USER, operationType = OperationType.SPEC)
    public ResponseData login(){
	     ResponseData responseData = new ResponseData<>();
       try {
           // todo 登录操作;
           if(/**登录成功*/){
			    responseData.setStatus(ServiceResponseCodeEnum.SYS_SUCCESS.getCode());
         	  	responseData.setMsg(ServiceResponseCodeEnum.SYS_SUCCESS.getMsg());         
       	   }else{
              responseData.setStatus(ServiceResponseCodeEnum.SYS_FAILED.getCode());
         	  	responseData.setMsg(ServiceResponseCodeEnum.SYS_FAILED.getMsg());
           }
       }catch (Exception e) {
         responseData.setStatus(ServiceResponseCodeEnum.SYS_FAILED.getCode());
         responseData.setMsg(e.getMessage());
       }
       return responseData;
    }
}
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLogger {
    /** 操作业务名 */
    String name();

    /** 操作表名 */
    OperationTable table();

    /** 操作人 id */
    long operatorId() default 0L;

    /** 操作人名称 */
    String operatorName() default "";

    /** 不需要记录的字段 */
    String[] column() default {};

    /** 操作类型 操作类型(添加ADD,删除DELETE,修改UPDATE)*/
    OperationType operationType();

    String desc() default "";

    /** 0业务日志;1系统日志 */
    byte type() default 0;

    /** 异常事件严重程度 */
    byte level() default 1;

    /** 操作业务id */
    int idRef() default -1;

    /** 成功不记录,只记录失败操作*/
    boolean toLogger() default false;
}
public enum OperationType {
    SPEC((byte)0, "SPEC"),
    ADD((byte)1, "ADD"),
    DELETE((byte)2, "DELETE"),
    UPDATE((byte)3, "UPDATE"),
    private final Byte code;
    private final String type;

    OperationType(Byte code, String type) {
        this.code=code;
        this.type=type;
    }

    public Byte getCode() {
        return code;
    }
    public String getType() {
        return type;
    }
}
public enum OperationTable {
    USER(1L, "user"),
    DEVICE(2L, "device");
    private final Long id;
    private final String name;

    OperationTable(Long id, String name) {
        this.id=id;
        this.name=name;
    }

    public Long getId() {
        return id;
    }
    public String getName() {
        return name;
    }
}
  1. 自定义注解处理器
@Aspect
@Component
public class OperationLogAop {
    Logger logger = LoggerFactory.getLogger(OperationLogAop.class);
    @Autowired
    ILoggerService loggerService;

    //Controller层切点
    @Pointcut("@annotation(com.xyz.log.OperationLogger)")
    public void controllerAspect() {
    }

    @Around("controllerAspect()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        ResponseData result = null;
        try {
            // 记录操作日志...谁..在什么时间..做了什么事情..
            result = (ResponseData) joinPoint.proceed();
            // 在操作失败时候也记录操作人id
            Long id = 0L;
            if(result != null){
                if(result.getData() instanceof UserDetailVo){
                    id = ((UserDetailVo) result.getData()).getId();
                } else if(result.getData() instanceof UserServiceBean){
                    id = ((UserServiceBean) result.getData()).getId();
                }
                handleLog(joinPoint, result, id);

            }
        } catch (Throwable e) {
            // todo
            // 异常处理记录日志..log.error(e);
        }
        return result;
    }
    private void handleLog(JoinPoint joinPoint,ResponseData result,Long operatorId) {
        Map<String,Object> nameAndArgs = null;
        Long operationKey = null;
        String status = result.getStatus();
        try {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String ip = IpUtil.getIpAddr(request);
            //获取横切的方法名
            String methodName = joinPoint.getSignature().getName();
            //获取拦截的class
            String classType = joinPoint.getTarget().getClass().getName();
            //加载这个类
            Class<?> clazz = Class.forName(classType);
            //获取这个类上的方法名
            Method[] methods = clazz.getDeclaredMethods();
            // token中放了uid,在拦截器校验token时候setsetAttribute("uid", token中uid)
            String uid = (String) request.getAttribute("uid");
            uid = uid == null? "0":uid;
            for (Method method:methods){
                // 这个方法上面的注解是否含有自定义的注解
                // 并且方法名等于切点访问的方法名
                if (method.isAnnotationPresent(OperationLogger.class)
                        &&method.getName().equals(methodName)){
                    //获取method的注解
                    OperationLogger operationLogger = method.getAnnotation(OperationLogger.class);
                    HelmetOperationLogDto logDto = new HelmetOperationLogDto();
                    //获取用户请求方法的参数并序列化为JSON格式字符串
                    Object[] args = joinPoint.getArgs();
                    if(null != args && args.length > 0) {
                        //获取参数名称和值
                        nameAndArgs = getParameMap(joinPoint);
                        int idRef = operationLogger.idRef();
                        if(idRef != -1){
                            operationKey = (Long) args[idRef];

                        }
                        if(operationLogger.column().length != 0){
                            for (String s : operationLogger.column()) {
                                nameAndArgs.remove(s);
                            }
                        }
                    }
                    logDto.setName(operationLogger.name());
                    logDto.setTableId(operationLogger.table().getId());
                    logDto.setTableName(operationLogger.table().getName());
                    logDto.setOperatorId(Long.valueOf(uid).equals(0L)?operatorId:Long.valueOf(uid));
                    logDto.setOperationType(operationLogger.operationType().getCode());
                    logDto.setOperationTime(new Date());
                    logDto.setOperationIp(ip);
                    logDto.setType(operationLogger.type());
                    logDto.setNameAndArgs(nameAndArgs);
                    logDto.setOperationKey(operationKey);
                    logDto.setLevel(operationLogger.level());
                    if(ServiceResponseCodeEnum.SYS_SUCCESS.getCode().equals(status)){
                        logDto.setOperationDesc("操作成功");
                    } else if(ServiceResponseCodeEnum.SYS_FAILED.getCode().equals(status)){
                        logDto.setOperationDesc("操作失败");
                        logDto.setLevel((byte)2);
                        result.setData(null);
                    } else if(ServiceResponseCodeEnum.PERMISSION_NOT_RIGHT.getCode().equals(status)){
                        logDto.setOperationDesc("越权访问");
                        logDto.setLevel((byte)3);
                        result.setData(null);
                    } else if(ServiceResponseCodeEnum.FAILED_LOGIN.getCode().equals(status)){
                        logDto.setOperationDesc("登录失败");
                        result.setData(null);
                        result.setStatus(ServiceResponseCodeEnum.SYS_FAILED.getCode());
                    } else if(ServiceResponseCodeEnum.FAILED_LOGIN_REPEATED.getCode().equals(status)){
                        logDto.setOperationDesc("连续登录多次失败");
                        logDto.setLevel((byte)3);
                        result.setData(null);
                        result.setStatus(ServiceResponseCodeEnum.SYS_FAILED.getCode());
                    } else if(ServiceResponseCodeEnum.FAILED_LOGIN_LOCK.getCode().equals(status)){
                        logDto.setOperationDesc("连续登录多次失败, 账号已经锁定");
                        logDto.setLevel((byte)3);
                        result.setData(null);
                        result.setStatus(ServiceResponseCodeEnum.SYS_FAILED.getCode());
                    }
                    loggerService.addLog(logDto);
                }
            }
        } catch (Exception e) {
            logger.error("记录用户操作日志失败!", e);
        }


    }



    public Map<String, Object> getParameMap(JoinPoint joinPoint) {
        Map<String, Object> map = new HashMap<String, Object>();

        Object[] args = joinPoint.getArgs(); // 参数值
        String[] argNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames(); // 参数名

        for (int i = 0; i < argNames.length; i++) {
            if ("response".equals(argNames[i]) || "request".equals(argNames[i]) || "password".equals(argNames[i])) {
                continue;
            } else {
                map.put(argNames[i], args[i]);
            }
        }

        return map;
    }



}

使用TypeHandler实现数据的加解密转换

https://www.cnblogs.com/wangjuns8/p/8688815.html

  1. 先自定义一个转化类,继承自BaseTypeHandler
public class AESEncryptHandler extends BaseTypeHandler{
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, AESUtil.encrypt((String)parameter));
    }

    @Override
    public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String columnValue = rs.getString(columnName);
        return AESUtil.decrypt(columnValue);
    }

    @Override
    public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String columnValue = rs.getString(columnIndex);
        return AESUtil.decrypt(columnValue);
    }

    @Override
    public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String columnValue = cs.getString(columnIndex);
        return AESUtil.decrypt(columnValue);
    }
}

2)在resultMap上加引用,对应从数据库里取数据时转换成JAVA对象时的解密

–> 在mapper.xml里的对应字段上加上引用

<arg column="code" jdbcType="VARCHAR" javaType="java.lang.String" typeHandler="com.xyz.handler.AESEncryptHandler"/>
<arg column="user_phone" jdbcType="VARCHAR" javaType="java.lang.String" typeHandler="com.xyz.handler.AESEncryptHandler"/>
<resultMap id="UserByTaskResultMap" type="com.xyz.dto.user.UserByIdDto">
    <result property="id" column="id"/>
    <result property="code" column="code" typeHandler="com.xyz.handler.AESEncryptHandler"/>
    <result property="username" column="username"/>
    <result property="num" column="num"/>
</resultMap>

所有涉及user表的code,user_phone字段都在这个mapper的resultMap映射关系中加上typeHandler,进行解密

3)在sql语句中加入引用,对应从JAVA对数据库传递数据的加密动作(所有用到敏感信息的地方都要加)

insert和update语句

<if test="code != null" >
#{code,jdbcType=VARCHAR,typeHandler=com.xyz.handler.AESEncryptHandler},
</if>
<if test="userPhone != null" >
#{userPhone,jdbcType=VARCHAR,typeHandler=com.xyz.handler.AESEncryptHandler},
</if>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值