背景:
由于安全因素考虑且针对某高校内网防止中间人攻击抓包偷取到用户的账户和密码,开始时考虑使用https进行通信验证,但是会弹出如一下页面影响用户体验,内网环境无法使用公网的证书进行证书认证等,所以直接使用RSA非对称加密对用户信息进行加密,保证用户账户密码无法明文获取。
同时也因为RSA通信加密原因之前使用的xss防御会因为编码加密使xss防御失效,所以想到了使用Spring Validation进行解密后的参数验证,防止XSS攻击。
所需依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.2.2</version>
</dependency>
后端代码
RSA加密工具类
package com.xjl.encrypt;
import com.xjl.common.exception.EasyInvoiceException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.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;
/**
* RSAUtil类提供了RSA算法的加密、解密以及密钥对生成功能。
* 公钥加密、私钥解密、私钥加密、公钥解密方法均实现了RSA算法的加密、解密过程。
* 密钥对的生成通过createKeys方法实现,支持指定密钥长度。
* RSAUtil使用了Apache Commons Codec和BouncyCastle库来辅助处理编码和RSA算法。
* <p>
* 作者:lkb
* 版本:1.0
*/
public class RSAUtil {
public static final String CHARSET = "UTF-8";
/**
* RSA_ALGORITHM常量指定了RSA算法。
*/
public static final String RSA_ALGORITHM = "RSA";
/**
* 创建指定长度的RSA密钥对。
*
* @param keySize 密钥长度
* @return 包含公钥和私钥的Map对象
*/
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.encodeBase64String(publicKey.getEncoded());
// 得到私钥
Key privateKey = keyPair.getPrivate();
String privateKeyStr = Base64.encodeBase64String(privateKey.getEncoded());
// map装载公钥和私钥
Map<String, String> keyPairMap = new HashMap<String, String>();
keyPairMap.put("publicKey", publicKeyStr);
keyPairMap.put("privateKey", privateKeyStr);
// 返回map
return keyPairMap;
}
/**
* 从base64编码的字符串中获取公钥。
*
* @param publicKey 经过base64编码的公钥字符串
* @return RSAPublicKey对象
*/
public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
// 通过X509编码的Key指令获得公钥对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
return (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
}
/**
* 从base64编码的字符串中获取私钥。
*
* @param privateKey 经过base64编码的私钥字符串
* @return RSAPrivateKey对象
*/
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));
return (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
}
/**
* 使用公钥对数据进行加密。
*
* @param data 待加密的数据
* @param publicKey 公钥对象
* @return 经过加密的base64编码字符串
*/
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 EasyInvoiceException("加密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 使用私钥对数据进行解密。
*
* @param data 待解密的数据,经过base64编码
* @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 经过加密的base64编码字符串
*/
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);
}
}
/**
* 使用公钥对
* <p>
* 数据进行解密。
*
* @param data 待解密的数据,经过base64编码
* @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算法数据加解密过程中的分段处理。
*
* @param cipher Cipher对象,用于加解密
* @param opmode 加密或解密模式
* @param datas 待处理的数据
* @param keySize 密钥长度
* @return 处理后的数据
*/
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;
}
/**
* RSAUtil类的主方法,用于测试生成密钥对功能。
*/
public static void main(String[] args) {
// 创建密钥对
Map<String, String> keys = RSAUtil.createKeys(1024);
// 从Map中获取密钥对
String publicKey = keys.get("publicKey");
String privateKey = keys.get("privateKey");
// 获取公钥
System.out.println("publicKey:" + publicKey);
// 获取私钥
System.out.println("privateKey:" + privateKey);
}
}
Json序列化器
package com.xjl.sys.log.utils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.json.JsonWriteFeature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Slf4j
public class Json {
/**
* 创建一个Jackson ObjectMapper实例,用于处理JSON序列化和反序列化
*/
private static final ObjectMapper OBJECT_MAPPER = JsonMapper.builder()
// 在序列化时,排除空属性
.serializationInclusion(JsonInclude.Include.NON_EMPTY)
// 在序列化时,禁用空bean的异常
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
// 在序列化时,禁用将日期写为时间戳的功能
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
// 在反序列化时,禁用未知属性的异常
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
// 配置禁用非ASCII字符的转义
.configure(JsonWriteFeature.ESCAPE_NON_ASCII.mappedFeature(), false)
.build();
/**
* 将对象转换为JSON字符串
*
* @param object 要转换的对象
* @return 对象的JSON表示形式
*/
public static String toJsonString(Object object) {
try {
return OBJECT_MAPPER.writeValueAsString(object);
} catch (JsonProcessingException e) {
log.error("对象转json错误:", e);
}
return null;
}
/**
* 将JSON字符串转换为指定类型的对象
*
* @param json 要解析的JSON字符串
* @param clazz 目标对象的类型
* @return 解析后的对象
*/
public static <T> T parseObject(String json, Class<T> clazz) {
T result = null;
try {
result = OBJECT_MAPPER.readValue(json, clazz);
} catch (Exception e) {
log.error("对象转json错误:", e);
}
return result;
}
/**
* 将JSON字符串转换为指定类型的对象列表
*
* @param json 要解析的JSON字符串
* @param clazz 目标对象的数组类型
* @return 解析后的对象列表
*/
public static <T> List<T> parseArray(String json, Class<T[]> clazz) {
T[] result = null;
try {
result = OBJECT_MAPPER.readValue(json, clazz);
} catch (Exception e) {
log.error("Json转换错误:", e);
}
return result != null ? Arrays.asList(result) : Collections.emptyList();
}
/**
* 将JSON字符串转换为JsonNode对象,即映射
*
* @param jsonStr 要解析的JSON字符串
* @return JsonNode对象
*/
public static JsonNode parseJson(String jsonStr) {
JsonNode jsonNode = null;
try {
jsonNode = OBJECT_MAPPER.readTree(jsonStr);
} catch (Exception e) {
log.error("Json转换错误:", e);
}
return jsonNode;
}
public static JsonNode parseJson(String jsonStr, String key) {
JsonNode jsonNode = null;
try {
jsonNode = OBJECT_MAPPER.readTree(jsonStr);
} catch (Exception e) {
log.error("Json转换错误:", e);
}
assert jsonNode != null;
return jsonNode.get(key);
}
/**
* 获取全局的ObjectMapper实例,以便进行更高级的自定义配置
*
* @return ObjectMapper实例
*/
public static ObjectMapper getObjectMapper() {
return OBJECT_MAPPER;
}
}
加解密的拦截实现
实现类
package com.xjl.encrypt;
import com.xjl.annotation.ApiDecrypt;
import com.xjl.config.EncryptConfig;
import com.xjl.sys.log.utils.Json;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.util.StreamUtils;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
/**
* @author lkb
* @version 1.0
*/
@Slf4j
public class DecryptHttpInputMessage implements HttpInputMessage {
private final HttpHeaders headers;
private final InputStream body;
public DecryptHttpInputMessage(HttpInputMessage inputMessage, EncryptConfig encryptConfig, ApiDecrypt decrypt) throws Exception {
String privateKey = encryptConfig.getPrivateKey();
String charset = encryptConfig.getCharset();
boolean showLog = encryptConfig.isShowLog();
boolean timestampCheck = encryptConfig.isTimestampCheck();
if (StringUtils.isEmpty(privateKey)) {
throw new IllegalArgumentException("privateKey is null");
}
if (StringUtils.isEmpty(privateKey)) {
throw new IllegalArgumentException("privateKey is null");
}
this.headers = inputMessage.getHeaders();
String content = StreamUtils.copyToString(inputMessage.getBody(), Charset.defaultCharset());
String hex = String.valueOf(Json.parseJson(content, "data")).replaceAll("\"", "");
String decryptBody = RSAUtil.privateDecrypt(hex, RSAUtil.getPrivateKey(encryptConfig.getPrivateKey()));
System.out.println(decryptBody);
if (showLog) {
log.info("Encrypted data received:{},After decryption:{}", content, decryptBody);
}
// 开启时间戳检查
if (timestampCheck) {
// 容忍最小请求时间
long toleranceTime = System.currentTimeMillis() - decrypt.timeout();
long requestTime = Json.parseJson(decryptBody, "timestamp").asLong();
// 如果请求时间小于最小容忍请求时间, 判定为超时
if (requestTime < toleranceTime) {
log.error("Encryption request has timed out, toleranceTime:{}, requestTime:{}, After decryption:{}", toleranceTime, requestTime, decryptBody);
throw new EncryptRequestException("request timeout");
}
}
this.body = new ByteArrayInputStream(decryptBody.getBytes());
}
@Override
public @NotNull InputStream getBody() {
return body;
}
@Override
public @NotNull HttpHeaders getHeaders() {
return headers;
}
}
package com.xjl.encrypt;
import com.xjl.annotation.ApiDecrypt;
import com.xjl.config.EncryptConfig;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Objects;
/**
* @author lkb
* @version 1.0
*/
@ControllerAdvice
@Slf4j
public class EncryptRequestBodyAdvice implements RequestBodyAdvice {
private boolean encrypt;
private ApiDecrypt decryptAnnotation;
@Resource
private EncryptConfig encryptConfig;
@Override
public boolean supports(MethodParameter methodParameter, @NotNull Type targetType, @NotNull Class<? extends HttpMessageConverter<?>> converterType) {
Method method = methodParameter.getMethod();
if (Objects.isNull(method)) {
encrypt = false;
return false;
}
if (method.isAnnotationPresent(ApiDecrypt.class) && encryptConfig.isOpen()) {
encrypt = true;
decryptAnnotation = methodParameter.getMethodAnnotation(ApiDecrypt.class);
return true;
}
// 此处如果按照原逻辑直接返回encrypt, 会造成一次修改为true之后, 后续请求都会变成true, 在不支持时, 需要做修正
encrypt = false;
return false;
}
@Override
public Object handleEmptyBody(Object body, @NotNull HttpInputMessage inputMessage, @NotNull MethodParameter parameter, @NotNull Type targetType, @NotNull Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
@Override
public @NotNull HttpInputMessage beforeBodyRead(@NotNull HttpInputMessage inputMessage, @NotNull MethodParameter parameter, @NotNull Type targetType,
@NotNull Class<? extends HttpMessageConverter<?>> converterType) {
if (encrypt) {
try {
return new DecryptHttpInputMessage(inputMessage, encryptConfig, decryptAnnotation);
} catch (EncryptRequestException e) {
throw e;
} catch (Exception e) {
log.error("Decryption failed", e);
}
}
return inputMessage;
}
@Override
public @NotNull Object afterBodyRead(@NotNull Object body, @NotNull HttpInputMessage inputMessage, @NotNull MethodParameter parameter, @NotNull Type targetType,
@NotNull Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
}
package com.xjl.encrypt;
/**
* 请求加密超时异常
*
* @author lkb
* @version 1.0
*/
public class EncryptRequestException extends RuntimeException {
public EncryptRequestException(String msg) {
super(msg);
}
}
package com.xjl.encrypt;
import cn.hutool.core.codec.Base64;
import com.xjl.annotation.ApiEncrypt;
import com.xjl.config.EncryptConfig;
import com.xjl.sys.log.utils.Json;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* @author lkb
* @version 1.0
*/
@Slf4j
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {
private static final ThreadLocal<Boolean> ENCRYPT_LOCAL = new ThreadLocal<>();
private boolean encrypt;
@Resource
private EncryptConfig encryptConfig;
@Override
public boolean supports(MethodParameter returnType, @NotNull Class<? extends HttpMessageConverter<?>> converterType) {
Method method = returnType.getMethod();
if (Objects.isNull(method)) {
return encrypt;
}
encrypt = method.isAnnotationPresent(ApiEncrypt.class) && encryptConfig.isOpen();
return encrypt;
}
@Override
public Object beforeBodyWrite(Object body, @NotNull MethodParameter returnType, @NotNull MediaType selectedContentType,
@NotNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response) {
Boolean status = ENCRYPT_LOCAL.get();
if (null != status && !status) {
ENCRYPT_LOCAL.remove();
return body;
}
if (encrypt) {
String publicKey = encryptConfig.getPublicKey();
try {
String content = Json.toJsonString(body);
if (!StringUtils.hasText(publicKey)) {
throw new NullPointerException("Please configure encrypt.privateKey parameter!");
}
assert content != null;
String encodedData = RSAUtil.publicEncrypt(content, RSAUtil.getPublicKey(encryptConfig.getPrivateKey()));
String result = Base64.encode(encodedData);
if (encryptConfig.isShowLog()) {
log.info("Pre-encrypted data:{},After encryption:{}", content, result);
}
return result;
} catch (Exception e) {
log.error("Encrypted data exception", e);
}
}
return body;
}
}
注解
package com.xjl.annotation;
import java.lang.annotation.*;
/**
* @author lkb
* @version 1.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiDecrypt {
/**
* 请求参数一定要是加密内容
*/
boolean required() default false;
/**
* 请求数据时间戳校验时间差
* 超过(当前时间-指定时间)的数据认定为伪造
* 注意应用程序需要捕获 {@link Exception} 异常
*/
long timeout() default 3000;
}
package com.xjl.annotation;
import java.lang.annotation.*;
/**
* 强制加密注解
*
* @author lkb
* @version 1.0
*/
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiEncrypt {
/**
* 响应加密忽略,默认不加密,为 true 时加密
*/
boolean response() default false;
}
package com.xjl.annotation;
import com.xjl.config.EncryptConfig;
import com.xjl.encrypt.EncryptRequestBodyAdvice;
import com.xjl.encrypt.EncryptResponseBodyAdvice;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* @author lkb
* @version 1.0
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import({EncryptConfig.class,
EncryptResponseBodyAdvice.class,
EncryptRequestBodyAdvice.class})
public @interface EnableApiSecurity {
}
XSS全局实现与注解验参实现
XSS工具类
package com.xjl.xss;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;
/**
* 描述: 过滤 HTML 标签中 XSS 代码
*
* @author lkb
* @version 1.0
*/
public class XssUtil {
/**
* 使用自带的 basicWithImages 白名单
*/
private static final Safelist WHITE_LIST = Safelist.relaxed();
/**
* 配置过滤化参数, 不对代码进行格式化
*/
private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false);
static {
// 富文本编辑时一些样式是使用 style 来进行实现的
// 比如红色字体 style="color:red;"
// 所以需要给所有标签添加 style 属性
// WHITE_LIST.addAttributes(":all", "style");
}
public static String clean(String content) {
return Jsoup.clean(content, "", WHITE_LIST, OUTPUT_SETTINGS);
}
}
XSS全局拦截实现
package com.xjl.xss;
import cn.hutool.core.util.StrUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
/**
* xss 攻击过滤
*
* @author lkb
* @version 1.0
*/
public class XssWrapper extends HttpServletRequestWrapper {
/**
* Constructs a request object wrapping the given request.
*
* @param request The request to wrap
* @throws IllegalArgumentException if the request is null
*/
public XssWrapper(HttpServletRequest request) {
super(request);
}
/**
* 对数组参数进行特殊字符过滤
*/
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) {
return null;
}
int count = values.length;
String[] encodedValues = new String[count];
for (int i = 0; i < count; i++) {
encodedValues[i] = cleanXss(values[i]);
}
return encodedValues;
}
/**
* 对参数中特殊字符进行过滤
*/
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (StrUtil.isBlank(value)) {
return value;
}
return cleanXss(value);
}
/**
* 获取attribute,特殊字符过滤
*/
@Override
public Object getAttribute(String name) {
Object value = super.getAttribute(name);
if (value instanceof String && StrUtil.isNotBlank((String) value)) {
return cleanXss((String) value);
}
return value;
}
/**
* 对请求头部进行特殊字符过滤
*/
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
if (StrUtil.isBlank(value)) {
return value;
}
return cleanXss(value);
}
private String cleanXss(String value) {
return XssUtil.clean(value);
}
}
package com.xjl.xss;
import cn.hutool.core.util.ReUtil;
import cn.hutool.http.HtmlUtil;
import com.xjl.annotation.Xss;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 自定义xss校验注解实现
*
* @author lkb
* @version 1.0
*/
public class XssValidator implements ConstraintValidator<Xss, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
return !ReUtil.contains(HtmlUtil.RE_HTML_MARK, value);
}
}
package com.xjl.xss;
import cn.hutool.core.io.IoUtil;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* 构建可重复读取inputStream的request
*
* @author lkb
* @version 1.0
*/
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {
super(request);
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
body = IoUtil.readBytes(request.getInputStream(), false);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public int available() throws IOException {
return body.length;
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
package com.xjl.filter;
import com.xjl.xss.XssWrapper;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 一些简单的安全过滤:
*
* @author lkb
* @version 1.0
*/
@Slf4j
public class XssFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// log.info("uri:{}", req.getRequestURI());
// xss 过滤
chain.doFilter(new XssWrapper(req), resp);
}
@Override
public void destroy() {
}
}
package com.xjl.filter;
import com.xjl.xss.RepeatedlyRequestWrapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Repeatable 过滤器
*
* @author lkb
* @version 1.0
*/
public class RepeatableFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ServletRequest requestWrapper = null;
if (request instanceof HttpServletRequest
&& StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
}
if (null == requestWrapper) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
}
@Override
public void destroy() {
}
}
使用Spring Validation参数验证,实现XSS防御
package com.xjl.annotation;
import com.xjl.xss.XssValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author lkb
* @version 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Constraint(validatedBy = {XssValidator.class})
public @interface Xss {
String message() default "不允许任何脚本运行";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
配置类
Filter配置类
package com.xjl.config;
import com.xjl.filter.RepeatableFilter;
import com.xjl.filter.XssFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.DispatcherType;
/**
* @author lkb
* @version 1.0
*/
@Configuration
public class FilterConfig {
/**
* xss Filter
*
* @return 返回过滤后的数据
*/
@Bean
public FilterRegistrationBean<XssFilter> xssFilterRegistration() {
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new XssFilter());
registration.addUrlPatterns("/*");
registration.setName("xssFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
return registration;
}
/**
* RepeatableFilter
*
* @return 返回过滤后的数据
*/
@Bean
public FilterRegistrationBean<RepeatableFilter> repeatBodyReaderFilter() {
FilterRegistrationBean<RepeatableFilter> registration = new FilterRegistrationBean<>();
RepeatableFilter filter = new RepeatableFilter();
registration.setFilter(filter);
registration.addUrlPatterns("/*");
registration.setName("repeatableFilter");
registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
return registration;
}
}
加解密配置类
package com.xjl.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author lkb
* @version 1.0
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "api.encrypt")
public class EncryptConfig {
/**
* 公钥
*/
private String publicKey;
/**
* 私钥
*/
private String privateKey;
private String charset = "UTF-8";
private boolean open = true;
private boolean showLog = true;
/**
* 请求数据时间戳校验时间差
* 超过指定时间的数据认定为伪造
*/
private boolean timestampCheck = false;
}
加解密配置类
package com.xjl.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author lkb
* @version 1.0
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "api.encrypt")
public class EncryptConfig {
/**
* 公钥
*/
private String publicKey;
/**
* 私钥
*/
private String privateKey;
private String charset = "UTF-8";
private boolean open = true;
private boolean showLog = true;
/**
* 请求数据时间戳校验时间差
* 超过指定时间的数据认定为伪造
*/
private boolean timestampCheck = false;
}
YAML文件填写加解密配置信息
api:
encrypt:
open: true
show-log: true
#公钥
public-key:
#私钥
private-key:
使用与案例
在需要的Application启动类添加@EnableApiSecurity注解启动加解密
@EnableApiSecurity
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}
指定接口使用RSA加密与XSS防御效验
@PostMapping("/login")
@RefreshLimit
@ApiDecrypt
public Result<?> login(@Valid @RequestBody AuthenticationDTO authenticationDTO) throws IOException {
// 实现逻辑
}
实体类验参
package com.xjl.dto;
import com.xjl.annotation.Xss;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
/**
* @author lkb
* @version 1.0
*/
@Data
public class AuthenticationDTO {
/**
* 验证码id
*/
@Xss(message = "用户账号不能包含脚本字符")
@ApiModelProperty(value = "验证码id", required = true)
private String id;
/**
* 用户名
*/
@Pattern(regexp = "^[a-zA-Z0-9]{1,10}$", message = "{valid.format}")
@Xss(message = "用户账号不能包含脚本字符")
@ApiModelProperty(value = "用户名", required = true)
protected String userName;
/**
* 密码
*/
@Length(min = 1, max = 50, message = "{valid.password.length}")
@NotBlank(message = "passWord不能为空")
@Xss(message = "用户账号不能包含脚本字符")
@ApiModelProperty(value = "密码", required = true)
protected String passWord;
}
前端代码
前端为Vue3
示例代码
import { JSEncrypt } from "encryptlong"
/** 登录按钮 Loading */
const loading = ref(false)
/** 验证码图片 URL */
// const codeUrl = ref("")
/** 登录表单数据 */
const loginFormData: LoginRequestData = reactive({
userName: "",
passWord: "",
id: ""
})
/** 登录表单校验规则 */
const loginFormRules: FormRules = {
userName: [{ required: true, message: "请输入用户名", trigger: "blur" }],
passWord: [
{ required: true, message: "请输入密码", trigger: "blur" },
{ min: 4, max: 16, message: "长度在 4 到 16 个字符", trigger: "blur" }
],
code: [{ required: true, message: "请输入验证码", trigger: "blur" }]
}
// const verify = ref()
// const captchaType = ref("blockPuzzle") // blockPuzzle 滑块 clickWord 点击文字
// const captcha: LoginCodeData = reactive({
// captchaType: captchaType
// })
// 获取验证码
// function testpost(captchaVerification: any) {
// axios({
// method: "POST",
// url: "http://localhost:9998/captcha/get",
// data: {
// captchaVerification
// }
// }).then((res: any) => {
// codeUrl.value = res.data
// })
// }
// const getCode = () => {
// // testpost(loginFormData.captchaVerification)
// getLoginCodeApi(captcha).then((res: any) => {
// console.log(res.repData.captchaVerification)
// })
// console.log(captcha)
// verify.value.show()
// }
/** 登录逻辑 */
const handleLogin = () => {
router.push({ path: "/" })
const rsa = { data: RSA(loginFormData) }
loading.value = true
useUserStore()
.login(rsa)
.then((res: any) => {
router.push({ path: "/" })
ElMessage.success(res.msg)
})
.catch(() => {
loginFormData.passWord = ""
})
.finally(() => {
loading.value = false
})
}
const RSA = (data: any) => {
const encryptor = new JSEncrypt() // 实例
encryptor.setPublicKey(
"*****"
) // 设置公匙
return encryptor.encryptLong(JSON.stringify(data)) // 进行加密并返回加密后的字符串
}
const loginBtn = () => {
loginFormRef.value?.validate((valid: boolean, fields: any) => {
if (valid) {
if (checked1.value === true) {
// 样式配置
const config = {
requestCaptchaDataUrl: "http://172.16.84.217:8080/captcha/gen",
validCaptchaUrl: "http://172.16.84.217:8080/captcha/check",
bindEl: "#captcha-div",
// 验证成功回调函数
validSuccess: (res: any, c: any, tac: any) => {
loginFormData.id = res.data.id
tac.destroyWindow()
handleLogin()
}
}
new window.TAC(config).init()
} else {
ElMessage.warning("请先同意用户协议")
}
} else {
ElMessage.error("表单校验不通过", fields)
}
})
}
const checked1 = ref(false)
</script>
<template>
<div class="login-container">
<ThemeSwitch class="theme-switch" />
<div class="login-card">
<div class="title">
<img src="@/assets/layouts/logo-invoice.png" />
</div>
<div class="content">
<el-form ref="loginFormRef" :model="loginFormData" :rules="loginFormRules" @keyup.enter="loginBtn()">
<el-form-item>
<div id="captcha-div" />
</el-form-item>
<el-form-item prop="userName">
<el-input
v-model.trim="loginFormData.userName"
placeholder="用户名"
type="text"
tabindex="1"
:prefix-icon="User"
size="large"
/>
</el-form-item>
<el-form-item prop="passWord">
<el-input
v-model.trim="loginFormData.passWord"
placeholder="密码"
type="password"
tabindex="2"
:prefix-icon="Lock"
size="large"
show-password
/>
</el-form-item>
<el-button :loading="loading" type="primary" size="large" @click.prevent="loginBtn">登 录</el-button>
</el-form>
<div style="display: flex; align-items: center">
<el-checkbox v-model="checked1" />
<router-link to="/userxieyi" target="_blank" style="margin-left: 2px; font-size: 14px">
<span style="color: blue">使用条款</span>
</router-link>
</div>
</div>
</div>
</div>
</template>