核心概念与基础

在开始编码之前,你必须理解微信网页开发的几个核心概念和限制。

java web 微信网页开发
(图片来源网络,侵删)

微信 JS-SDK

这是整个微信网页开发的核心,它是一组由微信提供的、基于微信内的网页开发工具包,通过使用 JS-SDK,网页开发者可以借助微信的能力,方便地使用微信原生的一些功能,如:

  • 分享:将网页分享给好友或朋友圈。
  • 扫一扫:调用微信扫一扫功能。
  • 地理位置:获取用户地理位置。
  • 图像/音频/视频:处理媒体文件。
  • 界面操作:如关闭当前网页、打开小程序等。

关键限制: JS-SDK 的使用有严格的域名白名单限制,你只能在微信后台配置好的、且经过认证的域名下才能调用这些接口。

OAuth2.0 网页授权

这是微信网页开发中最关键、也最容易让人困惑的一步,由于微信环境是封闭的,网页无法直接获取用户的 openid(用户在公众号下的唯一标识)和 access_token

网页授权流程分为两种模式:

java web 微信网页开发
(图片来源网络,侵删)
  • snsapi_base (静默授权)

    • 特点:用户无感知,静默完成授权。
    • 获取信息:只能获取用户的 openid
    • 应用场景:不需要获取用户基本信息,只需要知道是谁在访问即可,个性化推荐、记录用户行为。
  • snsapi_userinfo (手动授权)

    • 特点:会弹出一个授权页面,用户点击“同意”后才能获取信息。
    • 获取信息:可以获取 openidnickname(昵称)、headimgurl(头像)、sex(性别)等用户基本信息。
    • 应用场景:需要展示用户头像、昵称,或进行与用户身份强相关的操作。

授权流程图解(以 snsapi_userinfo 为例):

用户访问你的网页
    |
    v
你的服务器重定向到微信授权URL (带上你的appid和回调地址)
    |
    v
用户在微信内点击“同意授权”
    |
    v
微信重定向到你配置的回调地址 (带上 code 参数)
    |
    v
你的服务器拿着 code 去向微信服务器请求 access_token 和 openid
    |
    v
微信服务器返回 access_token 和 openid
    |
    v
(如果是snsapi_userinfo) 用 access_token 去请求用户信息
    |
    v
获取到用户信息,你的服务器可以创建或更新用户账户,然后重定向到原始网页或执行其他操作

微信支付 (H5 支付)

如果你的网页需要集成微信支付,流程比网页授权更复杂:

java web 微信网页开发
(图片来源网络,侵删)
  1. 用户在网页点击支付。
  2. 你的后台服务器生成一个支付订单,调用微信的统一下单接口 (wxpay.unifiedorder)。
  3. 微信返回一个 prepay_id(预支付交易会话标识)。
  4. 你的后台服务器将 appId, timeStamp, nonceStr, package, signType 等参数签名后,返回给前端。
  5. 前端调用微信 JS-SDK 的 chooseWXPay 方法,弹出微信支付收银台。

Java Web 开发环境搭建

你需要一个标准的 Java Web 开发环境。

  • JDK: 1.8 或更高版本。
  • 构建工具: Maven 或 Gradle (推荐 Maven,社区支持更广)。
  • Web 容器: Tomcat, Jetty 等。
  • IDE: IntelliJ IDEA 或 Eclipse。
  • Spring Boot: 强烈推荐使用 Spring Boot,它能极大地简化配置,让你专注于业务逻辑,而不是 XML 配置文件。

开发步骤详解 (以 Spring Boot 为例)

我们将以一个最常见的场景为例:用户访问网页 -> 静默授权获取 openid -> 在页面上显示欢迎信息

第 1 步:微信公众平台配置

  1. 登录微信公众平台https://mp.weixin.qq.com/
  2. 获取 AppID 和 AppSecret:在“开发” -> “基本配置”中找到。
  3. 配置网页授权域名
    • 进入“设置” -> “公众号设置” -> “功能设置”。
    • 在“网页授权域名”一栏,填写你的域名(www.yourdomain.com),注意,这里不需要加 http://https://,也不需要加路径
    • 域名需要先进行备案,并且可以通过该域名访问一个验证文件。

第 2 步:后端项目搭建 (Spring Boot)

  1. 创建一个新的 Spring Boot 项目。
  2. 添加必要的依赖 (pom.xml):
<dependencies>
    <!-- Spring Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Lombok (简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <!-- HttpClient (用于调用微信API) -->
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.13</version>
    </dependency>
    <!-- FastJSON (处理JSON) -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.83</version>
    </dependency>
</dependencies>

第 3 步:配置文件 (application.yml)

将你的 AppID 和 AppSecret 配置到文件中。

server:
  port: 8080
wechat:
  app-id: "你的AppID"
  app-secret: "你的AppSecret"
  # 回调地址,必须和公众号配置的域名一致
  redirect-uri: "http://www.yourdomain.com/wechat/callback"
  # snsapi_base 或 snsapi_userinfo
  scope: "snsapi_base"

第 4 步:编写后端 Controller

这是核心逻辑所在,处理授权流程。

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@Controller
public class WeChatController {
    @Value("${wechat.app-id}")
    private String appId;
    @Value("${wechat.app-secret}")
    private String appSecret;
    @Value("${wechat.redirect-uri}")
    private String redirectUri;
    @Value("${wechat.scope}")
    private String scope;
    @Autowired
    private WeChatService weChatService; // 我们将业务逻辑抽离到Service层
    /**
     * 1. 用户访问的入口
     * 重定向到微信授权URL
     */
    @GetMapping("/wechat/login")
    public String wechatLogin(HttpSession session) throws UnsupportedEncodingException {
        // 对回调地址进行URL编码
        String encodedRedirectUri = URLEncoder.encode(redirectUri, "UTF-8");
        // 构造微信授权URL
        String authUrl = String.format(
            "https://open.weixin.qq.com/connect/oauth2/authorize?" +
            "appid=%s" +
            "&redirect_uri=%s" +
            "&response_type=code" +
            "&scope=%s" +
            "&state=STATE#wechat_redirect",
            appId, encodedRedirectUri, scope
        );
        // 重定向到微信
        return "redirect:" + authUrl;
    }
    /**
     * 2. 微信授权后的回调地址
     * 微信会带着 code 参数访问这个地址
     */
    @GetMapping("/wechat/callback")
    public String callback(@RequestParam("code") String code, HttpSession session) {
        // 使用 code 去换取 access_token 和 openid
        JSONObject userInfo = weChatService.getOpenidAndUserInfo(code, scope);
        if (userInfo != null) {
            String openid = userInfo.getString("openid");
            // 将 openid 存入 session,后续请求可以用来识别用户
            session.setAttribute("openid", openid);
            // 如果是snsapi_userinfo,这里已经拿到了用户信息
            String nickname = userInfo.getString("nickname");
            System.out.println("用户昵称: " + nickname);
        }
        // 获取 openid 后,可以重定向到你的业务主页
        return "redirect:/index";
    }
    /**
     * 3. 业务主页
     * 从 session 中获取 openid,并展示给用户
     */
    @GetMapping("/index")
    @ResponseBody
    public String index(HttpSession session) {
        String openid = (String) session.getAttribute("openid");
        if (openid != null) {
            return "欢迎回来!您的 openid 是: " + openid;
        } else {
            return "未授权,请先<a href='/wechat/login'>点击登录</a>";
        }
    }
}

第 5 步:编写 Service 层

将调用微信 API 的逻辑封装起来。

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.stereotype.Service;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
@Service
public class WeChatService {
    public JSONObject getOpenidAndUserInfo(String code, String scope) {
        // 1. 通过 code 获取 access_token 和 openid
        String tokenUrl = String.format(
            "https://api.weixin.qq.com/sns/oauth2/access_token?" +
            "appid=%s" +
            "&secret=%s" +
            "&code=%s" +
            "&grant_type=authorization_code",
            "你的AppID", "你的AppSecret", code
        );
        JSONObject tokenInfo = sendGetRequest(tokenUrl);
        if (tokenInfo == null || !tokenInfo.containsKey("access_token")) {
            return null; // 获取失败
        }
        String accessToken = tokenInfo.getString("access_token");
        String openid = tokenInfo.getString("openid");
        // 2. 如果是 snsapi_userinfo,则进一步获取用户信息
        if ("snsapi_userinfo".equals(scope)) {
            String userInfoUrl = String.format(
                "https://api.weixin.qq.com/sns/userinfo?" +
                "access_token=%s" +
                "&openid=%s",
                accessToken, openid
            );
            return sendGetRequest(userInfoUrl);
        }
        // 如果是 snsapi_base,只返回包含 openid 的对象
        JSONObject result = new JSONObject();
        result.put("openid", openid);
        return result;
    }
    private JSONObject sendGetRequest(String url) {
        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            HttpGet httpGet = new HttpGet(url);
            HttpResponse response = httpClient.execute(httpGet);
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                String result = EntityUtils.toString(entity, "UTF-8");
                return JSON.parseObject(result);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

第 6 步:前端页面

在你的 src/main/resources/static 目录下创建 index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">微信网页开发示例</title>
</head>
<body>
    <h1>欢迎来到我的网站</h1>
    <p id="user-info">正在加载...</p>
    <script>
        // 页面加载时,从后端获取用户信息
        window.onload = function() {
            fetch('/index')
                .then(response => response.text())
                .then(data => {
                    document.getElementById('user-info').innerText = data;
                })
                .catch(error => {
                    console.error('Error:', error);
                    document.getElementById('user-info').innerText = '加载失败,请刷新页面重试。';
                });
        };
    </script>
</body>
</html>

第 7 步:部署与测试

  1. 将你的 Spring Boot 应用打包成 JAR 或 WAR 文件。
  2. 部署到服务器上,确保可以通过 http://www.yourdomain.com:8080 访问。
  3. 注意:微信只允许 httphttps 协议,且端口必须是 80 或 443,如果你在本地开发(端口8080),可以使用 内网穿透工具(如 frp, ngrok)将本地端口映射到公网域名。
  4. 在手机微信中访问 http://www.yourdomain.com/wechat/login,测试整个流程。

JS-SDK 集成示例(分享功能)

假设你已经通过 OAuth2.0 获取了用户的 access_tokenopenid,现在要实现网页分享功能。

后端生成签名

JS-SDK 的调用需要签名,签名规则比较复杂,后端需要生成 signature, timestamp, noncestr

// 在你的 Controller 或 Service 中添加一个方法
@GetMapping("/jsapi/signature")
@ResponseBody
public JSONObject getJsapiSignature() {
    // 1. 获取 access_token (通过你的公众号的AppID和Secret,不是网页授权的)
    String accessToken = weChatService.getOfficialAccountAccessToken();
    if (accessToken == null) {
        return null;
    }
    // 2. 获取 jsapi_ticket
    String ticket = weChatService.getJsapiTicket(accessToken);
    if (ticket == null) {
        return null;
    }
    // 3. 生成签名
    String url = "http://www.yourdomain.com/current-page-url"; // 当前页面的完整URL
    String nonceStr = UUID.randomUUID().toString().replace("-", "");
    long timestamp = System.currentTimeMillis() / 1000;
    String string1 = "jsapi_ticket=" + ticket +
                     "&noncestr=" + nonceStr +
                     "&timestamp=" + timestamp +
                     "&url=" + url;
    String signature = SHA1Util.sha1(string1);
    // 4. 返回给前端
    JSONObject result = new JSONObject();
    result.put("appId", "你的AppID");
    result.put("timestamp", timestamp);
    result.put("nonceStr", nonceStr);
    result.put("signature", signature);
    return result;
}

(你需要自己实现 getOfficialAccountAccessTokengetJsapiTicket 方法,并缓存它们,因为它们有有效期)

前端调用 JS-SDK

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">分享示例</title>
    <!-- 引入微信JS-SDK -->
    <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
</head>
<body>
    <h1>这是一个可以分享的页面</h1>
    <script>
        // 页面加载时,从后端获取签名配置
        window.onload = function() {
            fetch('/jsapi/signature')
                .then(response => response.json())
                .then(config => {
                    if (config) {
                        // 通过config接口注入权限验证配置
                        wx.config({
                            beta: true,
                            debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
                            appId: config.appId,
                            timestamp: config.timestamp,
                            nonceStr: config.nonceStr,
                            signature: config.signature,
                            jsApiList: [
                                'updateAppMessageShareData', // 分享给朋友
                                'updateTimelineShareData'   // 分享到朋友圈
                            ] // 必填,需要使用的JS接口列表
                        });
                        wx.ready(function () {
                            // 分享给朋友
                            wx.updateAppMessageShareData({
                                title: '分享给朋友的标题', // 分享标题
                                desc: '分享给朋友的描述', // 分享描述
                                link: 'http://www.yourdomain.com/share-page', // 分享链接,该链接域名或路径必须在当前公众号JSAPI安全域名范围内
                                imgUrl: 'http://www.yourdomain.com/images/share-icon.png', // 分享图标
                                success: function () {
                                    // 设置成功
                                }
                            });
                            // 分享到朋友圈
                            wx.updateTimelineShareData({
                                title: '分享到朋友圈的标题', // 分享标题
                                link: 'http://www.yourdomain.com/share-page', // 分享链接,该链接域名或路径必须在当前公众号JSAPI安全域名范围内
                                imgUrl: 'http://www.yourdomain.com/images/share-icon.png', // 分享图标
                                success: function () {
                                    // 设置成功
                                }
                            });
                        });
                        wx.error(function (res) {
                            alert('配置失败: ' + res.errMsg);
                        });
                    }
                });
        };
    </script>
</body>
</html>

常见问题与最佳实践

  1. 域名与端口:务必使用微信配置的域名,并且端口是 80 或 443,本地开发务必使用内网穿透。
  2. 缓存access_tokenjsapi_ticket 都有有效期(7200秒),一定要缓存起来,避免频繁请求微信API,否则可能会被限制。
  3. 用户状态管理openid 是识别用户的关键,通常在用户首次授权时,根据 openid 在你的数据库中创建或查找用户记录,并生成你自己的 user_id,后续请求通过 sessionJWT 来保持登录状态。
  4. 安全性:注意保护你的 AppSecret,不要泄露在前端代码中,所有与微信服务器的敏感交互都应该在后端完成。
  5. 错误处理:微信API调用可能会失败,要有完善的错误处理和日志记录机制。
  6. 测试:多在真机上测试,微信在 iOS 和 Android 上的表现可能略有不同。

希望这份详细的指南能帮助你顺利开启 Java Web 微信网页开发之旅!