/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
*
* 1. This software is for development use only under a valid license
* from BladeX.
*
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
*
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
*
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
*
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
*
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.auth.handler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.common.cache.CacheNames;
import org.springblade.common.constant.TenantConstant;
import org.springblade.core.launch.props.BladeProperties;
import org.springblade.core.oauth2.exception.ExceptionCode;
import org.springblade.core.oauth2.handler.AbstractAuthorizationHandler;
import org.springblade.core.oauth2.props.OAuth2Properties;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.provider.OAuth2Validation;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.tenant.BladeTenantProperties;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.*;
import org.springblade.system.cache.ParamCache;
import org.springblade.system.cache.SysCache;
import org.springblade.system.pojo.entity.Tenant;
import java.time.Duration;
import java.util.Date;
import java.util.List;
import static org.springblade.auth.constant.BladeAuthConstant.FAIL_COUNT;
import static org.springblade.auth.constant.BladeAuthConstant.FAIL_COUNT_VALUE;
/**
* BladeAuthorizationHandler
*
* @author BladeX
*/
@Slf4j
@RequiredArgsConstructor
public class BladeAuthorizationHandler extends AbstractAuthorizationHandler {
private final BladeRedis bladeRedis;
private final BladeProperties bladeProperties;
private final BladeTenantProperties tenantProperties;
private final OAuth2Properties oAuth2Properties;
/**
* 自定义弱密码列表
*/
private static final List WEAK_PASSWORDS = List.of("admin", "hr", "manager", "boss");
/**
* 认证前校验
*
* @param request 请求信息
* @return boolean
*/
@Override
public OAuth2Validation preValidation(OAuth2Request request) {
if (request.isPassword() || request.isCaptchaCode()) {
// 生产环境弱密码校验
if (bladeProperties.isProd() && isWeakPassword(request.getPassword())) {
return buildValidationFailure(ExceptionCode.INVALID_USER_PASSWORD);
}
// 判断登录是否锁定
OAuth2Validation failCountValidation = validateFailCount(request.getTenantId(), request.getUsername());
if (!failCountValidation.isSuccess()) {
return failCountValidation;
}
}
return super.preValidation(request);
}
/**
* 认证前失败回调
*
* @param validation 失败信息
*/
@Override
public void preFailure(OAuth2Request request, OAuth2Validation validation) {
// 增加错误锁定次数
addFailCount(request.getTenantId(), request.getUsername());
log.error("用户:{},认证失败,失败原因:{}", request.getUsername(), validation.getMessage());
}
/**
* 认证校验
*
* @param user 用户信息
* @param request 请求信息
* @return boolean
*/
@Override
public OAuth2Validation authValidation(OAuth2User user, OAuth2Request request) {
// 密码模式、刷新token模式、验证码模式需要校验租户状态
if (request.isPassword() || request.isRefreshToken() || request.isCaptchaCode()) {
// 租户校验
OAuth2Validation tenantValidation = validateTenant(user.getTenantId());
if (!tenantValidation.isSuccess()) {
return tenantValidation;
}
// 判断登录是否锁定
OAuth2Validation failCountValidation = validateFailCount(user.getTenantId(), user.getAccount());
if (!failCountValidation.isSuccess()) {
return failCountValidation;
}
}
return super.authValidation(user, request);
}
/**
* 认证成功回调
*
* @param user 用户信息
*/
@Override
public void authSuccessful(OAuth2User user, OAuth2Request request) {
// 清空错误锁定次数
delFailCount(user.getTenantId(), user.getAccount());
log.info("用户:{},认证成功", user.getAccount());
}
/**
* 认证失败回调
*
* @param user 用户信息
* @param validation 失败信息
*/
@Override
public void authFailure(OAuth2User user, OAuth2Request request, OAuth2Validation validation) {
// 自定义认证失败回调
}
/**
* 判断是否为弱密码
*
* @param rawPassword 加密密码
* @return boolean
*/
private boolean isWeakPassword(String rawPassword) {
// 获取公钥
String publicKey = oAuth2Properties.getPublicKey();
// 获取私钥
String privateKey = oAuth2Properties.getPrivateKey();
// 解密密码
String decryptPassword = SM2Util.decrypt(rawPassword, publicKey, privateKey);
return WEAK_PASSWORDS.stream()
.anyMatch(weakPass -> weakPass.equalsIgnoreCase(decryptPassword));
}
/**
* 租户授权校验
*
* @param tenantId 租户id
* @return OAuth2Validation
*/
private OAuth2Validation validateTenant(String tenantId) {
// 租户校验
Tenant tenant = SysCache.getTenant(tenantId);
if (tenant == null) {
return buildValidationFailure(ExceptionCode.USER_TENANT_NOT_FOUND);
}
// 租户授权时间校验
Date expireTime = tenant.getExpireTime();
if (tenantProperties.getLicense()) {
String licenseKey = tenant.getLicenseKey();
String decrypt = DesUtil.decryptFormHex(licenseKey, TenantConstant.DES_KEY);
Tenant license = JsonUtil.parse(decrypt, Tenant.class);
if (license == null || !license.getId().equals(tenant.getId())) {
return buildValidationFailure(ExceptionCode.UNAUTHORIZED_USER_TENANT);
}
expireTime = license.getExpireTime();
}
if (expireTime != null && expireTime.before(DateUtil.now())) {
return buildValidationFailure(ExceptionCode.UNAUTHORIZED_USER_TENANT);
}
return new OAuth2Validation();
}
/**
* 判断登录是否锁定
*
* @param tenantId 租户id
* @param account 账号
* @return OAuth2Validation
*/
private OAuth2Validation validateFailCount(String tenantId, String account) {
int cnt = getFailCount(tenantId, account);
int failCount = Func.toInt(ParamCache.getValue(FAIL_COUNT_VALUE), FAIL_COUNT);
if (cnt >= failCount) {
log.error("用户:{},已锁定,请求ip:{}", account, WebUtil.getIP());
return buildValidationFailure(ExceptionCode.USER_TOO_MANY_FAILS);
}
return new OAuth2Validation();
}
/**
* 获取账号错误次数
*
* @param tenantId 租户id
* @param username 账号
* @return int
*/
private int getFailCount(String tenantId, String username) {
if (Func.hasEmpty(tenantId, username)) {
return 0;
}
return Func.toInt(bladeRedis.get(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, username)), 0);
}
/**
* 设置账号错误次数
*
* @param tenantId 租户id
* @param username 账号
*/
private void addFailCount(String tenantId, String username) {
if (Func.hasEmpty(tenantId, username)) {
return;
}
int count = getFailCount(tenantId, username);
bladeRedis.setEx(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, username), count + 1, Duration.ofMinutes(30));
}
/**
* 设置账号错误次数
*
* @param tenantId 租户id
* @param username 账号
* @param count 次数
*/
private void setFailCount(String tenantId, String username, int count) {
bladeRedis.setEx(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, username), count + 1, Duration.ofMinutes(30));
}
/**
* 清空账号错误次数
*
* @param tenantId 租户id
* @param username 账号
*/
private void delFailCount(String tenantId, String username) {
bladeRedis.del(CacheNames.tenantKey(tenantId, CacheNames.USER_FAIL_KEY, username));
}
}