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. 请求签名验证(进阶安全版)
单纯的自定义头容易被抓包复用,签名验证能解决这个问题。核心思路是:客户端用请求参数+时间戳+随机数+密钥生成唯一签名,服务端用同样规则计算签名并对比,同时通过时间戳防止重放攻击。
实现思路:
- 客户端生成当前时间戳
timestamp、随机数nonce; - 把请求参数按字典序排序后,和
timestamp、nonce拼接,用HMAC-SHA256算法结合密钥生成签名sign; - 请求时携带
timestamp、nonce、sign三个参数; - 服务端先检查时间戳是否在有效期内(比如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实现
- 在
conf/server.xml的<Host>标签下添加:<Valve className="org.apache.catalina.valves.rewrite.RewriteValve" /> - 在
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_id和client_secret,然后获取access_token,每次请求JAX-RS服务时携带这个token,服务端验证token的有效性和所属客户端。
核心流程:
- 移动应用调用授权服务器的
/token接口,携带client_id、client_secret、grant_type=client_credentials; - 授权服务器验证后返回
access_token; - 移动应用在请求JAX-RS接口时,添加请求头
Authorization: Bearer {access_token}; - JAX-RS服务端通过过滤器验证token有效性,确认是指定客户端的token后允许访问。
这个方案需要额外搭建授权服务(比如Keycloak、Spring Security OAuth2),适合大型项目。
最后提醒
- 永远不要仅依赖
User-Agent验证客户端,这个字段太容易伪造; - 敏感密钥、凭证要避免硬编码:客户端用混淆+动态加载,服务端用配置文件或环境变量;
- 必须启用HTTPS,防止请求内容被拦截篡改。
内容的提问来源于stack exchange,提问作者ABMi




