安全功能
安全审计要求:系统应提供覆盖所有用户的安全审计功能,对系统重要安全事件(包括用户和权限的增删改、配置定制、审计日志维护、用户登录和退出、越权访问、连接超时、密码重置、数据的备份和恢复等系统级事件,及业务数据增删改、业务流程定制、交易操作中断等业务级事件)进行审计
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
审计模块实现
- 自定义注解标记在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;
}
}
- 自定义注解处理器
@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
- 先自定义一个转化类,继承自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>