0%

说明

本文通过 自定义注解 + Redis 实现接口限流。

实现

  • 自定义注解
@Inherited
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
// 默认就是60秒内可以访问2次
public @interface RequestLimit {
    /**
     * 时间,秒为单位
     */
    int second() default 60;
    /**
     * second时间内允许访问的次数
     */
    int maxCount() default 2;
}
  • 通过AOP实现限流
@Configuration
@EnableAspectJAutoProxy
@Aspect
@Slf4j
@Order(-5) // 如果有多个AOP,需要将Order设置到最小,优先判断限流
public class RequestLimitAspect {
    // 这里通过将访问信息保存在redis中,判断是否限流
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    // 定义切点,所有添加RequestLimit注解的
    @Pointcut(value = "@annotation(org.xxx.xxx.annotation.RequestLimit)")
    private void handleRequestLimit() {
    }

    @Around("handleRequestLimit()")
    public Object handleResponseEncrypt(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        RequestLimit requestLimit = method.getAnnotation(RequestLimit.class);
        // 没有添加注解直接放行
        if (null == requestLimit) {
            return pjp.proceed();
        }
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        // 获取HttpServletRequest
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        // 获取公网IP
        String ip = CommonUtils.getPublicIp(request);
        // 实现同一个公网IP 限流
        String key = "limit:" + ip + ":" + request.getServletPath();
        String count = redisTemplate.opsForValue().get(key);
        if (StringUtils.isEmpty(count)) {
            count = "1";
            redisTemplate.opsForValue().set(key, count, requestLimit.second(), TimeUnit.SECONDS);
            return pjp.proceed();
        }
        if (Integer.valueOf(count) < requestLimit.maxCount()) {
            redisTemplate.opsForValue().increment(key);
            return pjp.proceed();
        }
        log.info("RemoteAddr:{} 请求接口:{} 请求过于频繁, 已拒绝请求.", ip, request.getServletPath());
        HttpServletResponse response = requestAttributes.getResponse();
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.getWriter()
                // 触发限流自定义返回内容
                .println(JSON.toJSONString(new Response(Constant.ResponseCode.RequestLimit, Constant.ResponseMsg.RequestLimit)));
        return null;

    }
}
  • 获取公网IP
public static String getPublicIp(HttpServletRequest request) {
    String ip = null;
    try {
        ip = request.getHeader("x-forwarded-for");
        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
    } catch (Exception e) {
        log.error("getPublicIp ERROR ", e);
    }
    //使用代理,则获取第一个IP地址
    if (StringUtils.isEmpty(ip) && ip.length() > 15) {
        if (ip.indexOf(",") > 0) {
            ip = ip.substring(0, ip.indexOf(","));
        }
    }
    return ip;
}

说明

springboot配置ExceptionHandler不仅可以统一处理全局异常,还可以用来自定义404、405返回。

自定义404、405

  • 配置application.yml
# 捕获404异常需要开启以下配置,其它异常无需开启
spring:
  mvc:
    throw-exception-if-no-handler-found: true
  resources:
    add-mappings: false
  • 编写java
@RestControllerAdvice
@Slf4j
public class ExceptionHandle {
    @ExceptionHandler(Exception.class)
    public Object handlerException(Exception e) {
        // 请求接口地址不存在 404
        if (e instanceof NoHandlerFoundException) {
            // Response是自定义的一个返回对象
            return new Response(Constant.NoHandlerFound, e.getMessage());
        }
        // 请求方法不支持 405
        if (e instanceof HttpRequestMethodNotSupportedException) {
            return new Response(Constant.MethodNotSupported, e.getMessage());
        }
        //其他异常都可以捕获
        return new Response(Constant.sysError, Constant.sysError);
    }
}

说明

gson 实现 yyyyMMddHHmmss 格式的时间与 LocalDateTime 类型相互转换.

// 自定义序列化
JsonSerializer<LocalDateTime> serializer =
                (localDateTime, type, jsonSerializationContext) -> new JsonPrimitive(localDateTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
// 自定义反序列化
JsonDeserializer<LocalDateTime> deserializer =
        (jsonElement, type, jsonDeserializationContext) -> LocalDateTime.parse(jsonElement.getAsJsonPrimitive().getAsString(), DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
Gson gson = new GsonBuilder()
        .registerTypeAdapter(LocalDateTime.class, serializer)
        .registerTypeAdapter(LocalDateTime.class, deserializer).create();

// 测试 object to json
TestObject object = new TestObject();
object.setDate(LocalDateTime.now());
System.out.println(gson.toJson(object)); // {"date":"20221115141202"}
// 测试 json to object
String json = "{\"date\":\"20221115140534\"}";
System.out.println(gson.fromJson(json, TestObject.class)); // TestObject(date=2022-11-15T14:05:34)
  • TestObject.java
@Data
@ToString
public class TestObject {
    private LocalDateTime date;
}

说明

Nginx Ingress Controller 支持通过 annotation 的方式配置服务器与客户端之间的双向 HTTPS 认证来保证连接的安全性。

创建自签的CA证书

openssl req -x509 -sha256 -newkey rsa:4096 -keyout ca.key -out ca.crt -days 356 -nodes -subj '/CN=Fern Cert Authority'

创建Server端证书

# 生成Server端证书的key和请求文件
openssl req -new -newkey rsa:4096 -keyout server.key -out server.csr -nodes -subj '/CN=foo.bar.com'
# 使用根证书签发Server端请求文件,生成Server端证书
openssl x509 -req -sha256 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt

创建Client端证书

# 生成Client端证书的请求文件
openssl req -new -newkey rsa:4096 -keyout client.key -out client.csr -nodes -subj '/CN=Fern'
# 使用根证书签发Client端请求文件,生成Client端证书
openssl x509 -req -sha256 -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 02 -out client.crt

创建CA证书的Secret

# 默认default命名空间创建
kubectl create secret generic ca-secret --from-file=ca.crt=ca.crt

创建Server证书的Secret

kubectl create secret generic tls-secret --from-file=tls.crt=server.crt --from-file=tls.key=server.key

创建测试用的Nginx Ingress用例

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
    nginx.ingress.kubernetes.io/auth-tls-secret: "default/ca-secret"
    nginx.ingress.kubernetes.io/auth-tls-verify-depth: "1"
    nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: "true"
  name: nginx-test
  namespace: default
spec:
  rules:
  - host: foo.bar.com
    http:
      paths:
      - backend:
          serviceName: http-svc
          servicePort: 80
        path: /
  tls:
  - hosts:
    - foo.bar.com
    secretName: tls-secret

测试

# 客户端不传证书访问
curl --cacert ./ca.crt  https://foo.bar.com
# 客户端传证书访问
curl --cacert ./ca.crt --cert ./client.crt --key ./client.key https://foo.bar.com

说明

nginx 证书双向认证是认证客户端证书是否是配置的根证书颁发的.

创建根证书

#创建根证书私钥:
openssl genrsa -out ca.key 1024
#创建根证书请求文件:
openssl req -new -out ca.csr -key ca.key
#创建根证书:
openssl x509 -req -in ca.csr -out ca.crt -signkey ca.key -CAcreateserial -days 3650
  • ca.crt : 签名有效期为10年的根证书
  • ca.key: 根证书私钥文件
  • ca.csr: 根证书请求文件

根据根证书创建服务端证书

#生成服务器端证书私钥:
openssl genrsa -out server.key 1024
#生成服务器证书请求文件,过程和注意事项参考根证书,本节不详述:
openssl req -new -out server.csr -key server.key
#生成服务器端公钥证书
openssl x509 -req -in server.csr -out server.crt -signkey server.key -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650

根据根证书创建客户端证书

#生成客户端证书秘钥:
openssl genrsa -out client.key 1024
#生成客户端证书请求文件,过程和注意事项参考根证书,本节不详述:
openssl req -new -out client.csr -key client.key
#生客户端证书
openssl x509 -req -in client.csr -out client.crt -signkey client.key -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650
#生客户端p12格式证书,需要输入一个密码,比如123456
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12

client.p12:客户端 p12 格式,这个证书文件包含客户端的公钥和私钥,主要用来给浏览器或 postman 访问使用.

nginx配置

server {
    listen       443 ssl;
    server_name  www.yourdomain.com;# 无域名可填写ip
    ssl                  on;  
    ssl_certificate      /data/sslKey/server.crt;  #server公钥证书
    ssl_certificate_key  /data/sslKey/server.key;  #server私钥
    ssl_client_certificate /data/sslKey/ca.crt;  #根证书,可以验证所有它颁发的客户端证书
    ssl_verify_client on;  #开启客户端证书验证  
    ssl_verify_depth 1; #如果客户端证书不是由根证书直接颁发,需要开启此参数

    location / {
        proxy_pass http://127.0.0.1:8080/;
    }
    
    #location / {
    #    root   html;
    #    index  index.html index.htm;
    # }
}

访问测试

  • curl 带证书访问
   curl --cert ./client.crt --key ./client.key https://foo.bar.com
  • postman
  1. 设置 General 中先把 SSL certificate verification 关掉.
  2. Certificates 中选择Add Certificates,PFX file 配置客户端 p12 文件和密码.

springboot单向认证和双向认证

  • 单向认证
    服务端开启 SSL 证书,通过 springboot 或者 nginx 都可以实现, 开启单向认证后访问服务端接口需要使用 https 协议即可。
server:
  port: 8443
  ssl:
    enabled: true
    #服务端证书路径, classpath:local-dev.p12
    key-store: classpath:server.jks
    #证书密码
    key-store-passwd: 123456
    #证书类型
    key-store-type: JKS
  • 双向认证
    springboot 服务端除了开启 SSL 证书,还要开启客户端证书认证 client-auth: need, 需要将客户端证书导入到服务端信任库中, 否则访问报错, 提示无法建立连接, 不接受您的登录证书, 或者您可能没有提供登录证书 等报错信息.
server:
  port: 8443
  ssl:
    enabled: true
    #服务端证书路径, classpath:local-dev.p12
    key-store: classpath:server.jks
    #证书密码
    key-store-passwd: 123456
    #证书类型
    key-store-type: JKS
    #是否需要进行认证,可选: need/want/none
    client-auth: need
    #可信任的客户端证书, classpath:local-dev.p12
    trust-store: classpath:server.jks
    #密码,即步骤一中输入的密码
    trust-store-password: 123456
    trust-store-type: JKS

使用 keytool 创建证书

# 创建 test_server, 如果是单向认证创建完 test_server.jks 并配置正确即可.
keytool -genkeypair -alias test_server -keypass 123456 -storepass 123456  -dname "C=CN,ST=JS,L=NJ,O=test,OU=dev,CN=test.server.cn" -keyalg RSA -keysize 20
48 -validity 3650 -keystore test_server.jks

# 双向认证还需要进行以下步骤
# 使用 keytool 创建 test_client 证书库
keytool -genkeypair -alias test_client -keypass 123456 -storepass 123456  -dname "C=CN,ST=JS,L=NJ,O=test,OU=dev,CN=test.client.cn" -keyalg RSA -keysize 20
48 -validity 3650 -keystore test_client.jks
# 从证书库中导出客户端证书, 注意:加上 -rfc 选项输出PEM编码格式的证书, 否则导入服务端信任库会报错.
keytool -exportcert -keystore test_client.jks -rfc -file test_client.cer -alias test_client -storepass 123456
# 将客户端证书导入到服务端信任库
keytool -importcert -keystore test_server.jks -file test_client.cer -alias test_client -storepass 123456 -noprompt

在信任库中, 导入后的证书为 trustedCertEntry 实体类型,而私钥证书为 PrivateKeyEntry.

上述操作完成后, 就可以通过客户端证书调用服务端接口,但是想要服务端调用客户端,需要按照上述步骤将服务端证书导入到客户端信任库.

  • keytool 查看证书库详情
keytool -list -keystore test_server.jks -storepass 123456
# 加 -v 查看详情
keytool -list -v -keystore test_server.jks -storepass 123456
  • keytool 查看证书详情
keytool -printcert -file test_client.cer

openssl 创建自签名证书

# 创建私钥(.key)
openssl genrsa -out my.key 2048
# 基于私钥(.key)创建证书签名请求(.csr)
openssl req -new -key my.key -out my.csr -subj "/C=CN/ST=shanghai/L=shanghai/O=example/OU=it/CN=domain1/CN=domain2"
# (可选)直接同时生成私钥(.key)和证书签名请求(.csr)
openssl req -new -newkey rsa:2048 -nodes -keyout my.key -out my.csr -subj "/C=CN/ST=shanghai/L=shanghai/O=example/OU=it/CN=domain1/CN=domain2"
# 使用自己的私钥(.key)签署自己的证书签名请求(.csr),生成自签名证书(.crt)
openssl x509 -req -in my.csr -out my.crt -signkey my.key -days 3650
# (可选)直接同时生成私钥(.key)和自签名证书(.crt)
openssl req -x509 -newkey rsa:2048 -nodes -keyout my.key -out my.crt -days 3650  -subj "/C=CN/ST=shanghai/L=shanghai/O=example/OU=it/CN=domain1/CN=domain2"

openssl 创建私有CA签发的证书

# 生成CA私钥(ca.key)和CA自签名证书(ca.crt)
openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.crt -days 3650  -subj "/C=CN/ST=shanghai/L=shanghai/O=example/OU=it/CN=domain1/CN=domain2"
# 生成Server端私钥(server.key)和证书签名请求(server.csr)
openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/C=CN/ST=shanghai/L=shanghai/O=example/OU=it/CN=domain1/CN=domain2"
# 使用CA证书(ca.crt)与密钥(ca.key)签署服务器的证书签名请求(server.csr),生成私有CA签名的服务器证书(server.crt)
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650
# 验证server.crt是否真得是由ca签发的, 结果显示 ok
openssl verify -CAfile ca.crt server.crt

openssl创建的证书导入java证书信任库

keytool -importcert -keystore test_server.jks -file openssl_client.crt -alias test_client -storepass 123456 -noprompt

证书格式转换(参考链接)

  • jks / pkcs12 格式转换
# jks 转 p12
keytool -importkeystore -srckeystore server.keystore -destkeystore server.p12 -srcalias serverkey -destalias serverkey \
    -srcstoretype jks -deststoretype pkcs12 -srcstorepass 111111 -deststorepass 111111  -destkeypass  111111 -noprompt
# p12 转 jks 同理
keytool -importkeystore -srckeystore server.p12 -destkeystore server.keystore \
-srcstoretype pkcs12 -deststoretype jks -srcalias server -destalias server \
-deststorepass 111111 -srcstorepass 111111
  • Nginx 证书 转 JKS
    Java 通常使用 JKS 作为证书存储格式,而Nginx往往采用 PEM 证书格式
# pem证书和私钥合成p12,注意定义-name 选项,这将作为keystore识别实体的参数
openssl pkcs12 -export -in server.crt -inkey server.key -passin pass:111111 -password pass:111111 -name server -out server.p12
# p12 证书转jks 证书
keytool -importkeystore -srckeystore server.p12 -destkeystore server.keystore \
-srcstoretype pkcs12 -deststoretype jks -srcalias server -destalias server \
-deststorepass 111111 -srcstorepass 111111

如果 p12 文件中未指定实体名称,使用 keytool 转换时则不需提供 srcalias/destalias 参数,而输出的 keystore 实体名称默认为 1

  • JKS 证书 转 Nginx证书
# jks 证书转p12
keytool -importkeystore -srckeystore server.keystore  -destkeystore server.p12 \
-srcstoretype jks -deststoretype pkcs12 -srcalias server -destalias server \
-deststorepass 111111 -destkeypass 111111 -srcstorepass 111111
# p12 证书提取pem证书和私钥
openssl pkcs12 -in server.p12 -clcerts -nokeys -password pass:111111 -out server.crt
openssl pkcs12 -in server.p12  -nocerts -password pass:111111 -passout pass:111111 -out server.key

postman 双向认证测试

  1. 创建服务端信任库
  2. 创建客户端证书
  3. 将客户端证书添加到服务端信任库
  4. postman 设置选择:setting - General - 关闭SSL certificate verification
  5. postman 设置选择:setting - Certificates - Client Certificates - 选择 Add Certificates
  6. 设置地址端口号CAR fileKEY file,证书有密码就输入Passphrase
  7. 测试