文章

后端系列 - 一、设计一个安全的HTTP接口

背景

前提:客户端无法被逆向,但是可以被网络代理。

目标:在上述前提下,设计一个安全的接口服务。

基础访问安全

  1. ip白名单

  2. ip黑名单机制 / 配置黑名单

  3. 幂等

  4. 限流

  5. 防止重放攻击 时间戳验证

方案选型:

  1. 如果是普通接口,仅仅是为了验证,采用BA Auth或者代码硬编码校验一个key即可,采用方案一。

  2. 如果是权限接口,请求和响应中没有敏感数据,一般在云服务厂商场景,采用AK SK方案即可。

  3. 安全性要求很高,采用AES + RSA + AK + SK方案。

极端场景:

  1. 采用双向SSL/TLS认证

常见字段含义

序号

字段名称

类型

示例

1

clientVersion

header

1.0.0

2

appId

header

用于服务端客户端获取秘钥信息

3

appKey

header

用于权限拆分

4

appSecret

header

用于权限拆分

5

timeStamp

header

时间戳

6

nonce

header

随机字符串

7

sign

header

content和上述内容排序后拼接生成签名

方案一、BA Auth

BA Auth 通常指的是 Basic Authentication,即基本认证,这是一种简单的HTTP认证机制,用户通过用户名和密码进行认证。以下是使用Java实现基本认证的示例代码:



import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

public class BasicAuthFilter extends javax.servlet.Filter {

    private static final String REALM_NAME = "MyRealm";

    @Override

    public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)

            throws java.io.IOException {

        String authHeader = request.getHeader("Authorization");

        if (authHeader != null && authHeader.startsWith("Basic ")) {

            try {

                String base64Credentials = authHeader.substring("Basic ".length());

                String credentials = java.util.Base64.getDecoder().decode(base64Credentials).toString();

                int p = credentials.indexOf(":");

                if (p != -1) {

                    String username = credentials.substring(0, p).trim();

                    String password = credentials.substring(p + 1).trim();

                    // 这里可以添加你的用户名和密码验证逻辑

                    if ("yourUsername".equals(username) && "yourPassword".equals(password)) {

                        chain.doFilter(request, response);

                        return;

                    }

                }

            } catch (IllegalArgumentException e) {

                // 解码失败

            }

        }

        response.setHeader("WWW-Authenticate", "Basic realm=\"" + REALM_NAME + "\"");

        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");

    }

    @Override

    public void init(javax.servlet.FilterConfig filterConfig) {

        // 初始化代码,如果有的话

    }

    @Override

    public void destroy() {

        // 清理代码,如果有的话

    }

}

在这个示例中,BasicAuthFilter 类扩展了 javax.servlet.Filter 类,覆盖了 doFilter 方法来实现基本认证。当用户尝试访问受保护的资源时,如果请求中没有包含有效的用户名和密码,服务器将返回401状态码和相应的WWW-Authenticate头,提示客户端需要进行基本认证。

你需要将这个过滤器配置到你的Web应用程序中,通常在`web.xml`文件中进行配置,或者使用注解或编程式的方式来注册这个过滤器。

请根据你的实际需求调整用户名和密码验证逻辑以及realm名称。

方案二、AK + SK

AK/SK(Access Key ID/Secret Access Key)是一种常见的认证方式,通常用于API调用,特别是在云服务提供商的API中,例如AWS、腾讯云、阿里云等。AK/SK认证机制通常包括以下步骤:

1. 用户注册并获取一对密钥,即Access Key ID(AK)和Secret Access Key(SK)。

2. 用户在API请求中包含AK和签名(使用SK生成)。

3. 服务端接收到请求后,验证签名的正确性,如果签名正确,则处理请求。

以下是一个使用Java实现AK/SK认证的简单示例。这个示例假设你已经有一个服务端,它接收API请求并验证签名。这里我们只实现客户端的签名生成部分。

import java.security.MessageDigest;

import java.security.NoSuchAlgorithmException;

import java.util.Base64;

import java.util.TreeMap;

public class AKSKAuth {

    private static final String ACCESS_KEY_ID = "your-access-key-id";

    private static final String SECRET_ACCESS_KEY = "your-secret-access-key";

    public static void main(String[] args) throws Exception {

        String httpMethod = "GET"; // HTTP请求方法

        String host = "api.example.com"; // 服务端域名

        String uri = "/path"; // 资源路径

        String queryParams = "param1=value1&param2=value2"; // 查询参数

        String now = System.currentTimeMillis() / 1000; // 当前时间戳

        // 构造待签名字符串

        StringBuilder canonicalRequest = new StringBuilder();

        canonicalRequest.append(httpMethod).append("\n");

        canonicalRequest.append(uri).append("\n");

        canonicalRequest.append(queryParams).append("\n");

        canonicalRequest.append("host:").append(host).append("\n");

        canonicalRequest.append("x-sdk-date:").append(now);

        // 生成签名

        String signature = sign(canonicalRequest.toString(), SECRET_ACCESS_KEY);

        // 构造请求头

        String authorizationHeader = "hmac id=\"" + ACCESS_KEY_ID + "\","

                + " algorithm=\"hmac-sha1\","

                + " headers=\"host x-sdk-date request-line-method request-uri\","

                + " signature=\"" + signature + "\"";

        // 发送API请求(示例,实际中使用HTTP客户端发送请求)

        System.out.println("Authorization: " + authorizationHeader);

    }

    // 使用HMAC-SHA1算法生成签名

    public static String sign(String data, String key) throws NoSuchAlgorithmException {

        // 使用给定的密钥创建签名

        byte[] keyBytes = key.getBytes();

        byte[] dataBytes = data.getBytes();

        MessageDigest md = MessageDigest.getInstance("HmacSHA1");

        md.update(keyBytes);

        byte[] hash = md.digest(dataBytes);

        // 将字节数组转换为Base64字符串

        return Base64.getEncoder().encodeToString(hash);

    }

}

这个示例代码展示了如何生成一个简单的签名。在实际应用中,签名的生成可能会更复杂,包括对请求的多个部分进行签名,并且可能需要遵循特定的签名规范。请根据你所使用的服务提供商的API文档来调整签名生成逻辑。

请注意,这个示例仅用于演示目的,实际应用中需要考虑安全性、错误处理和性能优化等因素。

方案三、 AES + RSA

  1. 在上述基础上每一个appId / AK / SK 具备自身的RSA/AES 秘钥,单独提供。

  2. 在请求时对敏感数据进行AES加密,然后通过RSA类似AK/SK的方式进行签名。

  3. 服务端收到数据后先根据入参查询RSA/AES秘钥,然后进行验签,验签完毕后进行解密,完成交互。

  4. 这里如果返回也涉及敏感数据,一般这个动作是对称的,即互相提供公钥,各自保存私钥。

    /**
     * AES加密方式生成密钥
     */
    public static String geneKey() throws Exception {
        //获取一个密钥生成器实例
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(128);
        SecretKey secretKey = keyGenerator.generateKey();
        String key = Base64.getEncoder().encodeToString(secretKey.getEncoded());
        return key;
    }

    /**
     * 生成pk/sk
     */
    public static Map<String, String> genKeyPair() throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        String privateKeyString = Base64.getEncoder().encodeToString(privateKey.getEncoded());
        String publicKeyString = Base64.getEncoder().encodeToString(publicKey.getEncoded());
        Map<String, String> map = new HashMap<>();
        map.put("pk", publicKeyString);
        map.put("sk", privateKeyString);
        return map;
    }

扩展场景:如何在一个没有鉴权的系统上快速增加鉴权

  1. Spring项目可以使用Spring Security ①username password 固定形式,集成简单。

  2. nginx 拦截 :WEB页面没有认证界面使用Nginx认证的两种方式