尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

微信登录后端接入全攻略:HttpClient配置与前后端分离实战

微信登录后端接入全攻略:HttpClient配置与前后端分离实战 最近整理后端学习笔记把 HttpClient 和微信登录这两个点拎出来重写了一遍。单看这两个词都不算难但放在同一个项目里尤其是前后端分离的场景下几乎是每个团队都会踩到坑的地方。这篇笔记不是官方文档的复述是我在实际项目里调试过几十次之后梳理出来的完整链路从授权流程、工具配置、参数计算到常见报错排查一次性说清楚。这篇内容适合正在做后端接口对接的同学也适合把微信登录从零接进 Spring Boot / Vue 项目的朋友。不管你用的是 Java、PHP 还是其他后端语言HttpClient 的调优思路和微信登录的流程设计都是通用的。我会尽量把“为什么要这么做”讲清楚而不是只丢给你一段能跑的代码。1. 微信登录很多人理解反了先说结论微信登录这件事主战场在后端不在前端。前端只是负责把用户引导到微信的授权页再拿回一个临时凭证 code真正和微信服务器打交道、换 token、拉用户信息的活全部由后端完成。1.1 前端直接调微信接口是行不通的微信开放平台的接口文档里写得很清楚获取 access_token 的接口需要用到 AppSecret。这个 AppSecret 一旦暴露在前端代码里就等于把你的应用钥匙贴在了大街上。我之前见过一个项目前端把 AppSecret 硬编码在 JS 文件里被爬虫扫到之后别人直接拿着这个密钥调接口、发消息、读用户信息最后只能紧急重置密钥并全量排查日志。所以微信登录的第一步必须想明白code 可以给前端access_token 绝对不能给前端。前端拿到 code 之后把它交给后端由后端去请求微信的接口完成后续流程。这也是很多新手最容易犯的错误——直接把敏感接口放在浏览器端调用然后发现跨域、被拦截、密钥泄露一坑接一坑。1.2 后端承接的核心职责后端在微信登录流程里干三件事接收前端传来的 code用 code appid secret 换取 access_token使用 access_token 拉取微信用户信息openid、unionid、昵称、头像等根据用户信息完成注册/登录逻辑生成自己的登录态比如 JWT token返回给前端第二点要特别说明微信的网页授权域名限制的是第一步前端重定向的域名并不限制后端的 API 调用。这是前后端分离项目里特别容易混淆的地方。你的前端可能部署在web.example.com后端在api.example.com微信开放平台那里只需要配置前端页面的域名就行后端拿着 code 去请求https://api.weixin.qq.com/sns/oauth2/access_token是不受这个域名约束的。但有个前提就是传给微信的 redirect_uri 里面的域名必须和开放平台配置的授权回调域名一致否则会直接报 redirect_uri 错误。2. HttpClient为什么选它配置得讲究HttpClient 是 Apache 出的一个 HTTP 客户端库Java 生态里用它调第三方接口非常主流。有人会问现在 Java 自带的java.net.http.HttpClient不也挺好吗为什么项目里还是很多人用 Apache HttpClient原因在于生态成熟度连接池管理、重试机制、拦截器、超时控制这些功能Apache HttpClient 的封装更完整踩过坑的人也多网上能找到大量实战案例。如果你用的是 Spring CloudFeign 底层也是可以用 HttpClient 替换默认的 JDK 客户端的。2.1 连接池和超时参数的计算思路这里重点说一下连接池配置。模拟一下场景你的服务部署在腾讯云微信接口在微信的服务器上两者之间网络往返大概 30ms 到 80ms。假设一次微信登录要发 2 个请求换 token 一次、拉用户信息一次单机 QPS 是 100 的话理论上需要的连接数就是 100 × 2 × 0.08 ≈ 16 个。实际生产环境建议把连接池上限设置为这个理论值的 3 到 5 倍。我常用的配置大致是这样的PoolingHttpClientConnectionManager manager new PoolingHttpClientConnectionManager(); manager.setMaxTotal(200); manager.setDefaultMaxPerRoute(50); manager.setValidateAfterInactivity(2000);setMaxTotal(200)连接池里最多同时存在 200 个连接setDefaultMaxPerRoute(50)每个目标主机最多 50 个连接setValidateAfterInactivity(2000)连接空闲超过 2 秒后再次使用前先做一次校验避免拿到失效连接超时参数按三个维度去设连接建立超时、从连接池获取连接的超时、等待响应数据的超时。RequestConfig config RequestConfig.custom() .setConnectTimeout(3000) .setConnectionRequestTimeout(3000) .setSocketTimeout(5000) .build();connectTimeout 设 3 秒是合理的微信接口在国内访问通常 1 秒内就能建立连接超过 3 秒大概率是网络问题或者对方服务异常没必要一直等。socketTimeout 设 5 秒是给自己留了余量也能避免接口异常时请求线程被长时间挂住拖垮整个服务。2.2 重试机制要谨慎网上很多教程教你设置重试次数我建议你要分场景。HttpClient 默认的重试机制是DefaultHttpRequestRetryExecutor它只对幂等请求做重试比如 GET、HEAD。对于 POST 请求如果你没有显式实现HttpRequestRetryHandler一般是不会重试的。但微信登录换取 token 的请求是 POST而且这个请求是幂等的——用同一个 code 重复换 token微信会返回同样的结果。所以在重试策略上可以考虑对特定场景做一次重试。不过要注意获取 access_token 的接口有调用频率限制单日调用上限通常是 2000 次左右重试太激进容易触发频控。我自己一般只做一次重试并且加上退避等待比如第一次失败后等 200ms 再试还失败就放弃并记录日志。HttpRequestRetryHandler retryHandler (exception, executionCount, context) - { if (executionCount 1) { return false; } if (exception instanceof InterruptedIOException) { return false; } if (exception instanceof SSLException) { return false; } if (exception instanceof ConnectTimeoutException) { return true; } return false; };这段重试逻辑只对连接超时做一次重试响应超时和 SSL 错误不重试。目的很明确连接超时通常是网络抖动重试一次大概率能成功而响应超时可能是微信那边处理慢了重试只会加重对方压力。2.3 连接管理的隐藏坑用 HttpClient 调微信接口还容易掉进这几个坑没有做空闲连接清理。连接池里的连接长时间闲置微信服务器会主动断开你下次拿到的就是一条断掉的连接请求直接报错。解决方式是配置一个后台线程定期把空闲超过 30 秒的连接清理掉。Timer timer new Timer(true); timer.schedule(new TimerTask() { Override public void run() { manager.closeExpiredConnections(); manager.closeIdleConnections(30, TimeUnit.SECONDS); } }, 0, 30000);SSL 证书校验问题。微信的接口是 HTTPS正常情况下 HttpClient 用系统默认的证书信任链就能完成校验不需要额外处理。但如果你的服务器是内网环境或者用了一些代理工具可能会出现证书验证失败。这时候不要图省事直接信任所有证书正确的做法是把微信的根证书导入到服务器的 truststore 里。连接不够时报错。报错信息一般是ConnectionPoolTimeoutException: Timeout waiting for connection from pool。这个问题排查思路是先看连接池上限是不是设小了再看请求是不是没有正确释放连接。用了try-with-resources或者finally里调用CloseableHttpResponse.close()连接才会归还到连接池。3. 微信扫码登录后端完整流程下面我直接贴一套我在项目里跑通的流程使用 Java Spring Boot HttpClient 实现。整体分五步。3.1 第一步前端拿 code后端收 code前端在页面上生成微信扫码登录的二维码用户扫码并确认后微信会重定向到你在开放平台配置的回调地址并带上一个 code 参数。前端把这个 code 取到然后 POST 给自己的后端接口。POST /api/login/wechat Content-Type: application/json { code: 0a3x8mKd3x8mKd, state: random_string }state 参数是用来防止 CSRF 攻击的。前端生成一个随机字符串放在生成二维码的 URL 里回调时微信会原样带回来后端需要校验这个值是否合法。很多项目省掉了这一步严格来说是有安全隐患的。3.2 第二步用 HttpClient 换 access_token后端收到 code 之后拼接微信接口的请求参数String url https://api.weixin.qq.com/sns/oauth2/access_token; ListNameValuePair params new ArrayList(); params.add(new BasicNameValuePair(appid, appId)); params.add(new BasicNameValuePair(secret, appSecret)); params.add(new BasicNameValuePair(code, code)); params.add(new BasicNameValuePair(grant_type, authorization_code)); String response httpClient.post(url, params);特别提醒这个 code 只能用一次有效期大概 5 分钟。如果后端处理出错导致重复提交微信会返回40029错误码提示 code 无效。所以前端拿到 code 后应该立即提交后端处理完也要记得能保证同一份 code 不被重复消费。3.3 第三步解析响应拉取用户信息微信返回的 JSON 长这样{ access_token: ACCESS_TOKEN, expires_in: 7200, refresh_token: REFRESH_TOKEN, openid: OPENID, scope: snsapi_login, unionid: UNIONID }拿到 access_token 后用另一个接口拉取用户信息String userInfoUrl https://api.weixin.qq.com/sns/userinfo ?access_token accessToken openid openid;这里有个业务层面的选择如果你的系统同时接入了微信开放平台、微信公众平台甚至小程序、App 等多端登录建议你保存 unionid 而不是 openid。因为同一个用户在微信生态里不同应用的 openid 是不同的但 unionid 是唯一的用 unionid 做用户主键才能实现多端账号打通。如果只做单一应用用 openid 作为唯一标识也够用。3.4 第四步生成自己的登录态微信的 access_token 有效期只有 7200 秒也就是 2 个小时而且它只能用来调用微信接口不能直接作为你系统的登录凭证。正确做法是后端用微信返回的 openid/unionid 去查用户表如果不存在就自动注册一个用户然后签发自己的 token。User user userMapper.selectByUnionId(unionId); if (user null) { user new User(); user.setUnionId(unionId); user.setNickname(nickname); user.setAvatar(avatar); userMapper.insert(user); } String appToken JwtUtil.createToken(user.getId(), user.getUnionId(), 7 * 24 * 3600 * 1000L);token 有效期看你的业务场景。我做过一个资讯类 App用户流失率比较高token 有效期设了 30 天配合 refresh_token 做续期如果是后台管理系统token 有效期 2 小时更安全。这里没有标准答案但有一个原则token 有效期越短越安全但用户体验越差需要根据业务权衡。3.5 第五步返回给前端登录完成后端返回自己的 token 给前端前端把 token 存起来之后每次请求都带上。这里不建议用 localStorage 存敏感登录态XSS 攻击可以直接把 token 偷走。更稳妥的方式是放在 HttpOnly 的 Cookie 里虽然会带来一点点跨域处理成本但安全性提升明显。这个我在下一节展开说。4. 前后端分离时代的登录态管理和跨域你如果搜“微信登录”相关的热词大概率会看到“vue3怎么连接后端”、“前后端分离项目实战”、“微信扫码登录”这些问题一起出现。原因是现在很多项目是前后端分离的前端跑在 5173 端口后端跑在 8080 端口之间隔着跨域这道坎。4.1 跨域和预检请求后端需要处理跨域让浏览器允许前端页面调用后端接口。Spring Boot 里最简单的做法Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(https://web.example.com); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意addAllowedOrigin不要直接用*因为当你允许携带 Cookie 的时候*是不起作用的浏览器会直接拦截响应并且allowCredentials(true)配合*存在安全风险。生产环境里建议精确写明允许的前端域名。还有一个点是setMaxAge(3600L)这是预检请求的缓存时间。如果你不设置这个值浏览器每次请求之前都会发一个 OPTIONS 预检请求白白增加网络开销设置 1 小时可以有效减少预检次数。我在项目里见过接口响应本身只要 100ms但预检请求就要 60ms 的情况对于接口性能要求高的场景这一步别省。4.2 登录态怎么传才安全前后端分离模式下token 传递方式有几种选择方案优点缺点适用场景Header 里放 Authorization代码直观后端处理简单token 暴露给 JS有 XSS 风险内部系统、API 服务、短期有效HttpOnly CookieJS 读取不到防 XSS跨域处理麻烦需要设置 SameSite面向用户的 Web 应用双 token 方案Cookie Refresh安全与体验平衡实现复杂度高对安全要求高的产品我个人的推荐是面向 C 端的 Web 应用用 HttpOnly Cookie SameSiteLax加上后端定期轮换 token。如果确实需要跨域就把 SameSite 设置为 None并开启 Secure同时后端精确指定 domain 和 path。这样配置之后前端 JS 里完全不需要关心 token 怎么存浏览器自动带上安全性和体验都能兼顾。4.3 换了微信以后自动退出是什么原因这个词条在热词里特别有意思“换了微信以后自动退出”。实际原因不是微信把登录态弄丢了而是你的登录态绑定维度没设计好。微信登录后的用户身份是基于 openid/unionid 的openid 和应用本身是绑定的和换了哪台设备没关系。所以正常设计下换手机、换微信账号都不会自动退出之前的会话。如果出现“换设备后自动退出”通常是这样几个原因服务器把 token 存储在了进程内存里没有持久化到 Redis服务重启或扩容后 token 全部失效登录时用了设备信息作为 token 生成因子换设备后签名校验失败多端登录互踢逻辑写得太激进比如同一账号登录了就撤销之前所有 token排查建议先看是不是多端互踢的问题再看 Redis 里存的 token 有没有掉线、过期时间是不是设太短。如果这两个都排除了大概率是代码里的设备绑定逻辑。微信开放平台的接口本身是没有设备概念的这个坑都是自己代码挖的。5. 常见问题与排查技巧实录这部分是真实的踩坑记录我按错误码和报错信息整理成了一张表格方便你做对照排查。5.1 微信接口常见错误码速查表错误码含义排查思路40013appid 无效检查后端配置的 appid 是不是填错或者用了别的平台的 appid40125AppSecret 错误检查 secret 是否对应同一个公众号/开放平台账号注意复制时别带空格40029code 无效code 只能使用一次且 5 分钟有效看看是不是重复提交了40030refresh_token 无效检查 refresh_token 是否过期或者已经被使用过41008缺少 code 参数后端收到的回调请求里没有 code看看回调地址对不对42001access_token 过期重新走一遍授权流程不要手动把 token 存在本地超过 2 小时45009接口调用超过限额等一段时间再试检查是不是有死循环在调接口我自己遇到最多的是 40029。有一次生产环境排查问题发现前端在微信回调页里误设置了window.location.reload()导致页面刷新了两次code 被重复提交第一次成功第二次报错。这种问题通常不影响用户最终体验因为第一次请求已经完成登录了但会污染日志让人误以为是代码逻辑出错。5.2 HttpClient 连接池耗尽的排查思路报错信息类似org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool第一步确认连接池上限是多少当前并发量是不是已经超过了这个值。用manager.getTotalStats()可以拿到当前连接池状态。第二步看代码里有没有正确关闭响应流。很多新手用 HttpClient 时只关闭了 CloseableHttpClient没有关闭响应对象导致连接一直没有释放回连接池。正确写法try (CloseableHttpResponse response httpClient.execute(request)) { String body EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); return body; }第三步看是不是有线程卡死。如果某个接口的 socketTimeout 设置得太大比如 60 秒而下游接口又一直不返回并发一高连接池就会被占满。我当时排查过一个案例帮别人调接口时对方把 socketTimeout 设成了 0无限等待微信接口异常时连接全挂住了。给你的建议很直接凡是调第三方接口超时参数必须显式设置而且要按接口的重要程度分级。核心链路设 3 秒非核心链路可以放宽到 10 秒但绝对不能无限等。5.3 部署 nginx 时的几个关键参数热词里有“后端部署时nginx需要什么信息”微信登录对接时尤其要注意这几点proxy_pass要指向后端服务的真实地址比如http://127.0.0.1:8080必须把Host、X-Real-IP、X-Forwarded-For这几个 header 传给后端否则后端拿不到用户的真实 IP微信回调的日志也没法溯源微信回调地址必须走 HTTPS微信开放平台强制要求回调域名是 443 端口。如果你只有 HTTP需要在 nginx 上配置 SSL 证书并在location块里把请求转发到后端还有一个非常隐蔽的坑如果你的服务经过 nginx 代理后端又是通过 request.getRequestURL() 来拼接回调地址的那一定要在 nginx 里设置proxy_set_header Host $host;和proxy_set_header X-Forwarded-Proto $scheme;。否则后端拿到的 protocol 可能是 HTTP拼出来的回调地址和开放平台配置的不一致微信直接报 redirect_uri 错误。改造后的 nginx 核心配置大致这样server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/cert/api.example.com.crt; ssl_certificate_key /etc/nginx/cert/api.example.com.key; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }5.4 小程序登录和手机号授权的差异如果你同时在接小程序登录注意和扫码登录的差异点。小程序登录用的是wx.login()拿到的 code然后通过https://api.weixin.qq.com/sns/jscode2session接口换 session_key 和 openid流程和网页扫码登录很像但接口地址不同、参数略有差异。还有一个场景是“微信授权手机号登录”这在手机 H5 里很常见。流程是前端点击授权后微信返回一个手机号加密数据后端需要用session_key做 AES 解密才能拿到明文手机号。解密算法微信文档里有示例我补充一个很容易踩的坑AES 解密的 key 是 session_key不是 access_token也不是 AppSecret。有人把 session_key 和 access_token 搞混解密一直失败浪费一整天。另外解密后的数据里purePhoneNumber才是手机号phoneNumber可能会有区号之类的格式差异。5.5 日志记录与脱敏接微信登录日志一定要打好。我习惯在每个关键节点打一条包含 traceId 的日志方便流式排查[wechat-login] [traceIdxxx] 收到code开始换取token [wechat-login] [traceIdxxx] 换取token成功openidoX8mKxxx耗时120ms [wechat-login] [traceIdxxx] 查询用户userId10086是否新用户true [wechat-login] [traceIdxxx] 签发token完成有效期604800s日志里不要打印完整的 access_token 和 AppSecret一旦日志外泄token 会被拿去直接调微信接口。打码方式很简单只保留前 4 位和后 4 位中间用***代替。6. 这类功能后续怎么扩展才不返工微信登录接入一次之后后续的迭代方向基本是围绕账号体系展开的。如果你刚做完第一版我建议你马上把下面这几件事做了不然后面会返工。6.1 用户表设计要兼容多端登录用户表的openid字段我建议设计成appid openid的组合唯一索引。因为一个用户在不同应用下的 openid 不一样如果只存 openid后续接入小程序、App、公众号时会撞车。预留一个union_id字段作为跨端统一的标识。CREATE TABLE wechat_user ( id bigint(20) NOT NULL AUTO_INCREMENT, app_id varchar(64) NOT NULL COMMENT 微信应用appid, open_id varchar(64) NOT NULL COMMENT 用户openid, union_id varchar(64) DEFAULT NULL COMMENT 用户unionid, nickname varchar(64) DEFAULT NULL, avatar_url varchar(512) DEFAULT NULL, created_at datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_app_open (app_id, open_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这样设计的好处是如果你以后接小程序登录复用同一套表结构只是 app_id 不同用户还能通过 union_id 打通账号不需要做数据迁移。6.2 令牌体系独立于第三方 access_token你的系统 token 生命周期应该完全独立管理。我见过有人直接把微信的 access_token 当成自己的登录态用两个小时过期后用户就被迫重新登录体验很差。正确方案是自己签 JWT token有效期自定义微信的 access_token 只用于换取用户信息用完即弃最多在 Redis 里缓存一段时间以备刷新。另外微信网页授权的 access_token 有一个刷新的机制通过 refresh_token 可以重新获取新的 access_token有效期也是 2 小时。我的建议是不要用这个刷新机制因为用户体验提升有限反而增加复杂度。直接让用户前端重新走一遍登录流程成本远小于维护 refresh_token 的代码。6.3 压测时随手看一眼连接池状态项目上线前压测一下微信登录接口是很有必要的。压测的时候注意看连接池的活动连接数。如果活动连接数长期顶着 maxPerRoute 的上限说明要么并发确实太高要么有连接泄漏。前者调大连接池并做限流后者查代码排查释放逻辑。我做过一次压测发现活动连接数一直在 50 上下而 QPS 才 20不正常。排查后发现问题出在响应流没关闭连接不被复用每秒都在新建连接修复之后 QPS 直接翻倍。代码层面检查连接释放有个笨但有效的方法在压测前后分别用manager.getTotalStats().getAvailable()看可用连接数是否恢复。如果压测结束后可用连接数明显低于压测前就得怀疑有连接泄漏了。承接这个话题我再分享一个自己处理连接泄漏的小技巧。网上很多文章让你在 finally 里关闭 response这没错但如果你用 Stream 读取了响应体返回后再关闭 response连接也可能已经被标记为不可复用。最稳妥的办法是用EntityUtils.consume()完全消费掉响应体再关闭连接。我一般直接写一个工具方法把 HTTP 提交和响应解析封装好避免每次调用都纠结释放问题。public static String postJson(String url, String jsonBody) throws IOException { HttpPost post new HttpPost(url); post.setHeader(Content-Type, application/json;charsetUTF-8); post.setEntity(new StringEntity(jsonBody, StandardCharsets.UTF_8)); try (CloseableHttpResponse response httpClient.execute(post)) { String body EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); EntityUtils.consumeQuietly(response.getEntity()); return body; } }EntityUtils.toString之后响应体已经完全读取再用consumeQuietly确保底层资源释放。这个小细节省了我很多次线上排查的时间。我个人这些年做下来最大的体会是HttpClient 和微信登录这类“第三方对接”的复杂度往往不在于接口文档本身而在于你周边的代码健不健壮。连接池有没有调好、超时有没有分级、日志有没有打够、token 有没有设计清楚——这些问题不出事的时候看着都无所谓一出事就是半夜爬起来看日志的级别。这篇笔记与其说是技术总结不如说是把这些年踩过的坑先替你们蹚了一遍。照着文里的流程接一遍至少能避开大部分常见的坑。如果你在接的时候遇到特殊情况欢迎带着日志来一起分析。
返回列表