安卓端微信H5下载文件处理:让微信自动弹起跳转外部浏览器窗口

配套视频:https://www.bilibili.com/video/BV1oA411B7gv/


背景

今天鼓捣了一下手机投屏到笔记本,就想录个视频展示一下学习成果,正好就想起了很早之前实现的这个功能。
H5文件下载是一个很简单的功能,但是把这个H5放在安卓版微信打开,功能就不能用了,因为安卓端的微信内置浏览器拦截了所有下载文件的请求。
即使微信的sdk也没有提供直接保存文件的接口,所以出路只有一条,就是跳到第三方应用进行下载,比如跳到手机浏览器、跳到微信小程序。如果是上架了应用宝的app,可以跳转应用宝下载。
之所以屏蔽,应该是H5无法监管的原因,但是不能理解的是,ios端的微信是可以下载的,难道苹果手机高人一等?

解决方案收集

最终选择的解决方案

  • 想到这个方案,是一个意外。

  • 一开始我只测试了zip的下载,确实不能下载,以至于我以偏概全地以为所有格式都不能下载,所以就转到百度上找答案。

    image-20230211165914298
  • 然后测试跟我说,ios的文件有些也不能预览,不能下载。

  • 所以我就丢下这个坑,先去解决ios的问题。

  • 百度发现ios也是伪下载,它是先以预览的方式打开文件,需要用户点击右上角手动保存。

  • 而且文件后缀和响应头 content-type要严格对应,不对应就会报错,预览不了

  • 参考:解决移动端H5下载文件提示文件类型无法识别或非法文件的问题

  • 改完ios的问题,我传了各种格式的文件测试了一遍,确认修复之后,又转回安卓端。

  • 随手点击了几下,就是这么几下让我看到了希望。

    image-20230211170009537
  • 并不是所有类型的文件都不能下载,针对docx、pdf、xlsx、txt等格式,微信会主动唤起跳转其他浏览器的选择弹窗。

  • 这比起前端写提示窗无疑要友好许多。

  • 所以只要发挥偷蒙拐骗的优良品质,让微信对所有文件一视同仁,都唤起跳转窗口就行了。

  • 到此,安卓端H5下载文件的问题完美解决。

  • 欺骗的手段也很简单,反正微信也不能下载,就所有的下载请求,都给它一个假文件,比如123456.xlsx。

java实现

  • 注意,如果接口使用cookie鉴权,跳转外部浏览器,cookie是带不过去的。
  • 需要提供一个不需要鉴权的接口,换一种方式鉴权,比如时效分享码或者直接携带sessionId之类的
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
public class ApiController {
    
    

    // 获取日志对象 Spring Boot 中内置了日志框架 Slf4j
    private static Logger log = LoggerFactory.getLogger(ApiController.class);

    /**
     * 处理微信文件下载
     * 欺骗安卓微信唤起打开外部浏览器的选择框
     * ios微信可以预览每种格式的文件,但是不支持直接下载,需要用户在预览页点右上角手动保存
     * 另外,ios对content_type要求严格,如果文件后缀和content_type对不上,连预览页都进不了
     * 企业微信不用做任何处理
     */
    @GetMapping("downloadFileWx")
    public void downloadFileWx(@RequestParam String path, HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    
        responseOutputFileWx(path, null, request, response);
    }

    /**
     * 响应文件流
     * @param path 文件路径
     * @param outputFileName 文件名称,赋值给响应头Content-Disposition
     * @param request
     * @param response
     */
    public void responseOutputFileWx(String path, String outputFileName,
                                   HttpServletRequest request, HttpServletResponse response)
            throws Exception {
    
    
        File file = new File(path);
        if (file == null || !file.exists() || !file.isFile()) {
    
    
            log.error("文件不存在");
            // 重定向到当前页面,相当于刷新页面
            String contextPath = request.getContextPath();
            response.sendRedirect(contextPath + "/downFile");
            return;
        }

        if (outputFileName == null || outputFileName.trim().length() == 0) {
    
    
            // 假如下载文件名参数为空,则设置为原始文件名
            outputFileName = file.getName();
        }
        ServletContext context = request.getServletContext();
        // 文件绝对路径
        String absolutePath = file.getAbsolutePath();
        // 获取文件的MIME type
        String mimeType = context.getMimeType(absolutePath);
        if (mimeType == null) {
    
    
            // 没有发现则设为二进制流
            mimeType = "application/octet-stream";
        }

        response.setContentType(mimeType);
        // 设置文件下载响应头
        String headerKey = "Content-Disposition";
        String headerValue = null;

        if (isWx(request)) {
    
    
            // 微信浏览器,打开手机默认浏览器下载文件
            // 注意排除企业微信
            try {
    
    
                if (isAndroidWx(request)) {
    
    
                    // 安卓端,xlsx文件类型会触发微信弹出跳转外部浏览器窗口,欺骗一下
                    response.setContentType("application/octet-stream");
                    outputFileName = "123456.xlsx";
                } else {
    
    
                    // ios 微信对contentType要求比较严格
                    // https://juejin.cn/post/6844904086463053837
                    if (absolutePath.endsWith("xlsx")) {
    
    
                        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
                    } else if (absolutePath.endsWith("xls")) {
    
    
                        response.setContentType("application/vnd.ms-excel");
                    } else if (absolutePath.endsWith("doc")) {
    
    
                        response.setContentType("application/msword");
                    } else if (absolutePath.endsWith("docx")) {
    
    
                        response.setContentType("application/application/vnd.openxmlformats-officedocument.wordprocessingml.document");
                    }
                }
                headerValue = String.format("attachment; filename=\"%s\"", URLEncoder.encode(outputFileName, "UTF-8"));
            } catch (Exception e) {
    
    
                headerValue = String.format("attachment; filename=\"%s\"", outputFileName);
                log.error(e.getMessage(), e);
            }
        } else {
    
    
            try {
    
    
                // 解决Firefox浏览器中文件名中文乱码
                // https://blog.csdn.net/Jon_Smoke/article/details/53699400
                headerValue = String.format("attachment; filename* = UTF-8''%s",
                        URLEncoder.encode(outputFileName, "UTF-8")
                );
            } catch (Exception e) {
    
    
                headerValue = String.format("attachment; filename=\"%s\"", outputFileName);
                log.error(e.getMessage(), e);
            }
        }
        response.setHeader(headerKey, headerValue);

        String fileName = file.getName();
        try (OutputStream outputStream = response.getOutputStream()) {
    
    
            response.setCharacterEncoding("utf-8");

            // 将下面2行放开,可以测试微信最原始反应
            // 设置返回类型
            // response.setContentType("multipart/form-data");
            // // 文件名转码一下,不然会出现中文乱码
            // response.setHeader("Content-Disposition", "attachment;fileName=" + encodeStr(fileName));

            byte[] bytes = readBytes(file);
            if (bytes == null) {
    
    
                log.error("文件不存在");
            }
            outputStream.write(bytes);
            log.info("文件下载成功!" + fileName);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

    /**
     * 对字符串(文件名或路径)进行url编码
     */
    private String encodeStr(String str) throws Exception {
    
    
        return URLEncoder.encode(str, "UTF-8");
    }

    /**
     * 将文件转为byte数组
     */
    public byte[] readBytes(File file) throws Exception {
    
    
        long len = file.length();
        // 无论数组的类型如何,数组中的最大元素数为Integer.MAX_VALUE,大约20亿
        if (len >= 2147483647L) {
    
    
            return null;
        } else {
    
    
            byte[] bytes = new byte[(int) len];

            try (FileInputStream in = new FileInputStream(file)) {
    
    
                int readLength = in.read(bytes);
                if ((long) readLength < len) {
    
    
                    log.error("文件未读取完全");
                    return null;
                }
            } catch (Exception var10) {
    
    
                return null;
            }

            return bytes;
        }
    }

    /**
     * 是否从安卓端微信请求,需要排除企业微信
     */
    private static boolean isAndroidWx(HttpServletRequest request) {
    
    
        String userAgent = request.getHeader("user-agent");
        return userAgent != null && userAgent.toLowerCase().indexOf("micromessenger") > -1
                && userAgent.toLowerCase().indexOf("wxwork") < 0
                && userAgent.toLowerCase().indexOf("android") > -1;
    }

    /**
     * 是否从微信请求,需要排除企业微信
     * 安卓或ios
     */
    private static boolean isWx(HttpServletRequest request) {
    
    
        String userAgent = request.getHeader("user-agent");
        return userAgent != null && userAgent.toLowerCase().indexOf("micromessenger") > -1
                && userAgent.toLowerCase().indexOf("wxwork") < 0;
    }

}

题外话:手机如何投屏笔记本

方式1:win10自带投屏

  • 按 “Windows 徽标键+I” 打开设置,设置–>系统–>投影到此电脑

  • 投影到此电脑中显示灰色不可选,或显示“我们正在确认这项功能”

  • 第一次需要安装 无线显示器

  • 手机使用电脑自带功能进行投屏

  • 手机投屏到笔记本之后,笔记本会被劫持,就是只能操作手机画面,鼠标移不出来,可以在电脑上用鼠标直接操作手机。

  • 这一点,有点不方便,比如想一边写代码,一边预览手机效果,就不能实现。

  • 另外,建议选择 仅第一次 需要验证,我第一次投成功了,关闭之后,就死活投不上去,主要是笔记本不能弹出确认对话框

  • 之后,重启电脑才能第二次投屏成功。

方式2:幕享 软件

猜你喜欢

转载自blog.csdn.net/weixin_44174211/article/details/128985936