步骤3:服务端比对 sign1 和 sign2 的值,若不一致,则认定为被篡改,判定为非法请求。
1.2.1 如何解决重放问题?
防重放,业界通常基于 nonce + timestamp 方案实现。每次请求接口时生成 timestamp 和 nonce 两个额外参数,其中 timestamp 代表当前请求时间,nonce 代表仅一次有效的随机字符串。生成这两个字段后,与其他参数一起进行签名,并发送至服务端。服务端接收请求后,先比较 timestamp 是否超过规定时间(如60秒),再查看 Redis 中是否存在 nonce,最后校验签名是否一致,是否有篡改。private static String getAppKey() { long num = IdUtils.nextId(); StringBuilder sb = new StringBuilder(); do { int remainder = (int) (num % 62); sb.insert(0, BASE62_CHARACTERS.charAt(remainder)); num /= 62; } while (num != 0); return sb.toString(); }通过这个算法生成的 AppId 和 AppSecret 形如:
appKey=6iYWoL2hBk9, appSecret=5de8bc4d8278ed4f14a3490c0bdd5cbe369e8ec93.2 API校验器
//认证接口 public interface ApiAuthenticator { AuthenticatorResult auth(ServerWebExchange request); } //具体实现 @Slf4j public class ProtectedApiAuthenticator implements ApiAuthenticator { ... }3.2 网关过滤器
@Component @Slf4j public class ApiAuthenticatorFilter implements GlobalFilter, Ordered { ... @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 获取认证逻辑 ApiAuthenticator apiAuthenticator = getApiAuthenticator(rawPath); AuthenticatorResult authenticatorResult = apiAuthenticator.auth(exchange); if (!authenticatorResult.isResult()) { return Mono.error(new HttpServerErrorException( HttpStatus.METHOD_NOT_ALLOWED, authenticatorResult.getMessage())); } return chain.filter(exchange); } /** * 确定认证策略 * @param rawPath 请求路径 */ private ApiAuthenticator getApiAuthenticator(String rawPath) { String[] parts = rawPath.split("/"); if (parts.length >= 4) { String parameter = parts[3]; return switch (parameter) { case PROTECT_PATH -> new ProtectedApiAuthenticator(); case PRIVATE_PATH -> new PrivateApiAuthenticator(); case PUBLIC_PATH -> new PublicApiAuthenticator(); case DEFAULT_PATH -> new DefaultApiAuthenticator(); default -> throw new IllegalStateException("Unexpected value: " + parameter); }; } return new DefaultApiAuthenticator(); } }上面提到过,不同类型的服务其接口认证不一样,为了便于区分,可以规定对于外部请求都增加一个特定的请求前缀 /pt/,如 apigw.xxx.com/order-service/api/pt/creadeOrder。这样在过滤器内部就需要通过 getApiAuthenticator() 方法确定认证逻辑。
@Slf4j public class ProtectedApiAuthenticator implements ApiAuthenticator { @Override public AuthenticatorResult auth(ServerWebExchange exchange) { // 1. 校验参数 boolean checked = preAuthenticationCheck(requestHeader); if (!checked) { return new AuthenticatorResult(false, "请携带正确参数访问"); } // 2 . 重放校验 // 判断timestamp时间戳与当前时间是否操过60s(过期时间根据业务情况设置),如果超过了就提示签名过期。 long now = System.currentTimeMillis() ; if (now - Long.parseLong(requestHeader.getTimestamp()) > 60000) { return new AuthenticatorResult(false, "请求超时,请重新访问"); } // 3. 判断nonce boolean nonceExists = distributedCache.hasKey(NONCE_KEY + requestHeader.getNonce()); if (nonceExists) { return new AuthenticatorResult(false, "请勿重复提交请求"); } else { distributedCache.put(NONCE_KEY + requestHeader.getNonce(), requestHeader.getNonce(), 60000); } // 4. 签名校验 SortedMap<String, Object> requestBody = CachedRequestUtil.resolveFromBody(exchange); String sign = buildSign(requestHeader,requestBody); if(!sign.equals(requestHeader.getSign())){ return new AuthenticatorResult(false, "签名错误"); } return new AuthenticatorResult(true, ""); }这样的写法虽然能够完成校验逻辑,但稍显不够优雅。在这种场景中,使用设计模式中的责任链模式是非常合适的选择。通过责任链模式,将校验逻辑分解为多个责任链节点,每个节点专注于一个方面的校验,使得代码更加清晰和易于维护。
@Slf4j public class ProtectedApiAuthenticator implements ApiAuthenticator { @Override public AuthenticatorResult auth(ServerWebExchange exchange) { ... //构建校验对象 ProtectedRequest protectedRequest = ProtectedRequest.builder() .requestHeader(requestHeader) .requestBody(requestBody) .build(); //责任链上下文 SecurityVerificationChain securityVerificationChain = SpringBeanUtils.getInstance().getBean(SecurityVerificationChain.class); return securityVerificationChain.handler(protectedRequest); } }3.4 基于责任链的认证实现
public interface SecurityVerificationHandler extends Ordered { /** * 堆代码 duidaima.com * 请求校验 */ AuthenticatorResult handler(ProtectedRequest protectedRequest); }3.4.2 实现参数校验逻辑
@Component public class RequestParamVerificationHandler implements SecurityVerificationHandler { @Override public AuthenticatorResult handler(ProtectedRequest protectedRequest) { boolean checked = checkedHeader(protectedRequest.getRequestHeader()); if(!checked){ return new AuthenticatorResult(false,"请携带正确的请求参数"); } return new AuthenticatorResult(true,""); } private boolean checkedHeader(RequestHeader requestHeader) { return Objects.nonNull(requestHeader.getAppId()) && Objects.nonNull(requestHeader.getSign()) && Objects.nonNull(requestHeader.getNonce()) && Objects.nonNull(requestHeader.getTimestamp()); } @Override public int getOrder() { return 1; } }3.4.3 实现nonce的校验
@Component public class NonceVerificationHandler implements SecurityVerificationHandler { private static final String NONCE_KEY = "x-nonce-"; @Value("${dailymart.sign.timeout:60000}") private long expireTime ; @Resource private DistributedCache distributedCache; @Override public AuthenticatorResult handler(ProtectedRequest protectedRequest) { String nonce = protectedRequest.getRequestHeader().getNonce(); boolean nonceExists = distributedCache.hasKey(NONCE_KEY + nonce); if (nonceExists) { return new AuthenticatorResult(false, "请勿重复提交请求"); } else { distributedCache.put(NONCE_KEY + nonce, nonce, expireTime); return new AuthenticatorResult(true, ""); } } @Override public int getOrder() { return 3; } }3.4.4 实现签名认证
@Component @Slf4j public class SignatureVerificationHandler implements SecurityVerificationHandler { @Override public AuthenticatorResult handler(ProtectedRequest protectedRequest) { //1. 服务端按照规则重新签名 String serverSign = sign(protectedRequest); log.info("服务端签名结果: {}", serverSign); String clientSign = protectedRequest.getRequestHeader().getSign(); // 2、获取客户端传递的签名 log.info("客户端签名: {}", clientSign); if (!Objects.equals(serverSign,clientSign)) { return new AuthenticatorResult(false, "请求签名无效"); } return new AuthenticatorResult(true, ""); } /** * 服务端重建签名 * @param protectedRequest 请求体 * @return 签名结果 */ private String sign(ProtectedRequest protectedRequest) { RequestHeader requestHeader = protectedRequest.getRequestHeader(); String appId = requestHeader.getAppId(); String appSecret = getAppSecret(appId); // 1、 按照规则对数据进行签名 SortedMap<String, Object> requestBody = protectedRequest.getRequestBody(); requestBody.put("app_id",appId); requestBody.put("nonce_number",requestHeader.getNonce()); requestBody.put("request_time",requestHeader.getTimestamp()); StringBuilder signBuilder = new StringBuilder(); for (Map.Entry<String, Object> entry : requestBody.entrySet()) { signBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } signBuilder.append("appSecret=").append(appSecret); return DigestUtils.md5DigestAsHex(signBuilder.toString().getBytes()).toUpperCase(); } @Override public int getOrder() { return 4; } }3.4.5 责任链上下文
@Component @Slf4j public class SecurityVerificationChain { @Resource private List<SecurityVerificationHandler> securityVerificationHandlers; public AuthenticatorResult handler(ProtectedRequest protectedRequest){ AuthenticatorResult authenticatorResult = new AuthenticatorResult(true,""); for (SecurityVerificationHandler securityVerificationHandler : securityVerificationHandlers) { AuthenticatorResult result = securityVerificationHandler.handler(protectedRequest); // 有一个校验不通过理解返回 if(!result.isResult()){ return result; } } return authenticatorResult; } }组合所有的校验逻辑,任意一个校验逻辑不通过则直接返回。