原创 一安 一安未来 2023-12-01 08:00 发表于北京
收录于合集#干货分享集191个
大家好,我是一安~
介绍
开放接口
开放接口是指不需要登录凭证就允许被第三方系统调用的接口,这个时候肯定要考虑接口数据的安全性问题,比如数据是否被篡改,数据是否已经过时,数据是否可以重复提交等问题,为了防止开放接口被恶意调用,开放接口一般都需要验签才能被调用。
验签
验签是指第三方系统在调用接口之前,需要按照接口提供方的规则根据所有请求参数生成一个签名(字符串),在调用接口时携带该签名。接口提供方会验证签名的有效性,只有签名验证有效才能正常调用接口,否则请求会被驳回。
大致流程
实战
这里只是演示,未真正区分开客户端和服务端
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.13</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
自定义配置
私钥和公钥直接保存在文件中
spring:
redis:
host: localhost
port: 6379
password: root
database: 2
signature:
enable: true
key-pair:
# 调用方ID
test-1:
# 算法
algorithm: SHA256withRSA
# 私钥
private-key-path: classpath:signature/private
# 公钥
public-key-path: classpath:signature/public
# 生效时间(分钟)
effective-time: 3
加载配置信息
@Data
@ConditionalOnProperty(value = "signature.enable", havingValue = "true")
@Component
@ConfigurationProperties("signature")
public class SignatureProps {
private Boolean enable;
private Map<String, KeyPairProps> keyPair;
@Data
public static class KeyPairProps {
private SignAlgorithm algorithm;
private String publicKeyPath;
private String publicKey;
private String privateKeyPath;
private String privateKey;
private Integer effectiveTime;
}
}
签名管理类
@ConditionalOnBean(SignatureProps.class)
@Component
public class SignatureManager {
private final SignatureProps signatureProps;
public SignatureManager(SignatureProps signatureProps) {
this.signatureProps = signatureProps;
loadKeyPairByPath();
}
public SignatureProps.KeyPairProps getKeyPairPropsByClientID(String clientID) {
return signatureProps.getKeyPair().get(clientID);
}
/**
* 验签。验证不通过可能抛出运行时异常CryptoException
*
* @param clientID 调用方的唯一标识
* @param rawData 原数据
* @param signature 待验证的签名(十六进制字符串)
* @return 验证是否通过
*/
public boolean verifySignature(String clientID, String rawData, String signature) {
Sign sign = getSignByClientID(clientID);
if (ObjectUtils.isEmpty(sign)) {
return false;
}
// 使用公钥验签
return sign.verify(rawData.getBytes(StandardCharsets.UTF_8), HexUtil.decodeHex(signature));
}
/**
* 生成签名
*
* @param clientID 调用方的唯一标识
* @param rawData 原数据
* @return 签名(十六进制字符串)
*/
public String sign(String clientID, String rawData) {
Sign sign = getSignByClientID(clientID);
if (ObjectUtils.isEmpty(sign)) {
return null;
}
return sign.signHex(rawData);
}
private Sign getSignByClientID(String clientID) {
SignatureProps.KeyPairProps keyPairProps = signatureProps.getKeyPair().get(clientID);
if (ObjectUtils.isEmpty(keyPairProps)) {
return null; // 无效的、不受信任的调用方
}
return SecureUtil.sign(keyPairProps.getAlgorithm(), keyPairProps.getPrivateKey(), keyPairProps.getPublicKey());
}
/**
* 加载非对称密钥对
*/
private void loadKeyPairByPath() {
// 支持类路径配置,形如:classpath:signature/public
// 公钥和私钥都是base64编码后的字符串
signatureProps.getKeyPair()
.forEach((key, keyPairProps) -> {
keyPairProps.setPublicKey(loadKeyByPath(keyPairProps.getPublicKeyPath()));
keyPairProps.setPrivateKey(loadKeyByPath(keyPairProps.getPrivateKeyPath()));
if (ObjectUtils.isEmpty(keyPairProps.getPublicKey()) ||
ObjectUtils.isEmpty(keyPairProps.getPrivateKey())) {
throw new RuntimeException("No public and private key files configured");
}
});
}
private String loadKeyByPath(String path) {
if (ObjectUtils.isEmpty(path)) {
return null;
}
return IoUtil.readUtf8(ResourceUtil.getStream(path));
}
}
自定义验签注解
自定义验签注解,控制哪些接口需要验签
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface VerifySignature {
boolean resubmit() default true;//允许重复请求
}
切面验签
@ConditionalOnBean(SignatureProps.class)
@Component
@Slf4j
@Aspect
public class RequestSignatureAspect {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private SignatureManager signatureManager;
@Pointcut("@annotation(org.example.sign.anno.VerifySignature)")
public void signPointCut() {
}
@Before("signPointCut()")
public void verifySignature(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String clazz = joinPoint.getTarget().getClass().getName();
String name = method.getName();
VerifySignature verifySignature = method.getAnnotation(VerifySignature.class);
if(!verifySignature.resubmit()&&!redisTemplate.opsForValue().setIfAbsent(clazz+":"+name,"1",10, TimeUnit.SECONDS)){
throw new RuntimeException("不可重复提交");
}
HttpServletRequest request = getHttpServletRequest();
// 从请求头中提取调用法ID,不存在直接驳回
String clientID = request.getHeader("clientID");
if (ObjectUtils.isEmpty(clientID)) {
throw new RuntimeException("不受信任的调用方");
}
// 从请求头中提取签名,不存在直接驳回
String signature = request.getHeader("sign");
if (ObjectUtils.isEmpty(signature)) {
throw new RuntimeException("无效的签名");
}
// 从请求头中提取时间戳,不存在或者过期直接驳回
String timestamp = request.getHeader("timestamp");
if (ObjectUtils.isEmpty(timestamp) ||
ChronoUnit.MINUTES.between(
LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(timestamp)), ZoneOffset.ofHours(8)),
LocalDateTime.now()
) > signatureManager.getKeyPairPropsByClientID(clientID).getEffectiveTime()) {
throw new RuntimeException("签名已过期");
}
// 提取请求参数
String requestParamsStr = extractRequestParams(request);
// 验签。验签不通过抛出业务异常
verifySignature(clientID, requestParamsStr, signature);
}
/**
* 由于body输入流只能读取一次,所以拦截器中不能直接读取body的输入流,否则会造成后续@RequestBody的参数解析器读取不到body,因此需要使用ContentCachingRequestWrapper包装请求,缓存body内容
**/
private String extractRequestParams(HttpServletRequest request) {
// @RequestBody
String body = null;
if (request instanceof ContentCachingRequestWrapper) {
ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request;
body = new String(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
}
// @RequestParam
Map<String, String[]> paramMap = request.getParameterMap();
// @PathVariable
ServletWebRequest webRequest = new ServletWebRequest(request, null);
Map<String, String> uriTemplateVarNap = (Map<String, String>) webRequest.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
return CommonUtils.extractRequestParams(body, paramMap, uriTemplateVarNap);
}
/**
* 验证请求参数的签名
*/
public void verifySignature(String clientID, String requestParamsStr, String signature) {
try {
boolean verified = signatureManager.verifySignature(clientID, requestParamsStr, signature);
if (!verified) {
throw new RuntimeException("The signature verification result is false.");
}
} catch (Exception ex) {
log.error("Failed to verify signature", ex);
throw new RuntimeException("业务异常");
}
}
private HttpServletRequest getHttpServletRequest() {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
return sra.getRequest();
}
}
工具类
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory factory;
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
@Bean
public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForValue();
}
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
@Bean
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}
}
public class CommonUtils {
/**
* 提取所有的请求参数,按照固定规则拼接成一个字符串
*
* @param body post请求的请求体
* @param paramMap 路径参数(QueryString)。形如:name=zhangsan&age=18&label=A&label=B
* @param uriTemplateVarNap 路径变量(PathVariable)。形如:/{name}/{age}
* @return 所有的请求参数按照固定规则拼接成的一个字符串
*/
public static String extractRequestParams(String body, Map<String, String[]> paramMap, Map<String, String> uriTemplateVarNap) {
String paramStr = null;
if (!ObjectUtils.isEmpty(paramMap)) {
paramStr = paramMap.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> {
// 拷贝一份按字典序升序排序
String[] sortedValue = Arrays.stream(entry.getValue()).sorted().toArray(String[]::new);
return entry.getKey() + "=" + joinStr(",", sortedValue);
})
.collect(Collectors.joining("&"));
}
String uriVarStr = null;
if (!ObjectUtils.isEmpty(uriTemplateVarNap)) {
uriVarStr = joinStr(",", uriTemplateVarNap.values().stream().sorted().toArray(String[]::new));
}
// { userID: "xxx" }#name=zhangsan&age=18&label=A,B#zhangsan,18
return joinStr("#", body, paramStr, uriVarStr);
}
/**
* 使用指定分隔符,拼接字符串
*
* @param delimiter 分隔符
* @param strs 需要拼接的多个字符串,可以为null
* @return 拼接后的新字符串
*/
public static String joinStr(String delimiter,String... strs) {
if (ObjectUtils.isEmpty(strs)) {
return StrUtil.EMPTY;
}
StringBuilder sbd = new StringBuilder();
for (int i = 0; i < strs.length; i++) {
if (ObjectUtils.isEmpty(strs[i])) {
continue;
}
sbd.append(strs[i].trim());
if (!ObjectUtils.isEmpty(sbd) && i < strs.length - 1 && !ObjectUtils.isEmpty(strs[i + 1])) {
sbd.append(delimiter);
}
}
return sbd.toString();
}
}
request流只能获取一次
如果接口是用
@RequestBody
来接受数据,在拦截器中需要使用读取request
的输入流 ,但ServletRequest
中getReader()
和getInputStream()
只能调用一次
@ConditionalOnBean(SignatureProps.class)
@Component
public class RequestCachingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
boolean isFirstRequest = !isAsyncDispatch(request);
HttpServletRequest requestWrapper = request;
if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
requestWrapper = new ContentCachingRequestWrapper(request);
}
filterChain.doFilter(requestWrapper, response);
}
}
测试类
@RestController
public class controller {
@Resource
private SignatureManager signatureManager;
/**
* 数据验签
* 参数:{"name":"一安未来"}
**/
@VerifySignature(resubmit = false)
@PostMapping("/sign1")
public String sign1(@RequestBody Map<String,String> map){
System.out.println(map);
return "success";
}
/**
* 数据签名
* 参数:{"name":"一安未来"}
**/
@PostMapping("/sign2")
public String sign2(@RequestBody Map<String,String> map, HttpServletRequest httpRequest){
String clientID = httpRequest.getHeader("clientID");
String mapToJsonString = String.format("{%s}", String.join(",", map.entrySet().stream()
.map(e -> String.format("\"%s\":\"%s\"", e.getKey(), e.getValue()))
.collect(Collectors.toList())));
return signatureManager.sign(clientID,mapToJsonString);
}
}
一安未来
致力于Java,大数据;心得交流,技术分享;
122篇原创内容
公众号