Rails 微信开放平台 Omniauth 探索

night_7th · 发布于 2015年05月03日 · 最后由 pynix 回复于 2016年06月17日 · 9415 次阅读
15073
本帖已被设为精华帖!

准备

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

为此首先需要在微信开放平台上注册开发者账号,创建网站应用,获得对应的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

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

共收到 39 条回复
12859

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

2107

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

15999

赞,很有用

15999

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

273

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

仅个人看法。:)

68

不错。收藏先。

15073

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一样的,却没有找到,只找到了微信公众平台的,就只好自己实现了。

273

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

1107

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

15073

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

273

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

273

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

1107

#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.用户注册 要为这两种情况设计不同的行为和交互,而其他网站的接入回调的业务逻辑通常是统一的,这就引入了额外的开发成本。

273

#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

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

11340

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

1107

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

273

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

96

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

10912

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

273

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

npm install -g localtunnel
12016

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

96

用不着ngrok. 通过ssh把本地的端口映射到外部的某个vps即可. 类似ssh -R 80:localhost:80 user@public.oneapm.com 即可

12859

#2楼 @winnie 可以。

273

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

2456

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

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

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

15073

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

12435

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

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

就可以继续用了。

12435

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

12435

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

273

#28楼 @xiajian 赞,已收藏。

96

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

33楼 已删除
34楼 已删除
35楼 已删除
36楼 已删除
37楼 已删除
38楼 已删除
39楼 已删除
96

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

15073

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

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

4898

赞楼主,正好需要。

4898

@night_7th

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

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

15073

@springwq 谢谢~已经merge啦~

9800

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

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

9800

ngrok 被腾讯封了。

827 chairy11 谁来个微信开发学习资源大全? 中提及了此贴 07月06日 23:19
48楼 已删除
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册