api接口大多都支持访问信息的验证,其中参数的排序,加密都是经常用到的。有时候还需要将验证信息放到header中。
将api调用者的参数的key及头信息(时间戳、随机串,调用者标识)按照ascii码升序排列后 用系统中的salt加密 ,得到的签名与调用者传入的签名进行比较。已实现验证合法性的目的。
下面就给大家介绍下我在项目中是如何使用的。大家多提意见。
-
创建切片类
package com.xxx.openapis.annotations import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Inherited public @interface OpenAPI { }
-
创建返回结果类
package com.xxx.openapis.web import java.io.Serializable; public class AjaxResult implements Serializable { private final int status; private final Object data; private AjaxResult(int status, Object data) { this.status = status; this.data = data; } public static AjaxResult error(int errorCode, String errorMessage) { return new AjaxResult(errorCode, errorMessage); } public static AjaxResult success() { return new AjaxResult(0, null); } public static AjaxResult success(Object data) { return new AjaxResult(0, data); } public int getStatus() { return status; } public Object getData() { return data; } }
-
创建一个实现org.springframework.core.Ordered 的抽象切片类
package com.xxx.openapis.aspects; import com.xxx.openapis.web.AjaxResult; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.springframework.core.Ordered; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; @Aspect public abstract class AbstractAPIAspect implements Ordered { static final String X_API_TIMESTAMP = "x_api_timestamp";//时间戳 static final String X_API_NONCE = "x_api_nonce"; //随机串 static final String X_API_PARTNERCODE = "x_api_partnercode";//调用者标识 static final String X_API_SIGNATURE = "x_api_signature";//签名 static final long NONCE_TTL = 20 * 60 * 1000L;//随机串时间范围 @Inject HttpServletRequest request; @Around("@within(com.xxx.openapis.annotations.OpenAPI) && execution(public com.xxx.web.AjaxResult *(..))") public Object aound(ProceedingJoinPoint jp) { getLogger().debug("检查API请求"); AjaxResult error = check(); if (error != null) { getLogger().error("检查API请求,发现问题:{}", error.getData()); return error; } try { getLogger().debug("检查API请求,没有发现问题继续"); return jp.proceed(); } catch (Throwable throwable) { getLogger().error("执行出错", throwable); return AjaxResult.error(500, "执行出错," + throwable.getMessage()); } } protected abstract Logger getLogger(); protected abstract AjaxResult check(); }
-
创建时间戳检查类继承AbstractAspect抽象类
package com.xxx.openapis.aspects; import com.xxx.openapis.web.AjaxResult; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import javax.inject.Named; import static org.springframework.util.StringUtils.hasText; @Aspect @Named @Slf4j public class TimestampChecker extends AbstractAPIAspect { static final int ORDER = 1;//执行顺序 @Override protected Logger getLogger() { return log; } @Override protected AjaxResult check() { String header = request.getHeader(X_API_TIMESTAMP); if (!hasText(header)) return AjaxResult.error(401, "未提供请求时间戳"); long timestamp; try { timestamp = Long.valueOf(header); } catch (NumberFormatException e) { return AjaxResult.error(401, "请求时间戳应该是系统时间的毫秒数"); } if (Math.abs(timestamp - System.currentTimeMillis()) > NONCE_TTL) return AjaxResult.error(403, "时间超过允许的范围,可能是“重播”攻击"); return null; } @Override public int getOrder() { return ORDER; } }
-
创建一个随机数检查类继承AbstractAspect抽象类
package com.xxx.openapis.aspects; import com.xxx.openapis.web.AjaxResult; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.springframework.data.redis.core.StringRedisTemplate; import javax.inject.Inject; import javax.inject.Named; import java.util.concurrent.TimeUnit; import static org.springframework.util.StringUtils.hasText; @Aspect @Named @Slf4j public class NonceChecker extends AbstractAPIAspect { static final int ORDER = TimestampChecker.ORDER + 1; private static final String KEY_PREFIX = "DISTRIBUTOR.API.NONCE."; @Inject private StringRedisTemplate redisTemplate; @Override protected Logger getLogger() { return log; } @Override protected AjaxResult check() { String nonce = request.getHeader(X_API_NONCE); if (!hasText(nonce)) return AjaxResult.error(401, "未提供请求随机数"); if (nonceInRedis(nonce)) return AjaxResult.error(403, "已接收相同的请求,怀疑是“重播”攻击。"); return null; } private boolean nonceInRedis(String nonce) { if (redisTemplate == null) { getLogger().debug("未启用Redis,忽略此项检查"); return false; } if (hasText(redisTemplate.opsForValue().get(KEY_PREFIX + nonce))) return true; redisTemplate.opsForValue().set(KEY_PREFIX + nonce, nonce, NONCE_TTL, TimeUnit.MILLISECONDS); return false; } @Override public int getOrder() { return ORDER; } }
-
创建用户验证类继承AbstractAPIAspect抽象类
package com.xxx.openapis.aspects; import com.xxx.openapis.models.User; import com.xxx.openapis.web.AjaxResult; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import javax.inject.Inject; import javax.inject.Named; import static org.springframework.util.StringUtils.hasText; @Aspect @Named @Slf4j public class UserChecker extends AbstractAPIAspect { static final int ORDER = NonceChecker.ORDER + 1; @Inject private UserService userService; @Override protected Logger getLogger() { return log; } @Override protected AjaxResult check() { String userCode = request.getHeader(X_API_PARTNERCODE); if (!hasText(userCode)) { return AjaxResult.error(401, "未提用户商编号"); } User user = userService.getUser(userCode); if (user == null) return AjaxResult.error(403, "用户编号错误,不存在此用户"); if (user.isLocked()) return AjaxResult.error(403, "用户编号错误,此用户已冻结"); request.setAttribute("user", user); return null; } @Override public int getOrder() { return ORDER; } }
-
创建签名验证类继承AbstractAPIAspect抽象类
package com.xxx.openapis.aspects; import com.xxx.openapis.models.User; import com.xxx.openapis.web.AjaxResult; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.springframework.util.StringUtils; import javax.inject.Named; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Arrays; import java.util.Map; import java.util.stream.Collectors; import static org.springframework.util.StringUtils.hasText; @Aspect @Named @Slf4j public class SignatureChecker extends AbstractAPIAspect { private static final int ORDER = VendorChecker.ORDER + 1; @Override protected Logger getLogger() { return log; } @Override protected AjaxResult check() { String callerSign = request.getHeader(X_API_SIGNATURE); if (!hasText(callerSign)) return AjaxResult.error(401, "未提供请求签名"); User user= (User) request.getAttribute("user"); String salt = user.getSalt(); String sign = sign(request.getHeader(X_API_TIMESTAMP), request.getHeader(X_API_NONCE), request.getHeader(X_API_PARTNERCODE), request.getParameterMap(), salt); getLogger().debug("收到的签名{}", callerSign); getLogger().debug("计算签名{}", sign); if (!callerSign.equalsIgnoreCase(sign)) return AjaxResult.error(403, "签名错误"); return null; } private String sign(final String timestamp, final String nonce, final String partnerCode, final Map<String, String[]> requestParameters, String salt) { //请求参数进行排序,按照ASCII码升序排列 String toSign = timestamp + nonce + partnerCode + requestParameters.entrySet() .stream() .map(entry -> { String key = entry.getKey(); String value = Arrays.stream(entry.getValue()) .map(this::encode) .filter(StringUtils::hasText) .sorted() .collect(Collectors.joining(",")); if (hasText(value)) return key + '=' + value; else return null; }) .filter(StringUtils::hasText) .sorted() .collect(Collectors.joining("&")) + salt; return DigestUtils.md5Hex(toSign); } private String encode(String value) { try { return URLEncoder.encode(value, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); return null; } } @Override public int getOrder() { return ORDER; } }
-
创建controller类
package com.xxx.openapis.conctollers; import com.xxx.openapis.annotations.OpenAPI; import com.xxx.openapis.web.AjaxResult; import static com.xinnet.market.web.AjaxResult.error; import static com.xinnet.market.web.AjaxResult.success; @RestController @RequestMapping("/api") @OpenAPI //此处重点 加上它相当于告诉系统先走上边的几个检查符合条件后才进控制层 public class DistributorAPI { @PostMapping("/test") public AjaxResult updatePayStatus(@RequestParam("test") String test) String test) { try { //调用后端业务逻辑 return success(); } catch (OrderNotMacherException e) { return error(509, e.getMessage()); } } }