Rails项目接入微信登录

在 Rails 项目中,很流行用 devise 来做用户系统,够简单、够快速。但是在国内,似乎 devise 越来越不受欢迎。这是因为, devise 默认是用 email 作为登录凭证,更符合国外的习惯,但是越来越不符合国内用户的使用习惯。由于微信的强势流行,越来越多的国内用户们都被养成了“扫码登录”的习惯。

所以,如果是针对国内用户的项目,再使用庞大的 devise 显得没太大必要,扩展起来也更加麻烦,还不如手动自建用户系统。本文介绍 Rails 项目如何使用微信登录建立用户系统。

准备工作

  1. 微信开放平台注册开发者帐号;
  2. 进行开发者资质认证;
  3. 创建一个网站应用,并审核通过,获得相应的AppID和AppSecret。
  4. 申请微信登录且通过审核后,可开始接入流程。

以上的准备工作请参考微信官方文档,最终得到一个可用的AppIDAppSecret

安装 figaro 这个 Gem 管理加密信息,在 Gemfile 里添加

gem 'figaro'

运行

bundle exec figaro install

config/application.yml 里添加

OMNIAUTH_OPEN_WECHAT_APP_ID: '你申请到的AppID'
OMNIAUTH_OPEN_WECHAT_APP_SECRET: '你申请到的AppSecret'

这样就可以在 rails 项目中用 Figaro.env.OMNIAUTH_OPEN_WECHAT_APP_IDFigaro.env.OMNIAUTH_OPEN_WECHAT_APP_SECRET 来调用你的 AppID 和 AppSecret 了。

建立用户系统

如果没有尝试过手动自建用户系统,可以参考 Ruby on Rails 教程 中第 6~12 章。

如果是新建项目,推荐使用由深圳百分之八十公司提供的 rails-template 初始化项目,该模板经过长时间的打磨,集成了最常用的模块,省了非常多新建项目前期的重复工作。

创建用户模型

rails g model user

db/migrate/xxxx_create_users.rb

class CreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      t.string :wechat_unionid, null: false, comment: '微信用户的 UnionID'

      t.timestamps
    end
    add_index :users, :wechat_unionid, unique: true
  end
end

如果仅提供微信登录,只需要一个 wechat_unioid 字段作为用户的唯一标识。

rails g user_profile

db/migrate/xxxx_create_profiles.rb

class CreateUserProfiles < ActiveRecord::Migration[5.1]
  def change
    create_table :user_profiles do |t|
      t.references :user, foreign_key: true
      t.string :avatar, comment: '头像'
      t.string :name, comment: '姓名'
      t.string :phone, comment: '手机'
      t.string :email, comment: '邮箱'
      t.string :province, comment: '省份'
      t.string :city, comment: '城市'
      t.string :address, comment: '地址'

      t.timestamps
    end
  end
end

这些是成功调用微信接口之后能获取到的用户信息。

然后

rake db:migrate

接着建立各模型间的关联。

/models/user.rb 添加

class User < ApplicationRecord
  has_one :profile, class_name: 'UserProfile', dependent: :destroy

  validates :wechat_unionid, presence: true, uniqueness: true
end

models/user_profile.rb 里添加

class UserProfile < ApplicationRecord
  belongs_to :user
end

控制器定义登录方法

新建一个 session 控制器

touch app/controllers/session_controller.rb

先定义几个基本 method。

class SessionsController < ApplicationController
  def create
  end

  def failure
  end

  def destroy
  end
end

修改 routes.rb ,加入

Rails.application.routes.draw do
  match '/auth/:provider/callback', to: 'sessions#create', via: [:get, :post]
  match '/auth/failure', to: 'sessions#failure', via: :get
  delete '/logout', to: 'sessions#destroy', as: :logout
  # ...
end

application_controller.rb 里定义登入/登出的方法

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :authenticate_user! #默认全局必须登录
  helper_method :current_user

  private

  def authenticate_user!
    redirect_to root_path unless current_user
  end

  def current_user
    @_current_user ||= session[:current_user_id] && User.find_by(id: session[:current_user_id])
  end

  def user_sign_in(user)
    session[:current_user_id] = user.id
  end

  def user_sign_out
    session[:current_user_id] = nil
    @_current_user = nil
  end
end

登录步骤解释

根据微信开放平台的文档,微信用户扫码登录的流程分为3步:

  1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
  2. 通过code参数加上AppID和AppSecret等,通过API换取access_token;
  3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。

微信登录时序图

发起微信授权登录请求

我们先来做第一步,对照这上面的时序图,第一步其实分为5小步。

(1) 微信用户点击登录(请求登录网站);

(2) 网站向微信发起登录请求(请求微信 OAuth2.0授权登录);

(3) 微信返回一个登录二维码(请求用户确认);

(4) 用户扫码(用户确认);

(5) 微信返回授权信息(带上授权临时票据(code)重定向到网站)

这里采用的是 JS微信登录 方法。

app/views/layouts/application.html.erb 里加入

<%= javascript_include_tag 'https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js' %>

这是微信开放平台提供的登录 JS 文件。

创建一个 partial 放置登录二维码,比如 app/views/shared/_wechat_login.html.erb

<% unless current_user %>
  <div class="modal fade" id="wechat-login-modal">
	<div class="modal-dialog modal-md">
      <div class="modal-content">
        <div class="modal-header">
          <button class="close" data-dismiss="modal">
            <span>x</span>
          </button>
        </div>
        <div class="modal-body">
          <div class="row">
            <div id="wechat-login-container">
            </div>
          </div>
        </div>
      </div>
	</div>
  </div>
<% end %>

<script>
  if (typeof WxLogin !== 'undefined' ) {
    new WxLogin({
      id: 'wechat-login-container',
      appid: '#{Figaro.env.OMNIAUTH_OPEN_WECHAT_APP_ID!}',
      scope: 'snsapi_login',
      redirect_uri: '#{root_url + 'auth/open_wechat/callback'}',
      state: '',
      style: '',
      href: ''
    });
  }
</script>

设置一下样式,在 application.scss 里增加

#wechat-login-modal {
  .modal-header {
    border-bottom: none;
  }
}
#wechat-login-container {
  text-align: center;
}

再把这个 partial 引入到 application.html.erb 里去,就变成全局的了。

<%= render 'shared/wechat_login' %>

这个放置二维码的 modal 默认是隐藏(fade)的,再把调起的方法绑定到登录的按钮上去。

这样来实现:

为登录按钮增加一个 class

<botton class="wechat-login-required">
  登录
</botton>

然后在 application.js 里增加一个调起函数

$('.wechat-login-required').click(function() {
  $("#wechat-login-modal").modal();
});

注意:这里需要 JQuery 的支持。

这一步就完成了。

回顾一下这一步的流程:

当用户点击“登录”按钮的时候,会触发 $("#wechat-login-modal").modal(),也就是把 wechat_login 这个 partial 里的 modal 显示,在这个 modal 里,创建了一个 wxLogin 的对象,通过网站提供的 AppID 向微信开放平台请求得到了一个二维码,显示在 #wechat-login-container 的位置。

用户用微信扫码之后,微信就会返回一个授权临时票据code参数,返回到哪里呢?返回到我们自定义的 redirect_uri 里,也就是#{root_url + 'auth/open_wechat/callback'}。而这个地址在 routes.rb 里做了定义,其实最终是返回到了 sessions#create 里。

引入omniauth

接下来我们来做第2步和第3步

  1. 通过code参数加上AppID和AppSecret等,通过API换取access_token;
  2. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。

看起来很复杂,但其实有 gem 包可以直接用。

在 Gemfile 里添加

gem 'omniauth-open_wechat'

然后运行 bundle install

到 GitHub 上阅读更多的使用说明:omniauth-open_wechat

config/initializers 里新建一个 omniauth.rb 文件:

touch config/initializers/omniauth.rb

根据 omniauth-open_wechat 的说明进行配置:

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :open_wechat,
    Figaro.env.OMNIAUTH_OPEN_WECHAT_APP_ID!,
    Figaro.env.OMNIAUTH_OPEN_WECHAT_APP_SECRET!,
    scope: 'snsapi_login',
    provider_ignores_state: true
end

配置好之后,配合前面的第一步,只要调用 request.env['omniauth.auth'] ,这个 gem 包就帮我们完成了第2步和第3步,直接得到微信返回的用户信息了,格式是一个 Hash:

{
	"provider"=>"open_wechat", 
	"uid"=>"xxx...", 
	"info"=>{
		"nickname"=>"xxx", 
		"sex"=>1, 
		"province"=>"xxx", 
		"city"=>"xxx", 
		"country"=>"xxx", 
		"headimgurl"=>"xxx", 
		"name"=>"free"
	}, 
	"credentials"=>{
		"token"=>"xxx...", 
		"refresh_token"=>"xxx...", 
		"expires_at"=>2015-06-04 16:17:26 +0800, 
		"expires"=>true
	}, 
	"extra"=>{
		"raw_info"=>{
			"openid"=>"xxx...", 
			"nickname"=>"free", 
			"sex"=>1, 
			"language"=>"zh_CN", 
			"city"=>"xxx", 
			"province"=>"xxx", 
			"country"=>"CN", 
			"headimgurl"=>"xxx", 
			"privilege"=>[], 
			"unionid"=>"xxx..."
		}
	}
}

接下来,我们需要用这些信息来实现创建用户和用户登录。

前面讲过,微信返回的这些用户信息最后是到了 sessions#create 那里,所以我们接下来修改 app/controllers/sessions_controller.rb

class SessionsController < ApplicationController
  skip_before_action :authenticate_user!, only: [:create, :failure]
  skip_before_action :verify_authenticity_token, only: [:create]
  
  def create
    auth = request.env['omniauth.auth']
    # 在这里接收微信返回的用户信息。
    user = User.from_omniauth_for_open_wechat(auth)
    user_sign_in(user) if user
    redirect_to root_path
  end

  def failure
    redirect_to root_path
  end

  def destroy
    user_sign_out
    redirect_to root_path
  end
end

model/user.rb 里定义一个 from_omniauth_for_open_wechat 的类方法

def self.from_omniauth_for_open_wechat(auth)
  unionid = auth.extra.raw_info.unionid
  raise "No unionid found, please check omniauth configure!" if unionid.blank?

  user = User.find_or_create_by! wechat_unionid: unionid

  unless user.profile
    info = auth.info
    user.create_profile! name: info.nickname, avatar: info.headimgurl, \
      province: info.province, city: info.city, gender: info.sex
  end
  user
end

至此,整个登录过程完成了。

其他

当然,用 devise 也可以接入微信登录,方法也类似,但就需要开发者先读懂 devise 的文档了。

· rails, tech