安全渗透修复

This commit is contained in:
马三炮 2025-11-20 13:43:02 +08:00
parent e851fb6399
commit 6180b8684f
12 changed files with 335 additions and 33 deletions

View File

@ -72,8 +72,8 @@ public class SysOperLog extends BaseEntity
@Excel(name = "返回参数")
private String jsonResult;
/** 操作状态0正常 1异常) */
@Excel(name = "状态", readConverterExp = "0=正常,1=异")
/** 操作状态0异常 1正常) */
@Excel(name = "状态", readConverterExp = "0=异常,1=正")
private Integer status;
/** 错误消息 */
@ -96,5 +96,5 @@ public class SysOperLog extends BaseEntity
private String operatoType;
private String statusRes;
}

View File

@ -0,0 +1,23 @@
package com.bonus.system.api.model;
/**
* @author 马三炮
* @date 2025/11/19
*/
import lombok.Data;
import java.util.Date;
/**
* 会话信息实体存储用户登录状态与终端信息
*/
@Data
public class UserSession {
private String sessionId; // 唯一会话IDUUID生成
private String userId; // 用户唯一标识
private String terminal; // 终端标识如PC/Android/iOS/WeChat
private Date createTime; // 会话创建时间
private Date lastActiveTime; // 最后活跃时间
private boolean isValid; // 会话是否有效
}

View File

@ -0,0 +1,99 @@
package com.bonus.auth.controller;
import com.bonus.system.api.model.UserSession;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
@Component
public class LocalSessionManager {
// 内存缓存用户ID -> 会话信息ConcurrentHashMap保证并发安全
private final Map<String, UserSession> userSessionMap = new ConcurrentHashMap<>();
// 防止同一用户并发登录导致会话状态混乱
private final ReentrantLock lock = new ReentrantLock();
// 会话有效期2小时单位毫秒
private static final long SESSION_EXPIRE_MS = 7200 * 1000;
/**
* 用户登录创建会话强制下线旧会话
* @param userId 用户ID
* @param terminal 终端标识
* @return 新会话ID
*/
public String createSession(String userId, String terminal) {
lock.lock();
try {
// 1. 检查旧会话存在则标记为无效强制下线
UserSession oldSession = userSessionMap.get(userId);
if (oldSession != null && oldSession.isValid()) {
oldSession.setValid(false); // 标记旧会话无效
// 可选记录下线日志如用户XXX在Android终端登录PC终端被强制下线
}
// 2. 生成新会话
String newSessionId = UUID.randomUUID().toString().replace("-", "");
UserSession newSession = new UserSession();
newSession.setSessionId(newSessionId);
newSession.setUserId(userId);
newSession.setTerminal(terminal);
newSession.setCreateTime(new Date());
newSession.setLastActiveTime(new Date());
newSession.setValid(true);
// 3. 缓存新会话
userSessionMap.put(userId, newSession);
return newSessionId;
} finally {
lock.unlock();
}
}
/**
* 校验会话有效性含过期检查
* @param userId 用户ID
* @param sessionId 会话ID
* @return 有效返回true无效返回false
*/
public boolean validateSession(String userId, String sessionId) {
UserSession session = userSessionMap.get(userId);
if (session == null) {
return false; // 无会话记录
}
// 校验会话ID有效性是否过期
boolean isSessionValid = session.getSessionId().equals(sessionId)
&& session.isValid()
&& (System.currentTimeMillis() - session.getLastActiveTime().getTime() < SESSION_EXPIRE_MS);
if (isSessionValid) {
session.setLastActiveTime(new Date()); // 刷新最后活跃时间
return true;
}
return false;
}
/**
* 用户注销销毁会话
*/
public void destroySession(String userId) {
UserSession session = userSessionMap.get(userId);
if (session != null) {
session.setValid(false);
}
}
/**
* 清理过期会话可定时执行如每30分钟
*/
public void cleanExpiredSessions() {
long currentTime = System.currentTimeMillis();
userSessionMap.values().removeIf(session ->
!session.isValid() || (currentTime - session.getLastActiveTime().getTime() >= SESSION_EXPIRE_MS)
);
}
}

View File

@ -32,6 +32,10 @@ public class TokenController
@Autowired
private SysLoginService sysLoginService;
@Autowired
private LocalSessionManager sessionManager;
@PostMapping("login")
public R<?> login(@RequestBody LoginBody form)
{

View File

@ -6,10 +6,7 @@ import com.bonus.common.core.constant.UserConstants;
import com.bonus.common.core.domain.R;
import com.bonus.common.core.enums.UserStatus;
import com.bonus.common.core.exception.ServiceException;
import com.bonus.common.core.utils.AESCBCUtils;
import com.bonus.common.core.utils.JwtUtils;
import com.bonus.common.core.utils.ServletUtils;
import com.bonus.common.core.utils.StringUtils;
import com.bonus.common.core.utils.*;
import com.bonus.common.core.utils.ip.IpUtils;
import com.bonus.common.security.utils.SecurityUtils;
import com.bonus.system.api.RemoteLogService;
@ -41,8 +38,10 @@ public class SysLoginService
public LoginUser login(String username, String password,
String type, String jwtToken)
{
username = AESCBCUtils.decrypt(username);
password = AESCBCUtils.decrypt(password);
/* username = AESCBCUtils.decrypt(username);
password = AESCBCUtils.decrypt(password);*/
username = RSAUtil.decrypt(username);
password = RSAUtil.decrypt(password);
if(StringUtils.isNotEmpty(jwtToken)){
Claims claims = JwtUtils.parseToken(jwtToken);
jwtToken = (String) claims.get(SecurityConstants.DETAILS_USERNAME);

View File

@ -8,14 +8,14 @@ package com.bonus.common.core.constant;
public class CacheConstants
{
/**
* 缓存有效期默认720分钟
* 缓存有效期默认30分钟
*/
public final static long EXPIRATION = 720;
public final static long EXPIRATION = 30;
/**
* 缓存刷新时间默认120分钟
* 缓存刷新时间默认30分钟
*/
public final static long REFRESH_TIME = 120;
public final static long REFRESH_TIME = 30;
/**
* 权限缓存前缀

View File

@ -0,0 +1,89 @@
package com.bonus.common.core.utils;
/**
* @author 马三炮
* @date 2025/11/19
*/
import org.springframework.util.Base64Utils;
import javax.crypto.Cipher;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RSAUtil {
// 私钥从安全配置中获取Base64编码
private static final String PRIVATE_KEY = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC43RvfCkl42fK8YQUJCTLe7ScaH+OTBexWTwiU1GWyILYhNwGuhusmJ9Mgj7+wvxvB8p9ZT6HYCBfwdRlpkAmXbjzyGzZJx8l/npwpmwGOZN8LNRUSGDmOtOXET8sHeQG1j46BV9Tghn+2VL8ApT4dj+FitKro7/cNqHzbl4nqihEg/R3TKyx2Xo0yn/yDUmeFY9Qb4FWIFawjfwZ+09toQWpQ6L1mmkNYiN4doLMMohps67Uqvq/GIuaZNgV0HaZA0QyL2c6S8NKGbPfqUyS5kDvQ0EyUSM7oaf2GytaRsjJyGpOp96Lx2XXlQwN0p4lne7HjSvjvN+M8J/mLHZ51AgMBAAECggEAbh0eZeudugOwh68OrcQx7gG7Ad+u0bt8fCiVhvlBHipH8l7ufIPW+6JLKyEU4HXlVdBsRGmfkKu5frxxh6SJYg8C9H9rBeWgS4kxocK32ZaSGP3kcCPH3Q45soaI/AmDEB/TlY24uojC99tg7kR/TGKQsRfGmfud4B/VR4PrXa0egek75qtzZWA2E7bJjwbTQ7BGacz/JsbcbXWfWKYLIHsl0IGzyrqPRaX2iE6qBWDnWZO7Fot9f72fPzo1tiYwDG5tEF61gchNQvQswqVgKRW7u5/o1C11p+WUt5ImDiW/TScAYvQGZA1eHD9Ex3f+0Lb/eJPEvmOXHys+46dDfQKBgQDjGQ/QU5z5uJSmcroO7NZMoYF8pp737Clc90B74a7gDdvYC/TqEqaRji13m1v7VNwh6Q578KXfFYKoQviSXL93aoc09lPnfPZAZ9CORGAQBjYfXgx6KOiQ+GcQy7r/MF7j67OAPJ4jj/FzdSRw3+zeUZzDdf3VoaU8BjBdfSRl+wKBgQDQZApoZ+fKotn6czEG8sjRNYGthEAiElGo9GWlupmTbUt2Zo7l0ywqFjyaenGO68cew5mD/4L3orxOBIunF8nL5pufSTeQI2QS3LpHdpMTIwUxvvaaa86PSSUD31qkmb6BZtZrKlNKpLnjCpZ7tyIfok1xjErT0go04DAPN9uSTwKBgAFwJDB+hwzxxUc0jMcRat37W9WNPI53WV+0VR0ztabHj6/Ti4575cAgNVt/iVqTE/3G+wd/450BBt3H5skOe0vnScnlEWzy1qBbx+9OsYkIcQlL0Cpp3eclG4n4sEtzY/ZvhKh4Ocwh2jaP9FYepOJXg/Wc5qmWp3Q02dONulBHAoGBAJbY0+/fxxoX6FIzMDonZNaMNOK16PbA6/uxbiMZgDIH26hJdQdIZZ/RmOz51VnvsqPCU04Jl3RKiKSxBaZeweSned9D0MS12asNvIoeezhPEWOBKGF/yHZQLEsxUxvzbsOc/Xl1mfVeaHDBKU1Es57Sjdy0ATZzxI+h+SgK9xZhAoGAECzncIpn4I0IyJ16ArMNGqC/VlF73auGB2gPtI7G+v1aROtm6n8xKoJQmDMAM0tyRLUP//TSK8NgNw4ACUy3QhwI6R+cnVziv6v9ozVvyaPrBChDshtzCblRSFhKqq+f0tEYVns5VwlM5jp8uFhIKxxjo4ufCdd5v9Ot868ArIY="; // 后端存储的私钥Base64字符串
private static final String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuN0b3wpJeNnyvGEFCQky3u0nGh/jkwXsVk8IlNRlsiC2ITcBrobrJifTII+/sL8bwfKfWU+h2AgX8HUZaZAJl2488hs2ScfJf56cKZsBjmTfCzUVEhg5jrTlxE/LB3kBtY+OgVfU4IZ/tlS/AKU+HY/hYrSq6O/3Dah825eJ6ooRIP0d0yssdl6NMp/8g1JnhWPUG+BViBWsI38GftPbaEFqUOi9ZppDWIjeHaCzDKIabOu1Kr6vxiLmmTYFdB2mQNEMi9nOkvDShmz36lMkuZA70NBMlEjO6Gn9hsrWkbIychqTqfei8dl15UMDdKeJZ3ux40r47zfjPCf5ix2edQIDAQAB";
/**
* 解密RSA加密后的字符串
*/
public static String decrypt(String encryptedData) {
try {
// 1. 解码私钥
byte[] privateKeyBytes = Base64Utils.decodeFromString(PRIVATE_KEY);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec);
// 2. 初始化解密器
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); // 填充方式需与前端一致
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// 3. 分段解密前端分段加密后端需对应分段解密
byte[] encryptedBytes = Base64Utils.decodeFromString(encryptedData);
int maxDecryptLength = 256; // 2048位RSA的最大解密长度
StringBuilder decryptedResult = new StringBuilder();
for (int i = 0; i < encryptedBytes.length; i += maxDecryptLength) {
int length = Math.min(i + maxDecryptLength, encryptedBytes.length);
byte[] chunk = cipher.doFinal(encryptedBytes, i, length);
decryptedResult.append(new String(chunk, "UTF-8"));
}
return decryptedResult.toString();
}catch (Exception e){
System.out.println("公钥+++++++"+e.getMessage());
}
return "";
}
public static String encryptWithPublicKey(String data) {
try {
// 解码公钥
byte[] publicKeyBytes = Base64Utils.decodeFromString(PUBLIC_KEY);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(keySpec);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] dataBytes = data.getBytes("UTF-8");
int maxLength = 245;
StringBuilder encryptedResult = new StringBuilder();
for (int i = 0; i < dataBytes.length; i += maxLength) {
int end = Math.min(i + maxLength, dataBytes.length);
byte[] chunk = cipher.doFinal(dataBytes, i, end - i);
if (encryptedResult.length() > 0) {
encryptedResult.append(",");
}
encryptedResult.append(Base64Utils.encodeToString(chunk));
}
return encryptedResult.toString();
} catch (Exception e) {
System.out.println("加密异常: " + e.getMessage());
}
return "";
}
public static void main(String[] args) throws NoSuchAlgorithmException {
// Java生成RSA密钥对2048位
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(2048); // 密钥长度2048位
KeyPair keyPair = keyPairGen.generateKeyPair();
String publicKey = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()); // 公钥Base64编码前端使用
String privateKey = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()); // 私钥Base64编码后端存储
System.out.println("公钥+++++++"+publicKey);
System.out.println("私钥+++++++"+privateKey);
}
}

View File

@ -4,6 +4,8 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import com.bonus.common.core.utils.RSAUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.bonus.common.core.constant.CacheConstants;
@ -46,6 +48,8 @@ public class TokenService
String token = IdUtils.fastUUID();
Long userId = loginUser.getSysUser().getUserId();
String userName = loginUser.getSysUser().getUserName();
loginUser.getSysUser().setIdNumber(RSAUtil.encryptWithPublicKey(loginUser.getSysUser().getIdNumber()));
loginUser.getSysUser().setPhonenumber(RSAUtil.encryptWithPublicKey(loginUser.getSysUser().getPhonenumber()));
loginUser.setToken(token);
loginUser.setUserid(userId);
loginUser.setUsername(userName);
@ -135,7 +139,7 @@ public class TokenService
}
/**
* 验证令牌有效期相差不足120分钟自动刷新缓存
* 验证令牌有效期相差不足30分钟自动刷新缓存
*
* @param loginUser
*/

File diff suppressed because one or more lines are too long

View File

@ -137,7 +137,7 @@ function logout() {
//清除系统首页缓存
localStorage.removeItem('subComIds');
localStorage.removeItem('subComNamePlan');
localStorage.clear();
$.ajax({
type: 'delete',
url: DATA_URL + '/auth/logout',

View File

@ -40,7 +40,7 @@
</body>
<script src="js/libs/jquery-3.6.0.js"></script>
<script src="js/publicJs.js"></script>
<script src="js/AesCbc.js"></script>
<script src="js/Rsa.js"></script>
<script type="text/javascript" src="./layui/layui.js"></script>
<script type="text/javascript">
layui.use([ 'layer' ], function() {
@ -109,14 +109,18 @@
contentType : "application/json; charset=utf-8",
url : DATA_URL + '/auth/login',
data : JSON.stringify({
"username" : encryptCBC(username),
"password" : encryptCBC(password),
"username" : encryptRsa(username),
"password" : encryptRsa(password),
"jwtToken" : jwtToken
}),
success : function(data) {
if(data.code == '200'){
console.log("[[[[[:",data)
let roleLevel = data.data.loginUser.sysUser.roleLevel;
console.log(data)
const idNumber = decryptRsa(data.data.loginUser.sysUser.idNumber);
// 4. 解密
const phonenumber = decryptRsa(data.data.loginUser.sysUser.phonenumber);
// if(roleLevel != '5'){
localStorage.setItem("smz-token", data.data.access_token);
//保存用户名密码
@ -125,9 +129,9 @@
//保存登录人id
localStorage.setItem("userId", data.data.loginUser.userid);
//保存登录人身份证
localStorage.setItem("userIdNumber", data.data.loginUser.sysUser.idNumber);
localStorage.setItem("userIdNumber", idNumber);
//保存登录人手机号
localStorage.setItem("phonenumber", data.data.loginUser.sysUser.phonenumber);
localStorage.setItem("phonenumber", phonenumber);
//角色等级
localStorage.setItem("roleLevel", roleLevel);
//是否记录密码

View File

@ -111,6 +111,8 @@
"oper_id" AS operId,
"title",
"json_result" as jsonResult,
"oper_ip" as operIp,
"status" as status,
CASE
"business_type"
WHEN 1 THEN '查询'
@ -131,6 +133,12 @@
WHEN 2 THEN 'APP'
ELSE '其他'
END AS operatoType ,
CASE
"status"
WHEN 0 THEN '请求异常'
WHEN 1 THEN '请求成功'
ELSE '其他'
END AS statusRes ,
"create_time" AS opersTime
FROM
"ynrealname"."sys_oper_log"