解决方案:
对相应的参数增加SQL关键字的判断(select update delete),如果参数包含SQL关键字则判定参数失效。
对登录请求中的敏感信息以RSA加密的方式发送到服务器。
<dependency>
<groupId>org.bouncycastlegroupId>
<artifactId>bcprov-jdk16artifactId>
<version>1.46version>
dependency>
2.1 新增RSAUtils
public class RSAUtils {
//KeyPair is a simple holder for a key pair.
private static final KeyPair keyPair = initKey();
/**
* 初始化方法,产生key pair,提供provider和random
* @return KeyPair instance
*/
private static KeyPair initKey() {
try {
//添加provider
Provider provider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
Security.addProvider(provider);
//产生用于安全加密的随机数
SecureRandom random = new SecureRandom();
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", provider);
generator.initialize(1024, random);
return generator.generateKeyPair();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
/**
* 产生public key
* @return public key字符串
*/
public static String generateBase64PublicKey() {
PublicKey publicKey = (RSAPublicKey)keyPair.getPublic();
//encodeBase64(): Encodes binary data using the base64
//algorithm but does not chunk the output.
//getEncoded():返回key的原始编码形式
return new String(Base64.encodeBase64(publicKey.getEncoded()));
}
/**
* 解密数据
* @param string 需要解密的字符串
* @return 破解之后的字符串
*/
public static String decryptBase64(String string) {
//decodeBase64():将Base64数据解码为"八位字节”数据
return new String(decrypt(Base64.decodeBase64(string.getBytes())));
}
private static byte[] decrypt(byte[] byteArray) {
try {
Provider provider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
Security.addProvider(provider);
//Cipher: 提供加密和解密功能的实例
//transformation: "algorithm/mode/padding"
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", provider);
PrivateKey privateKey = keyPair.getPrivate();
//初始化
cipher.init(Cipher.DECRYPT_MODE, privateKey);
//doFinal(): 加密或者解密数据
byte[] plainText = cipher.doFinal(byteArray);
return plainText;
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
2.2 新增RASController
@Controller
@RequestMapping("/rsa")
public class RasController {
@RequestMapping("/public")
@ResponseBody
public Object getRasPublicKey() {
String publicKey = RSAUtils.generateBase64PublicKey();
return new Result<>(publicKey);
}
}
3.1 修改配置类
在继承PathMatchingFilterChainResolver的配置类中进行如下配置:
public class CustomPathMatchingFilterChainResolver extends PathMatchingFilterChainResolver {
/** 注册请求 路径映射 pattern. */
private static final String RSA_FILTER = "/rsa/public";
private CustomFilterChainManager customDefaultFilterChainManager;
/** 设置customDefaultFilterChainManager. */
public void setCustomDefaultFilterChainManager(
CustomFilterChainManager customDefaultFilterChainManager) {
this.customDefaultFilterChainManager = customDefaultFilterChainManager;
setFilterChainManager(customDefaultFilterChainManager);
}
@Override
public FilterChain getChain(ServletRequest request, ServletResponse response,
FilterChain originalChain) {
FilterChainManager filterChainManager = getFilterChainManager();
if (!filterChainManager.hasChains()) {
return null;
}
String requestURI = getPathWithinApplication(request);
List<String> chainNames = new ArrayList<String>();
for (String pathPattern : filterChainManager.getChainNames()) {
if (pathMatches(pathPattern, requestURI)) {
if (RSA_FILTER.equals(pathPattern)) {
return customDefaultFilterChainManager.proxy(originalChain, pathPattern);
}
chainNames.add(pathPattern);
}
}
if (CollectionUtils.isEmpty(chainNames)) {
return null;
}
return customDefaultFilterChainManager.proxy(originalChain, chainNames);
}
}
3.2 修改security-context.xml配置文件
在id为filterChainManager的bean中,修改name为defaultFilterChainDefinitions的property的value子标签,增加:
/rsa/public = anon
在继承AuthorizingRealm的类中对token中的参数进行解密,
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = RSAUtils.decryptBase64(upToken.getUsername().trim());
String password = "";
if (upToken.getPassword() != null) {
password = RSAUtils.decryptBase64(new String(upToken.getPassword()));
}
...
并且在验证登录成功之后修改token中的相应用户参数为非加密状态:
...
upToken.setUsername(username);
upToken.setPassword(password.toCharArray());
return authenticationInfo;
}
下载地址:
https://share.weiyun.com/5SdhNGK
//获取public key
var publicKey = null;
jQuery(document).ready(function() {
...
$("#form-submit").bind("click",function(){
getPublicKey();
var oldUsername = $("#form-username").val();
var oldPassword = $("#form-password").val();
convertFormValue();
event.preventDefault();
$("form[role='form']").submit();
resetForm(oldUsername, oldPassword);
});
});
function getPublicKey(){
var url = window.location.href;
url = url.substring(0,url.lastIndexOf("/")) + "/rsa/public";
$.ajax({
url: url,
type: "post",
async: false,
dataType: "json",
success: function(data) {
if(data) publicKey = data.data;
}
});
}
function convertFormValue() {
if(publicKey != null){
var encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);
var usrename = encrypt.encrypt($("#form-username").val().trim());
var password = encrypt.encrypt($("#form-password").val().trim());
$("#form-username").val(usrename);
$("#form-password").val(password);
}
}
function resetForm(username, password) {
$("#form-username").val(username);
$("#form-password").val(password);
}
解决方法:
在继承AuthorizingRealm的Realm类中增加如下配置:
public class CustomRealm extends AuthorizingRealm {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomRealm.class);
...
/**
* 集群中可能会导致出现验证多过5次的现象,因为AtomicInteger只能保证单节点并发
*/
private static final String RETRY_CACHE_NAME = "passwordRetryCache";
private static final Integer MAX_RETRY_COUNT = 5;
private Cache<String, AtomicInteger> passwordRetryCache;
/**
* 线程池
*/
private static ExecutorService executorService = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(10));
...
/**
* 验证当前登录的Subject.
* @param token 验证登录凭证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = RSAUtils.decryptBase64(upToken.getUsername().trim());
String password = "";
if (upToken.getPassword() != null) {
password = RSAUtils.decryptBase64(new String(upToken.getPassword()));
}
try {
doLoginTimeLimits(username);
...
} catch (Exception e) {
...
}
...
}
...
private void doLoginTimeLimits(String username) {
final String clientUserName = username;
passwordRetryCache = cacheManager.getCache(RETRY_CACHE_NAME);
//先查看是否系统中是否已有登录次数缓存
AtomicInteger retryCount = passwordRetryCache.get(clientUserName);
// 如果之前没有登录缓存,则创建一个登录次数缓存。
if (retryCount == null) {
retryCount = new AtomicInteger(0);
}
//将缓存记录的登录次数加1
retryCount.incrementAndGet();
//如果有且次数已经超过限制,则驳回本次登录请求。
if (retryCount.get() > MAX_RETRY_COUNT) {
LOGGER.error("登录次数超过限制");
throw new ExcessiveAttemptsException("用户:" + clientUserName + "登录次数已经超过限制");
}
//并将其保存到缓存中
passwordRetryCache.put(clientUserName, retryCount);
//debug
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("用户:{},尝试登录次数:{}", clientUserName, retryCount.get());
}
//判断是否登录成功
boolean isMatcher = false;
//如果成功则清除缓存
if (isMatcher) {
passwordRetryCache.remove(clientUserName);
}
synchronized (this) {
executorService.execute(() -> {
for (int i = 0; i < 60; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
passwordRetryCache.remove(clientUserName);
});
}
}
...
}
解决方法:
public void login(String username,String password){
SecurityUtils.getSubject().logout();
//登录验证
...
}
解决方法:
对每个错误的登录尝试发出相同的错误提示信息(“用户名或密码不正确!”)。
解决方法:
在密码input标签中增加如下属性:
autocomplete="false"
解决方法:
将提示出来的相关信息,在html页面中删除掉。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EFLBB935-1579432572730)(?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzMjA0NzA5,size_16,color_FFFFFF,t_70)]