feat(认证模块): 增加登录时密码加密以及限制密码错误次数

This commit is contained in:
zhouhao 2023-06-26 11:44:52 +08:00
parent 122750aa93
commit d45adde412
4 changed files with 398 additions and 0 deletions

View File

@ -0,0 +1,135 @@
package org.jetlinks.community.utils;
import lombok.SneakyThrows;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
public class CryptoUtils {
private static Key generateDESKey(byte[] password) throws Exception {
DESKeySpec dks = new DESKeySpec(password);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
return keyFactory.generateSecret(dks);
}
@SneakyThrows
public static byte[] encryptDES(byte[] password, byte[] ivParameter, byte[] data) {
Key secretKey = generateDESKey(password);
Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(ivParameter);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
return cipher.doFinal(data);
}
@SneakyThrows
public static byte[] decryptDES(byte[] password, byte[] ivParameter, byte[] data) {
Key secretKey = generateDESKey(password);
Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(ivParameter);
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
return cipher.doFinal(data);
}
@SneakyThrows
public static byte[] encryptAESCBC(byte[] password, byte[] ivParameter, byte[] data) {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
Key secretKey = new SecretKeySpec(fillBit(password, cipher.getBlockSize()), "AES");
IvParameterSpec iv = new IvParameterSpec(fillBit(ivParameter, cipher.getBlockSize()));
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
return cipher.doFinal(data);
}
@SneakyThrows
public static byte[] decryptAESCBC(byte[] password, byte[] ivParameter, byte[] data) {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
Key secretKey = new SecretKeySpec(fillBit(password, cipher.getBlockSize()), "AES");
IvParameterSpec iv = new IvParameterSpec(fillBit(ivParameter, cipher.getBlockSize()));
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
return cipher.doFinal(data);
}
@SneakyThrows
public static byte[] encryptAESECB(byte[] password, byte[] data) {
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
Key secretKey = new SecretKeySpec(fillBit(password, cipher.getBlockSize()), "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(fillBit(data, cipher.getBlockSize()));
}
@SneakyThrows
public static byte[] decryptAESECB(byte[] password, byte[] data) {
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
Key secretKey = new SecretKeySpec(fillBit(password, cipher.getBlockSize()), "AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decrypted = cipher.doFinal(data);
if (decrypted[decrypted.length - 1] != 0x00) {
return decrypted;
}
//去除补位的0x00
for (int i = decrypted.length - 1; i >= 0; i--) {
if (decrypted[i] != 0x00) {
return Arrays.copyOf(decrypted, i + 1);
}
}
return decrypted;
}
private static byte[] fillBit(byte[] data, int blockSize) {
int len = (data.length / blockSize + (data.length % blockSize == 0 ? 0 : 1)) * 16;
if (len == data.length) {
return data;
}
return Arrays.copyOf(data, len);
}
@SneakyThrows
public static KeyPair generateRSAKey() {
KeyPairGenerator keyPairGen;
keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(512);
return keyPairGen.generateKeyPair();
}
@SneakyThrows
public static byte[] decryptRSA(byte[] data, Key key) {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, key);
return cipher.doFinal(data);
}
@SneakyThrows
public static byte[] encryptRSA(byte[] data, Key key) {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(data);
}
@SneakyThrows
public static PublicKey decodeRSAPublicKey(String base64) {
byte[] keyBytes = Base64.decodeBase64(base64);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}
@SneakyThrows
public static PrivateKey decodeRSAPrivateKey(String base64) {
byte[] keyBytes = Base64.decodeBase64(base64);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
}

View File

@ -63,6 +63,12 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-access-logging-aop</artifactId>
<version>${hsweb.framework.version}</version>
</dependency>
<dependency>
<groupId>org.jetlinks.community</groupId>
<artifactId>common-component</artifactId>

View File

@ -0,0 +1,187 @@
package org.jetlinks.community.auth.login;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.web.authorization.annotation.Authorize;
import org.hswebframework.web.authorization.events.AbstractAuthorizationEvent;
import org.hswebframework.web.authorization.events.AuthorizationDecodeEvent;
import org.hswebframework.web.authorization.events.AuthorizationFailedEvent;
import org.hswebframework.web.authorization.exception.AccessDenyException;
import org.hswebframework.web.authorization.exception.AuthenticationException;
import org.hswebframework.web.exception.ValidationException;
import org.hswebframework.web.id.IDGenerator;
import org.hswebframework.web.logging.RequestInfo;
import org.hswebframework.web.utils.DigestUtils;
import org.jetlinks.core.utils.Reactors;
import org.jetlinks.community.utils.CryptoUtils;
import org.jetlinks.reactor.ql.utils.CastUtils;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.security.KeyPair;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@Authorize(ignore = true)
@RequestMapping
@Tag(name = "登录配置接口")
@Hidden
@AllArgsConstructor
@Slf4j
public class UserLoginLogicInterceptor {
private final UserLoginProperties properties;
private final ReactiveRedisOperations<String, String> redis;
@GetMapping("/authorize/login/configs")
@Operation(summary = "获取登录所需配置信息")
public Mono<Map<String, Object>> getConfigs() {
Map<String, Object> map = new ConcurrentHashMap<>();
List<Mono<Void>> jobs = new ArrayList<>();
if (properties.getEncrypt().isEnabled()) {
jobs.add(getEncryptKey()
.doOnNext(enc -> map.put("encrypt", enc))
.then());
} else {
map.put("encrypt", Collections.singletonMap("enabled", false));
}
// TODO: 2023/6/25 其他配置
return Flux.merge(jobs)
.then(Mono.just(map));
}
@GetMapping("/authorize/encrypt-key")
@Operation(summary = "获取登录加密key")
public Mono<Map<String, Object>> getEncryptKey() {
if (!properties.getEncrypt().isEnabled()) {
return Mono.empty();
}
String id = IDGenerator.RANDOM.generate();
KeyPair rasKey = CryptoUtils.generateRSAKey();
String pubKeyBase64 = Base64.getEncoder().encodeToString(rasKey.getPublic().getEncoded());
String priKeyBase64 = Base64.getEncoder().encodeToString(rasKey.getPrivate().getEncoded());
Map<String, Object> value = new LinkedHashMap<>();
value.put("enabled", true);
value.put("publicKey", pubKeyBase64);
value.put("id", id);
return redis
.opsForValue()
.set(createEncRedisKey(id), priKeyBase64, properties.getEncrypt().getKeyTtl())
.thenReturn(value);
}
@EventListener
public void handleAuthEvent(AuthorizationDecodeEvent event) {
if (properties.getBlock().isEnabled()) {
event.async(checkBlocked(event));
}
if (properties.getEncrypt().isEnabled()) {
event.async(Mono.defer(() -> doDecrypt(event)));
}
}
Mono<Void> doDecrypt(AuthorizationDecodeEvent event) {
String encId = event
.getParameter("encryptId")
.map(String::valueOf)
.orElseThrow(() -> new ValidationException("encryptId", "encryptId is required"));
String redisKey = createEncRedisKey(encId);
return redis
.opsForValue()
.get(redisKey)
.map(privateKey -> {
event.setPassword(
new String(
CryptoUtils.decryptRSA(Base64.getDecoder().decode(event.getPassword()),
CryptoUtils.decodeRSAPrivateKey(privateKey))
)
);
return true;
})
.onErrorResume(err -> {
log.warn("decrypt password error", err);
return Reactors.ALWAYS_FALSE;
})
.defaultIfEmpty(false)
.then(redis.opsForValue().delete(redisKey))
.doOnSuccess(success -> {
if (!success) {
throw new AuthenticationException(AuthenticationException.ILLEGAL_PASSWORD);
}
})
.then();
}
@EventListener
public void handleAuthFailed(AuthorizationFailedEvent event) {
if (event.getException() instanceof AuthenticationException) {
event.async(recordAuthFailed(event));
}
}
private Mono<Void> recordAuthFailed(AbstractAuthorizationEvent event) {
return createBlockRedisKey(event)
.flatMap(key -> redis
.opsForValue()
.increment(key, 1)
.then(redis.expire(key, properties.getBlock().getTtl())))
.then();
}
private Mono<RequestInfo> currentRequest() {
return Mono
.deferContextual(ctx -> Mono.justOrEmpty(ctx.getOrEmpty(RequestInfo.class)));
}
private Mono<String> createBlockRedisKey(AbstractAuthorizationEvent event) {
return currentRequest()
.map(request -> {
String hex = DigestUtils.md5Hex(digest -> {
if (properties.getBlock().hasScope(UserLoginProperties.BlockLogic.Scope.ip)) {
digest.update(properties.getBlock().getRealIp(request.getIpAddr()).getBytes());
}
if (properties.getBlock().hasScope(UserLoginProperties.BlockLogic.Scope.username)) {
digest.update(event.getUsername().trim().getBytes());
}
});
return "login:blocked:" + hex;
});
}
private Mono<Void> checkBlocked(AuthorizationDecodeEvent event) {
return this
.createBlockRedisKey(event)
.flatMap(key -> redis
.opsForValue()
.get(key)
.doOnNext(l -> {
if (CastUtils.castNumber(l).intValue() >= properties.getBlock().getMaximum()) {
throw new AccessDenyException("error.user.login.blocked");
}
}))
.then();
}
private static String createEncRedisKey(String encryptId) {
return "login:encrypt-key:" + encryptId;
}
}

View File

@ -0,0 +1,70 @@
package org.jetlinks.community.auth.login;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "user.login")
public class UserLoginProperties {
//加密相关配置
private EncryptionLogic encrypt = new EncryptionLogic();
//登录失败限制相关配置
private BlockLogic block = new BlockLogic();
@Getter
@Setter
public static class EncryptionLogic {
private boolean enabled;
private Duration keyTtl = Duration.ofMinutes(5);
}
@Getter
@Setter
public static class BlockLogic {
//开启登录失败限制
private boolean enabled;
//限制作用域,默认ip+用户名
private Scope[] scopes = Scope.values();
//最大登录失败次数
private int maximum = 5;
//代理深度,默认为1,用于获取经过代理后的客户端真实IP地址
private int proxyDepth = 1;
//限制时间,默认10分钟
private Duration ttl = Duration.ofMinutes(10);
public String getRealIp(String ipAddress) {
String[] split = ipAddress.split(",");
if (split.length > proxyDepth) {
return split[split.length - proxyDepth - 1].trim();
}
return split[split.length - 1].trim();
}
public boolean hasScope(Scope scope) {
for (Scope scope1 : scopes) {
if (scope1 == scope) {
return true;
}
}
return false;
}
public enum Scope {
ip,
username
}
}
}