安全渗透修复

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

@ -10,7 +10,7 @@ import java.util.Date;
/** /**
* 操作日志记录表 oper_log * 操作日志记录表 oper_log
* *
* @author zys * @author zys
*/ */
@Data @Data
@ -72,8 +72,8 @@ public class SysOperLog extends BaseEntity
@Excel(name = "返回参数") @Excel(name = "返回参数")
private String jsonResult; private String jsonResult;
/** 操作状态0正常 1异常) */ /** 操作状态0异常 1正常) */
@Excel(name = "状态", readConverterExp = "0=正常,1=异") @Excel(name = "状态", readConverterExp = "0=异常,1=正")
private Integer status; private Integer status;
/** 错误消息 */ /** 错误消息 */
@ -96,5 +96,5 @@ public class SysOperLog extends BaseEntity
private String operatoType; 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

@ -19,7 +19,7 @@ import javax.servlet.http.HttpServletRequest;
/** /**
* token 控制 * token 控制
* *
* @author zys * @author zys
*/ */
//@CrossOrigin //@CrossOrigin
@ -32,6 +32,10 @@ public class TokenController
@Autowired @Autowired
private SysLoginService sysLoginService; private SysLoginService sysLoginService;
@Autowired
private LocalSessionManager sessionManager;
@PostMapping("login") @PostMapping("login")
public R<?> login(@RequestBody LoginBody form) 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.domain.R;
import com.bonus.common.core.enums.UserStatus; import com.bonus.common.core.enums.UserStatus;
import com.bonus.common.core.exception.ServiceException; import com.bonus.common.core.exception.ServiceException;
import com.bonus.common.core.utils.AESCBCUtils; import com.bonus.common.core.utils.*;
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.ip.IpUtils; import com.bonus.common.core.utils.ip.IpUtils;
import com.bonus.common.security.utils.SecurityUtils; import com.bonus.common.security.utils.SecurityUtils;
import com.bonus.system.api.RemoteLogService; import com.bonus.system.api.RemoteLogService;
@ -23,7 +20,7 @@ import org.springframework.stereotype.Component;
/** /**
* 登录校验方法 * 登录校验方法
* *
* @author zys * @author zys
*/ */
@Component @Component
@ -41,8 +38,10 @@ public class SysLoginService
public LoginUser login(String username, String password, public LoginUser login(String username, String password,
String type, String jwtToken) String type, String jwtToken)
{ {
username = AESCBCUtils.decrypt(username); /* username = AESCBCUtils.decrypt(username);
password = AESCBCUtils.decrypt(password); password = AESCBCUtils.decrypt(password);*/
username = RSAUtil.decrypt(username);
password = RSAUtil.decrypt(password);
if(StringUtils.isNotEmpty(jwtToken)){ if(StringUtils.isNotEmpty(jwtToken)){
Claims claims = JwtUtils.parseToken(jwtToken); Claims claims = JwtUtils.parseToken(jwtToken);
jwtToken = (String) claims.get(SecurityConstants.DETAILS_USERNAME); jwtToken = (String) claims.get(SecurityConstants.DETAILS_USERNAME);
@ -149,7 +148,7 @@ public class SysLoginService
/** /**
* 记录登录信息 * 记录登录信息
* *
* @param username 用户名 * @param username 用户名
* @param status 状态 * @param status 状态
* @param message 消息内容 * @param message 消息内容
@ -172,4 +171,4 @@ public class SysLoginService
} }
remoteLogService.saveLogininfor(logininfor, SecurityConstants.INNER); remoteLogService.saveLogininfor(logininfor, SecurityConstants.INNER);
} }
} }

View File

@ -2,20 +2,20 @@ package com.bonus.common.core.constant;
/** /**
* 缓存的key 常量 * 缓存的key 常量
* *
* @author zys * @author zys
*/ */
public class CacheConstants 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.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import com.bonus.common.core.utils.RSAUtil;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.bonus.common.core.constant.CacheConstants; import com.bonus.common.core.constant.CacheConstants;
@ -19,7 +21,7 @@ import com.bonus.system.api.model.LoginUser;
/** /**
* token验证处理 * token验证处理
* *
* @author zys * @author zys
*/ */
@Component @Component
@ -46,6 +48,8 @@ public class TokenService
String token = IdUtils.fastUUID(); String token = IdUtils.fastUUID();
Long userId = loginUser.getSysUser().getUserId(); Long userId = loginUser.getSysUser().getUserId();
String userName = loginUser.getSysUser().getUserName(); 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.setToken(token);
loginUser.setUserid(userId); loginUser.setUserid(userId);
loginUser.setUsername(userName); loginUser.setUsername(userName);
@ -135,7 +139,7 @@ public class TokenService
} }
/** /**
* 验证令牌有效期相差不足120分钟自动刷新缓存 * 验证令牌有效期相差不足30分钟自动刷新缓存
* *
* @param loginUser * @param loginUser
*/ */
@ -167,4 +171,4 @@ public class TokenService
{ {
return ACCESS_TOKEN + token; return ACCESS_TOKEN + token;
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -137,7 +137,7 @@ function logout() {
//清除系统首页缓存 //清除系统首页缓存
localStorage.removeItem('subComIds'); localStorage.removeItem('subComIds');
localStorage.removeItem('subComNamePlan'); localStorage.removeItem('subComNamePlan');
localStorage.clear();
$.ajax({ $.ajax({
type: 'delete', type: 'delete',
url: DATA_URL + '/auth/logout', url: DATA_URL + '/auth/logout',
@ -292,4 +292,4 @@ function onclickIndex(lay_id){
layui.use(['layer'], function () { layui.use(['layer'], function () {
layer = layui.layer; layer = layui.layer;
}); });

View File

@ -40,7 +40,7 @@
</body> </body>
<script src="js/libs/jquery-3.6.0.js"></script> <script src="js/libs/jquery-3.6.0.js"></script>
<script src="js/publicJs.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" src="./layui/layui.js"></script>
<script type="text/javascript"> <script type="text/javascript">
layui.use([ 'layer' ], function() { layui.use([ 'layer' ], function() {
@ -67,7 +67,7 @@
if (top != self) { if (top != self) {
parent.location.href = '/login.html'; parent.location.href = '/login.html';
} }
var token = localStorage.getItem("smz-token"); var token = localStorage.getItem("smz-token");
if (token != null && token.trim().length != 0) { if (token != null && token.trim().length != 0) {
$.ajax({ $.ajax({
@ -109,14 +109,18 @@
contentType : "application/json; charset=utf-8", contentType : "application/json; charset=utf-8",
url : DATA_URL + '/auth/login', url : DATA_URL + '/auth/login',
data : JSON.stringify({ data : JSON.stringify({
"username" : encryptCBC(username), "username" : encryptRsa(username),
"password" : encryptCBC(password), "password" : encryptRsa(password),
"jwtToken" : jwtToken "jwtToken" : jwtToken
}), }),
success : function(data) { success : function(data) {
if(data.code == '200'){ if(data.code == '200'){
console.log("[[[[[:",data)
let roleLevel = data.data.loginUser.sysUser.roleLevel; 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'){ // if(roleLevel != '5'){
localStorage.setItem("smz-token", data.data.access_token); localStorage.setItem("smz-token", data.data.access_token);
//保存用户名密码 //保存用户名密码
@ -125,9 +129,9 @@
//保存登录人id //保存登录人id
localStorage.setItem("userId", data.data.loginUser.userid); 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); localStorage.setItem("roleLevel", roleLevel);
//是否记录密码 //是否记录密码
@ -260,4 +264,4 @@
}) })
} }
</script> </script>
</html> </html>

View File

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