You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Tomcat部署JAX-RS应用:如何仅允许指定移动应用访问WebService

限制Tomcat上JAX-RS服务仅允许指定移动应用访问的方案

嘿,这个需求在移动端对接后端服务时特别常见,我来给你分享几个实用且逐步递进的方案,从简单入门到进阶安全,你可以根据自己的项目规模和安全要求选择:

1. 自定义请求头验证(入门级)

这是最直接的实现方式——让你的移动应用在每次请求JAX-RS接口时,携带一个独有的自定义请求头,后端服务先验证这个头的内容是否匹配预设的密钥,不匹配就直接拒绝访问。

实现步骤:

  • 移动端配置:每次发起HTTP请求时添加自定义头,比如Android用OkHttp的示例:
    Request request = new Request.Builder()
        .url("your-jaxrs-api-endpoint")
        .addHeader("X-App-Secret", "your-unique-secret-key-123")
        .build();
    
  • JAX-RS过滤器实现:编写一个全局请求过滤器,拦截所有请求并验证头信息:
    @Provider
    @Priority(Priorities.AUTHENTICATION)
    public class AppAuthFilter implements ContainerRequestFilter {
        // 建议从配置文件读取密钥,不要硬编码
        private static final String VALID_SECRET = System.getProperty("mobile.app.secret");
        private static final String SECRET_HEADER = "X-App-Secret";
    
        @Override
        public void filter(ContainerRequestContext requestContext) throws IOException {
            String receivedSecret = requestContext.getHeaderString(SECRET_HEADER);
            
            // 验证失败则返回403禁止访问
            if (receivedSecret == null || !VALID_SECRET.equals(receivedSecret)) {
                requestContext.abortWith(Response.status(Response.Status.FORBIDDEN)
                        .entity("仅允许指定移动应用访问")
                        .build());
            }
        }
    }
    
  • 注册过滤器:在你的JAX-RS应用类中添加过滤器注册:
    @ApplicationPath("/api")
    public class JaxrsApplication extends Application {
        @Override
        public Set<Class<?>> getClasses() {
            Set<Class<?>> classes = new HashSet<>();
            classes.add(AppAuthFilter.class);
            // 加上你的其他资源类
            return classes;
        }
    }
    

注意点:

  • 移动端的密钥一定要做代码混淆,避免反编译后被轻易获取;
  • 必须配合HTTPS使用,防止请求头被中间人劫持。

2. 请求签名验证(进阶安全版)

单纯的自定义头容易被抓包复用,签名验证能解决这个问题。核心思路是:客户端用请求参数+时间戳+随机数+密钥生成唯一签名,服务端用同样规则计算签名并对比,同时通过时间戳防止重放攻击。

实现思路:

  1. 客户端生成当前时间戳timestamp、随机数nonce
  2. 把请求参数按字典序排序后,和timestampnonce拼接,用HMAC-SHA256算法结合密钥生成签名sign
  3. 请求时携带timestampnoncesign三个参数;
  4. 服务端先检查时间戳是否在有效期内(比如5分钟),再重新计算签名并对比。

服务端核心代码示例:

@Provider
@Priority(Priorities.AUTHENTICATION)
public class SignAuthFilter implements ContainerRequestFilter {
    private static final String APP_SECRET = System.getProperty("mobile.app.secret");
    private static final long VALID_TIME_WINDOW = 5 * 60 * 1000; // 5分钟有效期

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        MultivaluedMap<String, String> queryParams = requestContext.getUriInfo().getQueryParameters();
        String timestamp = queryParams.getFirst("timestamp");
        String nonce = queryParams.getFirst("nonce");
        String receivedSign = queryParams.getFirst("sign");

        // 参数不全直接拒绝
        if (timestamp == null || nonce == null || receivedSign == null) {
            abortRequest(requestContext);
            return;
        }

        // 验证时间戳是否过期
        try {
            long reqTime = Long.parseLong(timestamp);
            if (Math.abs(System.currentTimeMillis() - reqTime) > VALID_TIME_WINDOW) {
                abortRequest(requestContext);
                return;
            }
        } catch (NumberFormatException e) {
            abortRequest(requestContext);
            return;
        }

        // 生成服务端签名并对比
        String sortedParams = sortParams(queryParams);
        String serverSign = HmacSha256Util.generate(sortedParams + APP_SECRET, APP_SECRET);
        if (!serverSign.equals(receivedSign)) {
            abortRequest(requestContext);
        }
    }

    private void abortRequest(ContainerRequestContext requestContext) {
        requestContext.abortWith(Response.status(Response.Status.FORBIDDEN)
                .entity("签名验证失败")
                .build());
    }

    // 按字典序排序参数的工具方法
    private String sortParams(MultivaluedMap<String, String> params) {
        List<String> paramKeys = new ArrayList<>(params.keySet());
        Collections.sort(paramKeys);
        StringBuilder sb = new StringBuilder();
        for (String key : paramKeys) {
            if (!"sign".equals(key)) { // 排除签名参数本身
                sb.append(key).append("=").append(params.getFirst(key)).append("&");
            }
        }
        return sb.length() > 0 ? sb.substring(0, sb.length() - 1) : "";
    }
}

// HMAC-SHA256工具类
class HmacSha256Util {
    public static String generate(String data, String key) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKeySpec);
            byte[] bytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return Hex.encodeHexString(bytes);
        } catch (Exception e) {
            throw new RuntimeException("生成签名失败", e);
        }
    }
}

3. Tomcat层面拦截(无需修改业务代码)

如果不想在JAX-RS代码里做改动,也可以直接在Tomcat配置中添加拦截规则,比如利用RewriteValve或者自定义Valve来检查请求头。

示例:用RewriteValve实现

  1. conf/server.xml<Host>标签下添加:
    <Valve className="org.apache.catalina.valves.rewrite.RewriteValve" />
    
  2. conf/Catalina/localhost/rewrite.config中添加规则:
    # 检查自定义头X-App-Secret是否匹配,不匹配则返回403
    RewriteCond %{HTTP:X-App-Secret} !^your-unique-secret-key-123$
    RewriteRule ^/api/(.*)$ - [F]
    

这个方式适合简单场景,但灵活性不如代码层面的过滤器。

4. OAuth2.0客户端凭证模式(企业级方案)

如果你的项目架构复杂,涉及多客户端、权限细分,可以采用OAuth2.0的客户端凭证模式。移动应用先向授权服务器申请client_idclient_secret,然后获取access_token,每次请求JAX-RS服务时携带这个token,服务端验证token的有效性和所属客户端。

核心流程:

  1. 移动应用调用授权服务器的/token接口,携带client_idclient_secretgrant_type=client_credentials
  2. 授权服务器验证后返回access_token
  3. 移动应用在请求JAX-RS接口时,添加请求头Authorization: Bearer {access_token}
  4. JAX-RS服务端通过过滤器验证token有效性,确认是指定客户端的token后允许访问。

这个方案需要额外搭建授权服务(比如Keycloak、Spring Security OAuth2),适合大型项目。

最后提醒

  • 永远不要仅依赖User-Agent验证客户端,这个字段太容易伪造;
  • 敏感密钥、凭证要避免硬编码:客户端用混淆+动态加载,服务端用配置文件或环境变量;
  • 必须启用HTTPS,防止请求内容被拦截篡改。

内容的提问来源于stack exchange,提问作者ABMi

火山引擎 最新活动