1.防御xss攻击
1.1 模拟xss攻击
@RequestMapping("/getUserInfo")
public String getUserInfo(HttpServletRequest request) {
request.setAttribute("userName", request.getParameter("userName"));
return "userinfo";
}
模拟请求:
http://localhost:8080/getUserInfo?userName=
<script type="text/javascript">
window.location.href="http://www.tostyle.fun";
</script>
请求的结果为:
跳转到 http://www.tostyle.fun界面
1.2 防御
package com.tostyle.xss.filter;
import com.tostyle.xss.wrapper.XssHttpServletRequestWrapper;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* xss脚本防御过滤器
*
* @author tostyle
* 2022/4/11 11:17
*/
@WebFilter
@Component
public class XssFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//放行
filterChain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) servletRequest),servletResponse);
}
@Override
public void destroy() {
}
}
package com.tostyle.xss.wrapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
/**
* @author tostyle
* 2022/4/11 11:19
*/
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String oldValue = super.getParameter(name);
if(StringUtils.isEmpty(oldValue)){
return oldValue;
}
//将特殊的符号进行转义即可
return StringEscapeUtils.escapeHtml4(oldValue);
}
}
2. 防止篡改数据
解决办法就是对数据做加密签名操作,常见的加密操作有
2.1 对称加密
- 加密和解密都是同一个秘钥
优势:算法公开、加密快、效率高。
缺点:可以采用反编译破解客户端真实秘钥
加密与解密都是采用同一个秘钥,也就是客户端与服务器数据传输采用同一个秘钥实现
加解密,所以如果黑客反编译了客户端源代码就可以破解该密文。
2.1.1 DESUtil工具类
package com.tostyle.xss.utils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.security.SecureRandom;
/**
* 字符串加密解密工具,可逆加密,秘钥很重要,一定要自己改秘钥,打死也不要告诉其他人
*/
public class DESUtil {
// 密钥,是加密解密的凭据,长度为8的倍数
private static String PASSWORD_CRYPT_KEY = "12345678";
private final static String DES = "DES";
/**
* 加密
*
* @param src 数据源
* @param key 密钥,长度必须是8的倍数
* @return 返回加密后的数据
* @throws Exception
*/
public static byte[] encrypt(byte[] src, byte[] key) throws Exception {
// DES算法要求有一个可信任的随机数源
SecureRandom sr = new SecureRandom();
// 从原始密匙数据创建DESKeySpec对象
DESKeySpec dks = new DESKeySpec(key);
// 创建一个密匙工厂,然后用它把DESKeySpec转换成
// 一个SecretKey对象
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
SecretKey securekey = keyFactory.generateSecret(dks);
// Cipher对象实际完成加密操作
Cipher cipher = Cipher.getInstance(DES);
// 用密匙初始化Cipher对象
cipher.init(Cipher.ENCRYPT_MODE, securekey, sr);
// 现在,获取数据并加密
// 正式执行加密操作
return cipher.doFinal(src);
}
/**
* 解密
*
* @param src 数据源
* @param key 密钥,长度必须是8的倍数
* @return 返回解密后的原始数据
* @throws Exception
*/
public static byte[] decrypt(byte[] src, byte[] key) throws Exception {
// DES算法要求有一个可信任的随机数源
SecureRandom sr = new SecureRandom();
// 从原始密匙数据创建一个DESKeySpec对象
DESKeySpec dks = new DESKeySpec(key);
// 创建一个密匙工厂,然后用它把DESKeySpec对象转换成
// 一个SecretKey对象
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
SecretKey securekey = keyFactory.generateSecret(dks);
// Cipher对象实际完成解密操作
Cipher cipher = Cipher.getInstance(DES);
// 用密匙初始化Cipher对象
cipher.init(Cipher.DECRYPT_MODE, securekey, sr);
// 现在,获取数据并解密
// 正式执行解密操作
return cipher.doFinal(src);
}
/**
* 密码解密
*
* @param data
* @return
* @throws Exception
*/
public final static String decrypt(String data) {
try {
return new String(decrypt(hex2byte(data.getBytes()), PASSWORD_CRYPT_KEY.getBytes()));
} catch (Exception e) {
}
return null;
}
/**
* 密码加密
*
* @param password
* @return
* @throws Exception
*/
public final static String encrypt(String password) {
try {
return byte2hex(encrypt(password.getBytes(), PASSWORD_CRYPT_KEY.getBytes()));
} catch (Exception e) {
}
return null;
}
/**
* 二行制转字符串
*
* @param b
* @return
*/
public static String byte2hex(byte[] b) {
String hs = "";
String stmp = "";
for (int n = 0; n < b.length; n++) {
stmp = (java.lang.Integer.toHexString(b[n] & 0XFF));
if (stmp.length() == 1)
hs = hs + "0" + stmp;
else
hs = hs + stmp;
}
return hs.toUpperCase();
}
public static byte[] hex2byte(byte[] b) {
if ((b.length % 2) != 0)
throw new IllegalArgumentException("长度不是偶数");
byte[] b2 = new byte[b.length / 2];
for (int n = 0; n < b.length; n += 2) {
String item = new String(b, n, 2);
b2[n / 2] = (byte) Integer.parseInt(item, 16);
}
return b2;
}
// 测试用例,不需要传递任何参数,直接执行即可。
public static void main(String[] args) {
String basestr = "tostyle";
String str1 = encrypt(basestr);
System.out.println("原始值: " + basestr);
System.out.println("加密后: " + str1);
System.out.println("解密后: " + decrypt(str1));
}
}
2.1.3 Md5Utils
package com.tostyle.xss.utils;
import com.alibaba.fastjson.JSON;
import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
/**
* 参数验证签名
*/
public class Md5Utils{
private static Logger logger = LoggerFactory.getLogger(Md5Utils.class);
/** 加密密钥 */
private final static String APP_KEY = "mykey123456";
public final static String SECRET_KEY = "mysecret123456";
/** 字符编码 */
private final static String INPUT_CHARSET = "UTF-8";
/** 超时时间 */
private final static int TIME_OUT = 30 * 60 * 1000;
/**
* 请求参数Map转换验证Map
*
* @param requestParams
* 请求参数Map
* @param charset
* 是否要转utf8编码
* @return
* @throws UnsupportedEncodingException
*/
public static Map<String, String> toVerifyMap(Map<String, String[]> requestParams, boolean charset) {
Map<String, String> params = new HashMap<>();
for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
}
// 乱码解决,这段代码在出现乱码时使用。如果mysign和sign不相等也可以使用这段代码转化
if (charset)
valueStr = getContentString(valueStr, INPUT_CHARSET);
params.put(name, valueStr);
}
return params;
}
/**
* 除去数组中的空值和签名参数
*
* @param sArray
* 签名参数组
* @return 去掉空值与签名参数后的新签名参数组
*/
public static Map<String, String> paraFilter(Map<String, String> sArray) {
Map<String, String> result = new HashMap<>();
if (sArray == null || sArray.size() <= 0) {
return result;
}
for (String key : sArray.keySet()) {
String value = sArray.get(key);
if (value == null || value.equals("") || key.equalsIgnoreCase("sign")) {
continue;
}
result.put(key, value);
}
return result;
}
/**
* 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
*
* @param params
* 需要排序并参与字符拼接的参数组
* @return 拼接后字符串
*/
public static String createLinkString(Map<String, String> params) {
return createLinkString(params, false);
}
/**
* 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串
*
* @param params
* 需要排序并参与字符拼接的参数组
* @param encode
* 是否需要UrlEncode
* @return 拼接后字符串
*/
public static String createLinkString(Map<String, String> params, boolean encode) {
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
String prestr = "";
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
if (encode)
value = urlEncode(value, INPUT_CHARSET);
if (i == keys.size() - 1) {// 拼接时,不包括最后一个&字符
prestr = prestr + key + "=" + value;
} else {
prestr = prestr + key + "=" + value + "&";
}
}
return prestr;
}
/**
* 编码转换
*
* @param content
* @param charset
* @return
* @throws UnsupportedEncodingException
*/
private static byte[] getContentBytes(String content, String charset) {
if (charset == null || "".equals(charset)) {
return content.getBytes();
}
try {
return content.getBytes(charset);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:" + charset);
}
}
/**
* 编码转换
*
* @param content
* @param charset
* @return
*/
private static String getContentString(String content, String charset) {
if (charset == null || "".equals(charset)) {
return new String(content.getBytes());
}
try {
return new String(content.getBytes("ISO-8859-1"), charset);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("指定的编码集不对,您目前指定的编码集是:" + charset);
}
}
/**
* URL转码
*
* @param content
* @param charset
* @return
*/
private static String urlEncode(String content, String charset) {
try {
return URLEncoder.encode(content, charset);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("指定的编码集不对,您目前指定的编码集是:" + charset);
}
}
// TODO 签名
/**
* 生成要请求的签名参数数组
*
* @param sParaTemp
* 需要签名的参数Map
* @return 要请求的签名参数数组
*/
public static Map<String, String> signMap(Map<String, String[]> sParaTemp) {
// 请求参数Map转换验证Map,并生成要请求的签名参数数组
return sign(toVerifyMap(sParaTemp, false));
}
/**
* 生成要请求的签名参数数组
*
* @param sParaTemp
* 需要签名的参数
* @return 要请求的签名参数数组
*/
public static Map<String, String> sign(Map<String, String> sParaTemp) {
// 时间戳加入签名参数组中
sParaTemp.put("timestamp", String.valueOf(System.currentTimeMillis()));
// 除去数组中的空值和签名参数
Map<String, String> sPara = paraFilter(sParaTemp);
// 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
String prestr = createLinkString(sPara);
// 生成签名结果
String mysign = DigestUtils.md5Hex(getContentBytes(prestr + APP_KEY, INPUT_CHARSET));
// 签名结果加入请求提交参数组中
sPara.put("sign", mysign);
return sPara;
}
public static String getSignStr(Map<String, String> sParaTemp) {
return sign(sParaTemp).get("sign");
}
/**
* 生成要请求的签名参数字符串“参数=参数值”&链接
*
* @param sParaTemp
* 需要签名的参数Map
* @return 请求的签名参数字符串
*/
public static String signStringMap(Map<String, String[]> sParaTemp) {
// 生成要请求的签名参数数组
Map<String, String> sign = signMap(sParaTemp);
// 生成要请求的签名参数字符串“参数=参数值”&链接
return createLinkString(sign, true);
}
/**
* 生成要请求的签名参数字符串“参数=参数值”&链接
*
* @param sParaTemp
* 需要签名的参数
* @return
*/
public static String signString(Map<String, String> sParaTemp) {
// 生成要请求的签名参数数组
Map<String, String> sign = sign(sParaTemp);
// 生成要请求的签名参数字符串“参数=参数值”&链接
return createLinkString(sign, true);
}
// TODO 验证签名
/**
* 根据反馈回来的信息,生成签名结果
*
* @param paramsMap
* 通知返回来的请求参数Map
* @return 验证结果
*/
public static boolean verifyMap(Map<String, String[]> paramsMap) {
// 请求参数Map转换验证Map,并根据反馈回来的信息,生成签名结果
return verify(toVerifyMap(paramsMap, false));
}
/**
* 根据反馈回来的信息,生成签名结果
*
* @param params
* 通知返回来的参数数组
* @return 验证结果
*/
public static boolean verify(Map<String, String> params) {
String sign = "";
if (params.get("sign") != null) {
sign = params.get("sign");
} else {
logger.info("sign is null");
return false;
}
String timestamp = "";
if (params.get("timestamp") != null) {
timestamp = params.get("timestamp");
} else {
return false;
}
// 过滤空值、sign
Map<String, String> sParaNew = paraFilter(params);
// 获取待签名字符串
String preSignStr = createLinkString(sParaNew);
// 获得签名验证结果
String mysign = DigestUtils.md5Hex(getContentBytes(preSignStr + APP_KEY, INPUT_CHARSET));
if (mysign.equals(sign)) {
// 是否超时
long curr = System.currentTimeMillis();
if ((curr - Long.valueOf(timestamp)) > TIME_OUT) {
logger.info("api is time out");
return false;
}
return true;
} else {
return false;
}
}
public static void main(String[] args) {
Map<String,String> param=new HashMap<>();
param.put("tradeNo",System.currentTimeMillis()+"");
param.put("payAmt","1000");
Map<String, String> signString = sign(param);
System.out.println(JSON.toJSONString(signString));
}
}
2.2 非对称加密
公钥和私钥互换,生成一对秘钥对,分别为公钥和私钥。
1.公钥加密,必须私钥解密
2.私钥加密,必须公钥解密
所以在很多支付接口中为了保证安全性,客户端采用公钥加密,服务器端采用私钥解密,就算别人客户端的公钥,也无法解密,因为解密数据必须要私钥,而私钥存放在服务器端。
最为著名的就是RSA
2.2.1 RSAUtils
package com.tostyle.xss.utils;
import org.apache.commons.codec.binary.Base64;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
public class RSAUtils {
public static final String CHARSET = "UTF-8";
public static final String RSA_ALGORITHM = "RSA"; // ALGORITHM ['ælgərɪð(ə)m] 算法的意思
public static Map<String, String> createKeys(int keySize) {
// 为RSA算法创建一个KeyPairGenerator对象
KeyPairGenerator kpg;
try {
kpg = KeyPairGenerator.getInstance(RSA_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("No such algorithm-->[" + RSA_ALGORITHM + "]");
}
// 初始化KeyPairGenerator对象,密钥长度
kpg.initialize(keySize);
// 生成密匙对
KeyPair keyPair = kpg.generateKeyPair();
// 得到公钥
Key publicKey = keyPair.getPublic();
String publicKeyStr = Base64.encodeBase64URLSafeString(publicKey.getEncoded());
// 得到私钥
Key privateKey = keyPair.getPrivate();
String privateKeyStr = Base64.encodeBase64URLSafeString(privateKey.getEncoded());
// map装载公钥和私钥
Map<String, String> keyPairMap = new HashMap<String, String>();
keyPairMap.put("publicKey", publicKeyStr);
keyPairMap.put("privateKey", privateKeyStr);
// 返回map
return keyPairMap;
}
/**
* 得到公钥
* @param publicKey 密钥字符串(经过base64编码)
* @throws Exception
*/
public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
// 通过X509编码的Key指令获得公钥对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
return key;
}
/**
* 得到私钥
* @param privateKey 密钥字符串(经过base64编码)
* @throws Exception
*/
public static RSAPrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
// 通过PKCS#8编码的Key指令获得私钥对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
return key;
}
/**
* 公钥加密
* @param data
* @param publicKey
* @return
*/
public static String publicEncrypt(String data, RSAPublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET), publicKey.getModulus().bitLength()));
} catch (Exception e) {
throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 私钥解密
* @param data
* @param privateKey
* @return
*/
public static String privateDecrypt(String data, RSAPrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), privateKey.getModulus().bitLength()), CHARSET);
} catch (Exception e) {
throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 私钥加密
* @param data
* @param privateKey
* @return
*/
public static String privateEncrypt(String data, RSAPrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
//每个Cipher初始化方法使用一个模式参数opmod,并用此模式初始化Cipher对象。此外还有其他参数,包括密钥key、包含密钥的证书certificate、算法参数params和随机源random。
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET), privateKey.getModulus().bitLength()));
} catch (Exception e) {
throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 公钥解密
* @param data
* @param publicKey
* @return
*/
public static String publicDecrypt(String data, RSAPublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), publicKey.getModulus().bitLength()), CHARSET);
} catch (Exception e) {
throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
}
}
//rsa切割解码 , ENCRYPT_MODE,加密数据 ,DECRYPT_MODE,解密数据
private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize) {
int maxBlock = 0; //最大块
if (opmode == Cipher.DECRYPT_MODE) {
maxBlock = keySize / 8;
} else {
maxBlock = keySize / 8 - 11;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] buff;
int i = 0;
try {
while (datas.length > offSet) {
if (datas.length - offSet > maxBlock) {
//可以调用以下的doFinal()方法完成加密或解密数据:
buff = cipher.doFinal(datas, offSet, maxBlock);
} else {
buff = cipher.doFinal(datas, offSet, datas.length - offSet);
}
out.write(buff, 0, buff.length);
i++;
offSet = i * maxBlock;
}
} catch (Exception e) {
throw new RuntimeException("加解密阀值为[" + maxBlock + "]的数据时发生异常", e);
}
byte[] resultDatas = out.toByteArray();
IOUtils.closeQuietly(out);
return resultDatas;
}
// 简单测试____________
public static void main(String[] args) throws Exception {
Map<String, String> keyMap = RSAUtils.createKeys(1024);
String publicKey = keyMap.get("publicKey");
String privateKey = keyMap.get("privateKey");
System.out.println("公钥: \n\r" + publicKey);
System.out.println("私钥: \n\r" + privateKey);
System.out.println("公钥加密——私钥解密");
String str="tostyle";
String encodedData = RSAUtils.publicEncrypt(str, RSAUtils.getPublicKey(publicKey)); //传入明文和公钥加密,得到密文
System.out.println("密文:\r\n" + encodedData);
String decodedData = RSAUtils.privateDecrypt(encodedData, RSAUtils.getPrivateKey(privateKey)); //传入密文和私钥,得到明文
System.out.println("解密后文字: \r\n" + decodedData);
}
}