Rails项目接入微信登录
在 Rails 项目中,很流行用 devise 来做用户系统,够简单、够快速。但是在国内,似乎 devise 越来越不受欢迎。这是因为, devise 默认是用 email 作为登录凭证,更符合国外的习惯,但是越来越不符合国内用户的使用习惯。由于微信的强势流行,越来越多的国内用户们都被养成了“扫码登录”的习惯。
所以,如果是针对国内用户的项目,再使用庞大的 devise 显得没太大必要,扩展起来也更加麻烦,还不如手动自建用户系统。本文介绍 Rails 项目如何使用微信登录建立用户系统。
准备工作
- 在微信开放平台注册开发者帐号;
- 进行开发者资质认证;
- 创建一个网站应用,并审核通过,获得相应的AppID和AppSecret。
- 申请微信登录且通过审核后,可开始接入流程。
以上的准备工作请参考微信官方文档,最终得到一个可用的AppID和AppSecret。
安装 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_ID
和 Figaro.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步:
- 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;
- 通过code参数加上AppID和AppSecret等,通过API换取access_token;
- 通过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步,
- 通过code参数加上AppID和AppSecret等,通过API换取access_token;
- 通过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 的文档了。