跳到主要内容

三级等保-安全登录

登录相关功能

JeecgBoot的登录系统完全满足三级等保要求,主要包括:

  • 双因子身份鉴别:采用密码加上验证码、证书或令牌等第二要素
  • 登录超时机制:30分钟无操作自动退出
  • 密码加密传输:使用国产SM4参数加密
  • 失败锁定保护:默认连续5次密码错误后锁定账号30分钟

三级等保登录相关的核心代码在后端类 : SecurityLoginService

双因子登录

系统默认使用邮箱作为第二因子,登录需要提供用户名、密码和邮箱验证码。实现通过 LoginService 中的 sendEmailCodevalidateEmailCode 方法完成。

登录超时自动退出

基于 sa-token 框架实现,在 Level3ProtectConfigService 中配置最低活跃时间,超过30分钟无操作则自动退出登录状态。

private void setProp(Level3ProtectConfigForm configForm) {
......
// 设置 最低活跃时间(单位:秒)
if (this.loginActiveTimeoutSeconds > 0) {
StpUtil.getStpLogic().getConfigOrGlobal().setActiveTimeout(getLoginActiveTimeoutSeconds());
} else {
StpUtil.getStpLogic().getConfigOrGlobal().setActiveTimeout(-1);
}
}

登录密码请求加密

前端通过 lib/encrypt.js 使用SM4或AES加密密码,后端使用 ApiEncryptService 进行解密处理。

 try {
SmartLoading.show();
// 密码加密
let encryptPasswordForm = Object.assign({}, loginForm, {
password: encryptData(loginForm.password),
});
const res = await loginApi.login(encryptPasswordForm);
// 登录成功
stopRefreshCaptchaInterval();
localSave(LocalStorageKeyConst.USER_TOKEN, res.data.token ? res.data.token : '');
message.success('登录成功');
//更新用户信息到pinia
useUserStore().setUserLoginInfo(res.data);
//构建系统的路由
buildRoutes();
router.push('/home');
} catch (e) {
...
...
}

登录失败处理

系统使用 t_login_fail 表记录失败次数。当连续失败达到5次时,账户被锁定30分钟。SecurityLoginServicecheckLogin 方法负责校验失败次数和锁定状态。

  • 表结构
CREATE TABLE `t_login_fail` (
`login_fail_id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增id',
`user_id` bigint NOT NULL COMMENT '用户id',
`user_type` int NOT NULL COMMENT '用户类型',
`login_name` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '登录名',
`login_fail_count` int DEFAULT NULL COMMENT '连续登录失败次数',
`lock_flag` tinyint DEFAULT '0' COMMENT '锁定状态:1锁定,0未锁定',
`login_lock_begin_time` datetime DEFAULT NULL COMMENT '连续登录失败锁定开始时间',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`login_fail_id`) USING BTREE,
UNIQUE KEY `uid_and_utype` (`user_id`,`user_type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='登录失败次数记录表';
  • 后端类: SecurityLoginService
public ResponseDTO<LoginFailEntity> checkLogin(Long userId, UserTypeEnum userType) {
// 若登录最大失败次数小于1,无需校验
if (level3ProtectConfigService.getLoginFailMaxTimes() < 1) {
return ResponseDTO.ok();
}
LoginFailEntity loginFailEntity = loginFailDao.selectByUserIdAndUserType(userId, userType.getValue());
if (loginFailEntity == null) {
return ResponseDTO.ok();
}
// 校验登录失败次数
if (loginFailEntity.getLoginFailCount() < level3ProtectConfigService.getLoginFailMaxTimes()) {
return ResponseDTO.ok(loginFailEntity);
}
// 校验是否锁定
if (loginFailEntity.getLoginLockBeginTime() == null) {
return ResponseDTO.ok(loginFailEntity);
}
// 校验锁定时长
if (loginFailEntity.getLoginLockBeginTime().plusSeconds(level3ProtectConfigService.getLoginFailLockSeconds()).isBefore(LocalDateTime.now())) {
// 过了锁定时间
return ResponseDTO.ok(loginFailEntity);
}
LocalDateTime unlockTime = loginFailEntity.getLoginLockBeginTime().plusSeconds(level3ProtectConfigService.getLoginFailLockSeconds());
return ResponseDTO.error(UserErrorCode.LOGIN_FAIL_LOCK, String.format(LOGIN_LOCK_MSG, loginFailEntity.getLoginFailCount(), level3ProtectConfigService.getLoginFailLockSeconds() / 60, LocalDateTimeUtil.formatNormal(unlockTime)));
}