Rails 微信开放平台 Omniauth 探索

night_7th · May 03, 2015 · Last by jujurslot replied at January 27, 2021 · 17351 hits
Topic has been selected as the excellent topic by the admin.

准备

最近在折腾网站的微信登录功能,最终希望实现的效果是类似一号店这样的微信登录

为此首先需要在微信开放平台上注册开发者账号,创建网站应用,获得对应的 AppID 和 AppSecret,并申请微信登录接口。在这个过程中,会被要求填写网站信息,包括授权回调域和官网网址。

这里需要注意的是授权回调域的填写,刚开始我是这么填写的:

example.com/auth/wechat/callback

然而这么填写将会在后面的 OAuth 认证时提示 redirect_uri 参数错误。微信开放平台并没有给出授权回调域的官方填写说明,我是在微信公众平台的开发者文档里看到了这么一段话:

关于网页授权回调域名的说明

请注意,这里填写的是域名(是一个字符串),而不是 URL,因此请勿加 http://等协议头;

授权回调域名配置规范为全域名,比如需要网页授权的域名为:www.qq.com,配置以后此域名下面的页面http://www.qq.com/music.htmlhttp://www.qq.com/login.html 都可以进行 OAuth2.0 鉴权。

哦,原来改成这样就可以了:

example.com

调试

接下来,我想的是到底该如何调试,因为授权回调域不能填成 localhost,总不至于在线上调试吧。

搜索后发现有人推荐一款叫 ngrok 的软件,只需要注册并下载 ngrok,就可以通过 ngrok 获得一个外网域名,而这个外网域名实际访问的是本地主机。

具体 ngrok 的注册下载流程省略,按照ngrok 官网一步步来就可以了,这里需要自带梯子。

之后本地执行./ngrok http -subdomain=example 3000,出现下面的画面就算成功啦: ngrok 那么开发阶段,就把刚才提到的授权回调域改成自己设定的 ngrok 地址吧。

OAuth 流程

整个微信登录的流程是如下图所示的: wechat-auth-process

OAuth 过程可以使用 omniauth,omniauth 是一个利用 Rack 中间件实现的灵活的认证系统。

omniauth 需要结合具体平台的 strategy 使用,比如你需要做 github 账号的登录,那就需要omniauth-github这个 gem。omniauth 官方列出了社区维护的各个平台的strategy list,我在 list 中找到了微信的 omniauth strategy:omniauth-wechat-oauth2

具体怎么使用应该首先看看 README 的文档,我读到了这么一句话:

You need to get a wechat API key at: http://mp.weixin.qq.com

这不是微信公众平台的地址么,可我需要的是微信开放平台的 OAuth 认证啊,这两个微信平台的 OAuth 流程是否一致,需要确定下。于是我开始打开了微信开放平台和微信公众平台的 OAuth 流程的文档进行对比,发现果然有些细节处不一致,比如第一步请求 code 时,两者请求地址的形式略有不同,微信开放平台的请求 code 地址是:

https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

而微信公众平台的请求 code 地址却是:

https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

我去查看了 omniauth-wechat-oauth2 的 strategy 源码:

# /lib/omniauth/strategies/wechat.rb
option :client_options, {
  site:          "https://api.weixin.qq.com",
  authorize_url: "https://open.weixin.qq.com/connect/oauth2/authorize#wechat_redirect",
  token_url:     "/sns/oauth2/access_token",
  token_method:  :get
}

这个 authorize_url 的地址显然是微信公众平台请求 code 时的格式,这就意味着这个 gem 并不是我需要的。

不过我发现微信公众平台和微信开放平台的基本 OAuth 流程是类似的,于是我参考 omniauth-wechat-oauth2 的源码,自己实现了微信开放平台的 omniauth strategy,并托管在了GitHub 上。这里感谢一下 omniauth-wechat-oauth2 的原作者 skinnyworm,复用了大部分他的代码。

CSRF 错误

剩下的工作就和其他平台的 OAuth 流程差不多了,这里就不再赘述,具体可以看一下文章后的参考链接。

只是这里遇到了一个 CSRF 错误,特别提一下:

Started GET "/auth/wechat/callback?code=xxxxxxxx&state=xxxxxxxx" for 183.157.160.37 at 2015-04-28 22:26:48 +0800
I, [2015-04-28T22:26:48.017297 #18928]  INFO -- omniauth: (wechat) Callback phase initiated.
E, [2015-04-28T22:26:48.017785 #18928] ERROR -- omniauth: (wechat) Authentication failure! csrf_detected: OmniAuth::Strategies::OAuth2::CallbackError, csrf_detected | CSRF detected
E, [2015-04-28T22:26:48.017891 #18928] ERROR -- omniauth: (wechat) Authentication failure! invalid_credentials: OmniAuth::Strategies::OAuth2::CallbackError, csrf_detected | CSRF detected

这是因为我本地调试时习惯性地打开了 localhost:3000,而我又开启了 ngrok,调试时应该直接打开 example.ngrok.io 的。

参考

railscasts-china 的Omniauth 1

happypeter 的login-with-linkedin

感兴趣的同学还可以拓展阅读下微信开放平台和微信公众平台的区别

ngrok 速度有些慢,可以考虑用 tunnel.mobi,依赖 ngrok 的国内软件。

@zedde tunnel.mobi 能够在穿透内网吗?

赞,很有用

没有必要用那么多 gem,根据每个平台写个 strategies

微信有很多类“Oauth”的接口(公众号与企业号),其实独立封装成一个接口即可https://github.com/lanrion/weixin_authorize/blob/master/lib%2Fweixin_authorize%2Fapi%2Foauth.rb#L13 ,直接生成授权 URL,然后再独立获取数据,会更清晰点。

仅个人看法。:)

不错。收藏先。

Hi @ruby_sky 你这应该是微信公众平台的 OAuth 流程 我看链接/connect/oauth2/后跟的是authorize哦:

WeixinAuthorize.open_endpoint("/connect/oauth2/authorize?appid=#{app_id}&redirect_uri=#{redirect_uri}&response_type=code&scope=#{scope}&state=#{state}#wechat_redirect")

微信开放平台这一步的链接/connect/oauth2/后面跟的是qrconnect,略有区别。

我当初就是想找一个微信开放平台 omniauth strategy 的 gem,就像 omniauth-github、omniauth-linkedin 一样的,却没有找到,只找到了微信公众平台的,就只好自己实现了。

#7 楼 @night_7th 我举的例子是手机端的授权,你举的例子是 WEB 版本的,我想表明的是,其实,拼接成一个 URL 会不会更简单。不过楼主抽成一个 gem,也是很不错。

话说 LZ 可以考虑再深入一步,讲讲开放平台 unionid 的问题,这个估计是对接微信时最大的坑

@ruby_sky 哦,我懂啦,同样零基础开始开发的话你说的方式应该更简单。 我因为是在 skinnyworm 的源码基础上修改出来的,很多坑他都踩完啦,所以也没花费太久时间。 看你开源了好多的微信相关的 Gem,赞!

#10 楼 @night_7th 赞就要 Star 一下。😄

#9 楼 @jasl unionid 坑多吗?没有太多感觉。描述一下?

#12 楼 @ruby_sky 其实也还好了,像 KO 这种网站会接入多家网站,对于微信来说要多存一个 UnionID,存储数据结构要考虑一下。

话说我看过 omniauth-wechat-oauth2 还有我们用的 omniauth-wechat 都没把 unionid 放到返回的结构体里,按照微信支付的文档来看应当会随着返回的。 另外这两个 Gem(后者我贡献过代码解决了)不支持动态定制 scope(scope 在配置写死或者根据传入 URL 参数)还有 state 参数(这个做返回后的页面跳转很有帮助)

当然啦,是不是坑还是结合业务来看的。 首先,定制 scope 在一些场景下很有必要,比如,微信浏览器里的分享到___API,是需要 access_token 的,但此时如果 scope 为 snsapi_userinfo,那么就会弹出授权框,影响用户体验。但是用户注册的时候,还是希望告知用户授权并且这样也可以拿到更全面的用户信息,这时候就需要 snsapi_userinfo 了。

另外,要为微信设计特别的授权流程(如果多网站接入的话),前边说了,从微信处授权有两种目的:1.方便调用分享 API 2.用户注册 要为这两种情况设计不同的行为和交互,而其他网站的接入回调的业务逻辑通常是统一的,这就引入了额外的开发成本。

#13 楼 @jasl

顺便打一下广告: 公众号: https://github.com/lanrion/weixin_authorize (支持 scope 定制) https://github.com/lanrion/weixin_rails_middleware/ 企业号: https://github.com/lanrion/qy_wechat https://github.com/lanrion/qy_wechat_api (持续开发中)

unionid 是公众号开启了 UnionID 机制才会有这个值返回。如果要获取 UnionID 或者其他信息,其实只需要知道 openid,就能通过“获取用户信息接口”来获取。

oauth2(微信版本),我用很简单的做法,直接封装 URL,获取 code,再自己获取其他数据。


# 生成授权URL
# 可选值:scope="snsapi_base", state="weixin"
$weixin_client.authorize_url(url) 

# 获取sns info
def get_wechat_sns
  logger.debug("invoke get_wechat_sns")
  if session[:openid].blank? && params[:state].present?
    sns_info = $weixin_client.get_oauth_access_token(params[:code])
    logger.debug("Weixin oauth2 response: #{sns_info.result}")
    # 重复使用相同一个code调用时:
    if sns_info.result["errcode"] != "40029"
      session[:openid] = sns_info.result["openid"]

      set_current_phone_openid

      if current_user
        begin
          current_user.update!(:open_id, session[:openid])
        rescue => e
          logger.debug("get_wechat_sns: #{e}")
        end
      end
    end
  end
end

# 获取用户信息
user_info = $client.user(ENV["OPENID"])

分享 API,我是这么干的: https://github.com/lanrion/weixin_authorize/wiki/js-sdk

后面的“另外”描述,我看得不是很清晰。

pagekite 也不错,挺快的。只是只有一个月免费。

#14 楼 @ruby_sky unionid 那个不用那么麻烦,如果有开放平台,那么授权会返回 unionid,所以略修改下那俩 gem 的 strategy 就可以啦

#16 楼 @jasl 开放平台那个(PC 端的授权),还没有读过文档,不过顺便吐槽一下微信的文档与企业号试用用户申请体验超级差。

反向代理推荐个 proxylocal,简单方便也很稳定

坑啊,我们之前真的是在服务器上搞得。。。

ngrok 已经被墙了吧,我都改用:

npm install -g localtunnel

#21 楼 @ruby_sky 没有吧?今天还用过。不过最近倒是发现不太稳定,而且手机调试经常会遇见一些诡异的问题……

用不着 ngrok. 通过 ssh 把本地的端口映射到外部的某个 vps 即可。类似 ssh -R 80:localhost:80 [email protected] 即可

#22 楼 @evedreamer “诡异的问题”应该就是“被墙的感觉”。所以就换了 localtunnel。 #23 楼 @seveniruby 抽空试试你的方法。

对于网站的第三方登录,其实走的是本地浏览器,使用本地来解析域名的。

If the Consumer provided a callback URL in oauth_callback (as described in Consumer Directs the User to the Service Provider), the Service Provider constructs an HTTP GET request URL, and redirects the User’s web browser to that URL

所以你可以直接使用 localhost 作为回调域,如果一些变态的网站做了域名监测,也只需要在 host 里面配置一个 alias 即可。比如 127.0.0.1 love.dev.com

我之前研究了一些各种第三方登录的环境问题,需要去这里获取

@zlx_star 赞文章,说得挺全!我就是看到参考资料中第二篇提到了ngrok才去试用一下,发现还不错。

#21 楼 @ruby_sky 可以使用这个,http://www.tunnel.mobi/。在 $HOME/.ngrok 中添加:

server_addr: "tunnel.mobi:44433"
trust_host_root_certs: true

就可以继续用了。

#1 楼 @zedde 我觉的 只是搭了个 ngrok 的 server,或者转发了一下请求而已,我就直接在.ngrok 配置中,写入了 tunnel.mobi 提供的配置就可以用了。

#2 楼 @winnie 可以穿透内网的。

#28 楼 @xiajian 赞,已收藏。

@night_7th 大家有没有在发送消息时,遇到问题的?当用户向开放平台发消息,我们的 Rails 程序回复用户消息时,用户一直收不到信息,提示服务暂时不可用,通过微信自己的调试工具,显示加解密一直正常。但如果用公众服务号就没有这个问题。

33 Floor has deleted
34 Floor has deleted
35 Floor has deleted
36 Floor has deleted
37 Floor has deleted
38 Floor has deleted
39 Floor has deleted

@night_7th 项目开发中用 omniauth-wechat-oauth2 登录微信公众平台,用 tunnel.mobi 做了个代理,本地开发调试,但总是返回根你一样的 CSRF 错误,你说 这是因为我本地调试时习惯性地打开了localhost:3000,而我又开启了ngrok,调试时应该直接打开example.ngrok.io的 不太理解怎能弄,如果关掉了 rails s,本地 localhost:3000 是打不开的,同时 ngrok 映射的域名也打不开的,请问该怎么弄呢。

@vmeng 不要关闭rails s,你开了ngrok,本地既可以用localhost:3000打开,也可以使用ngrok设置的地址打开啊~

平时我们运行了rails s之后,都是打开localhost:3000,你现在直接打开ngrok设置的地址就好。

赞楼主,正好需要。

@night_7th

发现一个问题,request.env['omniauth'][uid] = nil,无法获取 UID。

https://github.com/yangsr/omniauth-wechat-oauth2/pulls

@springwq 谢谢~已经 merge 啦~

tx 坑爹,oauth2 的 gem 因为 client_id 参数名字不符合。。。

用了你的 gem 后,然我绑定备案后的回调郁闷,mdzz

ngrok 被腾讯封了。

chairy11 in 谁来个微信开发学习资源大全? mention this topic. 06 Jul 23:19
48 Floor has deleted

我想问下,为什么接入失败了?代码返回的数据都是可以的。

你好,我这边按照 omniauth 和 omniauth-wechat-oauth2 配置了微信测试公众平台的信息,rails s 启动了服务,ngrok 启动了内网穿透,然后我访问我配置的域名:xxx.xxx.cn/auth/wechat以后,弹出这样的提示框。 ,一直没找到问题出在哪。 log 中的打印是这样的:

Started GET "/auth/wechat" for 127.0.0.1 at 2017-12-24 23:28:41 +0800
I, [2017-12-24T23:28:41.423490 #34087]  INFO -- omniauth: (wechat) Request phase initiated.
Reply to mz2test

确认一下回调地址吧 😧 看提示是回调出问题了

You need to Sign in before reply, if you don't have an account, please Sign up first.