Oauth2 - tianlu1677/tianlu1677.github.io GitHub Wiki
问题: 如何实现独立的用户系统并且保护某些资源只能被允许的应用访问修改?
可选方案:
-
Rails engine (最简单,类似devise一样,封装用户的相关操作,在项目中引用。缺点: 不独立)
-
OpenID (简单,安全性不高,密码一旦泄漏就会全部泄漏,适合企业内部使用 ruby方案 rubycas-server) openid 认证流程
-
oauth2 (复杂,调用流程以及增加。 优点:适用情景广泛,业界推荐)
Oauth2
示例: 微信的Oauth2认证
定义:
OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。 OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
假设:
有一个"云冲印"的网站,可以将用户储存在Google的照片,冲印出来。用户为了使用该服务,必须让"云冲印"读取自己储存在Google上的照片。 问题是只有得到用户的授权,Google才会同意"云冲印"读取这些照片。那么,"云冲印"怎样获得用户的授权呢? 传统方法是,用户将自己的Google用户名和密码,告诉"云冲印",后者就可以读取用户的照片了。这样的做法有以下几个严重的缺点。
解决了面临的问题:
- (1)"云冲印"为了后续的服务,会保存用户的密码,这样很不安全。
- (2)Google不得不部署密码登录,而我们知道,单纯的密码登录并不安全。
- (3)"云冲印"拥有了获取用户储存在Google所有资料的权力,用户没法限制"云冲印"获得授权的范围和有效期。
- (4)用户只有修改密码,才能收回赋予"云冲印"的权力。但是这样做,会使得其他所有获得用户授权的第三方应用程序全部失效。
- (5)只要有一个第三方应用程序被破解,就会导致用户密码泄漏,以及所有被密码保护的数据泄漏。
角色 role
resource owner # 这里指用户 User
resource server # user server 服务应用
client # 泛指客户端,这里是 app_server, web_server, or admin_server
authorization server # 发放凭证以及验证凭证的服务, doorkeeper
抽象认证流程
+--------+ +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| |<-(B)-- Authorization Grant ---| |
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource |
| | | Server |
| |<-(F)--- Protected Resource ---| |
+--------+
使用 Doorkeeper + Oauth2 来搭建一套用户系统
Doorkeeper的作用是 Authorization Server
1. 安装
gem 'doorkeeper'
rails generate doorkeeper:install
rails generate doorkeeper:migration
rake db:migrate
# add_foreign_key :table_name, :users, column: :resource_owner_id # 这句时每个用户可以给自己创建授权应用, 对不需要
会在routes.rb里面自动加入 'use_doorkeeper'
生成一下的路由
GET /oauth/authorize/:code
GET /oauth/authorize
POST /oauth/authorize
DELETE /oauth/authorize
POST /oauth/token
POST /oauth/revoke
GET /oauth/applications
GET /oauth/authorized_applications
DELETE /oauth/authorized_applications/:id
GET /oauth/token/info
2. 修改配置文件:config/doorkeeper.rb
# 账户登陆
resource_owner_authenticator do
# 这里使用devise做登陆
current_user || warden.authenticate!(:scope => :user)
end
资源所属者,即用户用什么信息登陆, 不过这个是用户password的模式使用
resource_owner_from_credentials do |routes|
# 社交登陆或者手机或者邮箱
if (type = params[:type] && uid = params[:uid])
user = User.find_by(User.social_type[type].to_sym => uid)
else
user = User.where("email = :value OR phone = :value", value: params[:username]).first
user if user && user.valid_for_authentication? { user.valid_password?(params[:password]) }
end
end
在最后中增加
Doorkeeper.configuration.token_grant_types << 'password'
Oauth2 使用: 默认使用Faraday来作为HTTP请求工具
# 密码的形式获取access_token
require "oauth2"
client_id = '34c049f76382ecf0e1b6933d76f00a4cb3e3cb6963221da5c97bfe780f115496'
secret_id = '72978fa435bbd39b4b1d7dd42aadf0284d42d0de9ad2cd3191cbb1c17e7278a9'
redirect_uri = 'http://localhost:3001/users/auth/doorkeeper/callback'
client = OAuth2::Client.new(client_id, secret_id, site: 'http://server.qdaily.com:3010')
username = '[email protected]'
password = '12345678'
access_token = client.password.get_token(username, password)
puts user_info = access_token.get('/api/users/me').parsed #{id, email, created_at, updated_at}
验证码的形式
client_id = '34c049f76382ecf0e1b6933d76f00a4cb3e3cb6963221da5c97bfe780f115496'
secret_id = '72978fa435bbd39b4b1d7dd42aadf0284d42d0de9ad2cd3191cbb1c17e7278a9'
redirect_uri = 'http://localhost:3001/users/auth/doorkeeper/callback'
client = OAuth2::Client.new(client_id, secret_id, site: 'http://server.qdaily.com:3010')
client.auth_code.authorize_url(:redirect_uri => redirect_uri)
# 得到一个url,在浏览器中请求一下得到 一个 code
access_token = client.auth_code.get_token(code, :redirect_uri => redirect_uri)
puts user_info = access_token.get('/api/users/me').parsed #{id, email, created_at, updated_at}
user = User.find(user_info['id'])
给应用授权,则应用有访问文档的权限
client_id = '34c049f76382ecf0e1b6933d76f00a4cb3e3cb6963221da5c97bfe780f115496'
secret_id = '72978fa435bbd39b4b1d7dd42aadf0284d42d0de9ad2cd3191cbb1c17e7278a9'
redirect_uri = 'http://localhost:3001/users/auth/doorkeeper/callback'
client = OAuth2::Client.new(client_id, secret_id, site: 'http://server.qdaily.com:3010')
client.client_credentials.get_token
以上是建立独立的服务
与devise结合使用
主要要跳转链接认证的方式 code
- 加入 gems
gem 'devise'
gem 'oauth2'
gem 'omniauth-oauth2', '~> 1.3.1'
- 修改devise配置文件 在 config/initialize/devise.rb 中
require 'omniauth/strategies/doorkeeper'
config.omniauth :doorkeeper, appId,
screctId
- 增加doorkeeper的 认证策略
lib/omniauth/strategies/doorkeeper
文件中
require 'omniauth-oauth2'
module OmniAuth
module Strategies
class Doorkeeper < OmniAuth::Strategies::OAuth2
# change the class name and the :name option to match your application name
option :name, :doorkeeper
option :client_options, {
:site => "http://server.qdaily.com:3010", # 注意地址!!!
:authorize_url => "/oauth/authorize",
:token_url => "/oauth/token"
}
option :provider_ignores_state, true
uid { raw_info['id'] }
info do
{
id: raw_info["id"],
username: raw_info["username"],
email: raw_info["email"],
phone: raw_info["phone"]
}
end
def raw_info
@raw_info ||= access_token.get('/api/users/me').parsed # 请求服务器返回的内容
end
end
end
end
- 添加路由
:omniauth_callbacks => "users/omniauth_callbacks"
在 user.rb中增加
devise :omniauthable, omniauth_providers: [:doorkeeper]
- 控制器中实现具体内容
users/omniauth_callbacks_controller.rb 中实现
def doorkeeper
oauth_data = request.env['omniauth.auth']
@user = User.find_by(id: oauth_data['info']['id']) # 应该用create_or_find_by, 如果不是用同一个数据库的话
sign_in :user, @user
redirect_to root_url # 跳转
end
- 在view中某个位置中写登陆地址, 然后会跳转到 认证控制器,然后再跳转到 指定的url
<%= link_to '登陆', user_doorkeeper_omniauth_authorize_path %>
注意点: 1. site 应该改为动态,以及 raw_info 信息获取的方式 2. provider_ignores_state 忽略掉,服务器端未传递 3. gem 'omniauth-oauth2', '~> 1.3.1' # 1.4 版本有问题
在实际项目的使用
- 单纯的API调用
- 跳转链接方式 2.1 同一个数据库的时候,就调换认证相关信息 2.2 不同数据库,则先在主数据库 www.qdaily.com 注册信息。然后从 data.qdaily.com 跳转到认证服务器 user_server, 认证后则只需要 在 data.qdaily.com 写入该用户的基本信息则可实现用户的登陆,注册,找回密码等功能。