├── lib
├── weixin_rails_middleware
│ ├── version.rb
│ ├── engine.rb
│ ├── helpers
│ │ ├── auto_generate_weixin_token_secret_key.rb
│ │ ├── unique_token_helper.rb
│ │ ├── pkcs7_encoder.rb
│ │ ├── prpcrypt.rb
│ │ └── reply_weixin_message_helper.rb
│ ├── models
│ │ ├── encrypt_message.rb
│ │ ├── message.rb
│ │ └── reply_message.rb
│ ├── adapter
│ │ ├── single_public_account.rb
│ │ ├── multiple_public_account.rb
│ │ └── weixin_adapter.rb
│ └── configuration.rb
├── generators
│ ├── templates
│ │ ├── README
│ │ ├── add_encrypt_message_config_migration.rb
│ │ ├── add_weixin_secret_key_and_weixin_token_migration.rb
│ │ ├── install_weixin_rails_middleware.rb
│ │ └── weixin_controller.rb
│ └── weixin_rails_middleware
│ │ ├── encrypt_migration_generator.rb
│ │ ├── install_generator.rb
│ │ └── migration_generator.rb
└── weixin_rails_middleware.rb
├── .gitignore
├── config
└── routes.rb
├── bin
└── rails
├── Gemfile
├── Rakefile
├── weixin_rails_middleware.gemspec
├── MIT-LICENSE
├── app
└── controllers
│ └── weixin_rails_middleware
│ └── weixin_controller.rb
├── Gemfile.lock
└── README.md
/lib/weixin_rails_middleware/version.rb:
--------------------------------------------------------------------------------
1 | module WeixinRailsMiddleware
2 | VERSION = "1.3.3"
3 | end
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .bundle/
2 | log/*.log
3 | pkg/
4 | test/dummy/db/*.sqlite3
5 | test/dummy/db/*.sqlite3-journal
6 | test/dummy/log/*.log
7 | test/dummy/tmp/
8 | test/dummy/.sass-cache
9 | .DS_Store
10 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware/engine.rb:
--------------------------------------------------------------------------------
1 | module WeixinRailsMiddleware
2 | class Engine < ::Rails::Engine
3 |
4 | isolate_namespace WeixinRailsMiddleware
5 | engine_name :weixin_engine
6 |
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | WeixinRailsMiddleware::Engine.routes.draw do
2 | get 'weixin/:weixin_secret_key', to: 'weixin#index', as: :weixin_server
3 | post 'weixin/:weixin_secret_key', to: 'weixin#reply', as: :weixin_reply
4 | end
5 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
3 |
4 | ENGINE_ROOT = File.expand_path('../..', __FILE__)
5 | ENGINE_PATH = File.expand_path('../../lib/weixin_rails_middleware/engine', __FILE__)
6 |
7 | require 'rails/all'
8 | require 'rails/engine/commands'
9 |
--------------------------------------------------------------------------------
/lib/generators/templates/README:
--------------------------------------------------------------------------------
1 | ===============================================================================
2 | ## 如果有任何疑问,请添加微信(dht_ruby)有偿提问:
3 |
4 | ## 微信 Ruby 高级API:
5 |
6 | https://github.com/lanrion/weixin_authorize
7 |
8 | ## 企业号相关gem:
9 |
10 | https://github.com/lanrion/qy_wechat
11 |
12 | https://github.com/lanrion/qy_wechat_api
13 |
14 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware/helpers/auto_generate_weixin_token_secret_key.rb:
--------------------------------------------------------------------------------
1 | module WeixinRailsMiddleware
2 | module AutoGenerateWeixinTokenSecretKey
3 | extend ActiveSupport::Concern
4 |
5 | included do
6 |
7 | before_create do
8 | self.weixin_secret_key = generate_weixin_secret_key
9 | self.weixin_token = WeiXinUniqueToken.generate
10 | end
11 | end
12 |
13 | private
14 |
15 | def generate_weixin_secret_key
16 | WeiXinUniqueToken.generate(generator: :urlsafe_base64, size: 32).downcase
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Declare your gem's dependencies in weixin_rails_middleware.gemspec.
4 | # Bundler will treat runtime dependencies like base dependencies, and
5 | # development dependencies will be added by default to the :development group.
6 | gemspec
7 |
8 | # Declare any dependencies that are still in development here instead of in
9 | # your gemspec. These might include edge Rails or gems from your path or
10 | # Git. Remember to move these dependencies to your gemspec before releasing
11 | # your gem to rubygems.org.
12 |
13 | # To use debugger
14 | # gem 'debugger'
15 |
--------------------------------------------------------------------------------
/lib/generators/templates/add_encrypt_message_config_migration.rb:
--------------------------------------------------------------------------------
1 | class AddEncryptMessageConfigColumnsTo<%= table_name.camelize %> < ActiveRecord::Migration
2 | def self.up
3 | change_table(:<%= table_name %>) do |t|
4 | t.string :encoding_aes_key, limit: 43
5 | t.string :app_id
6 | end
7 | end
8 |
9 | def self.down
10 | # By default, we don't want to make any assumption about how to roll back a migration when your
11 | # model already existed. Please edit below which fields you would like to remove in this migration.
12 | raise ActiveRecord::IrreversibleMigration
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware/helpers/unique_token_helper.rb:
--------------------------------------------------------------------------------
1 | module WeixinRailsMiddleware
2 | module UniqueTokenHelper
3 | def self.generate(options = {})
4 | # SecureRandom: hex, base64, random_bytes, urlsafe_base64, random_number, uuid
5 | generator_method_type = options.delete(:generator).try(:to_sym) || :hex
6 | generator_method = SecureRandom.method(generator_method_type)
7 | token_size = options.delete(:size).try(:to_i) || 12
8 | return generator_method.call if generator_method_type == :uuid
9 | generator_method.call(token_size)
10 | end
11 | end
12 | end
13 |
14 | WeiXinUniqueToken = WeixinRailsMiddleware::UniqueTokenHelper
15 |
--------------------------------------------------------------------------------
/lib/generators/templates/add_weixin_secret_key_and_weixin_token_migration.rb:
--------------------------------------------------------------------------------
1 | class AddWeixinSecretKeyAndWeixinTokenTo<%= table_name.camelize %> < ActiveRecord::Migration
2 | def self.up
3 | change_table(:<%= table_name %>) do |t|
4 | t.string :weixin_secret_key
5 | t.string :weixin_token
6 | end
7 | add_index :<%= table_name %>, :weixin_secret_key
8 | add_index :<%= table_name %>, :weixin_token
9 | end
10 |
11 | def self.down
12 | # By default, we don't want to make any assumption about how to roll back a migration when your
13 | # model already existed. Please edit below which fields you would like to remove in this migration.
14 | raise ActiveRecord::IrreversibleMigration
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware/helpers/pkcs7_encoder.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | module WeixinRailsMiddleware
4 | module PKCS7Encoder
5 | extend self
6 |
7 | BLOCK_SIZE = 32
8 |
9 | def decode(text)
10 | pad = text[-1].ord
11 | pad = 0 if (pad < 1 || pad > BLOCK_SIZE)
12 | size = text.size - pad
13 | text[0...size]
14 | end
15 |
16 | # 对需要加密的明文进行填充补位
17 | # 返回补齐明文字符串
18 | def encode(text)
19 | # 计算需要填充的位数
20 | amount_to_pad = BLOCK_SIZE - (text.length % BLOCK_SIZE)
21 | amount_to_pad = BLOCK_SIZE if amount_to_pad == 0
22 | # 获得补位所用的字符
23 | pad_chr = amount_to_pad.chr
24 | "#{text}#{pad_chr * amount_to_pad}"
25 | end
26 |
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware/models/encrypt_message.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | # 标准的回包
3 | #
4 | #
5 | #
6 | # timestamp
7 | #
8 | #
9 |
10 | module WeixinRailsMiddleware
11 | class EncryptMessage
12 | include ROXML
13 | xml_name :xml
14 |
15 | xml_accessor :Encrypt, :cdata => true
16 | xml_accessor :Nonce, :cdata => true
17 | xml_accessor :TimeStamp, :as => Integer
18 | xml_accessor :MsgSignature, :cdata => true
19 |
20 | def to_xml
21 | super.to_xml(:encoding => 'UTF-8', :indent => 0, :save_with => 0)
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | begin
2 | require 'bundler/setup'
3 | rescue LoadError
4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5 | end
6 |
7 | require 'rdoc/task'
8 |
9 | RDoc::Task.new(:rdoc) do |rdoc|
10 | rdoc.rdoc_dir = 'rdoc'
11 | rdoc.title = 'WeixinRailsMiddleware'
12 | rdoc.options << '--line-numbers'
13 | rdoc.rdoc_files.include('README.rdoc')
14 | rdoc.rdoc_files.include('lib/**/*.rb')
15 | end
16 |
17 | APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18 | load 'rails/tasks/engine.rake'
19 |
20 |
21 |
22 | Bundler::GemHelper.install_tasks
23 |
24 | require 'rake/testtask'
25 |
26 | Rake::TestTask.new(:test) do |t|
27 | t.libs << 'lib'
28 | t.libs << 'test'
29 | t.pattern = 'test/**/*_test.rb'
30 | t.verbose = false
31 | end
32 |
33 |
34 | task default: :test
35 |
--------------------------------------------------------------------------------
/weixin_rails_middleware.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("../lib", __FILE__)
2 |
3 | # Maintain your gem's version:
4 | require "weixin_rails_middleware/version"
5 |
6 | # Describe your gem and declare its dependencies:
7 | Gem::Specification.new do |s|
8 | s.name = "weixin_rails_middleware"
9 | s.version = WeixinRailsMiddleware::VERSION
10 | s.authors = ["lanrion"]
11 | s.email = ["huaitao-deng@foxmail.com"]
12 | s.homepage = "http://github.com/lanrion/weixin_rails_middleware"
13 | s.summary = "weixin_rails_middleware for integration weixin"
14 | s.description = "weixin_rails_middleware for integration weixin develop"
15 |
16 | s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"]
17 |
18 | s.add_dependency 'railties', '>= 3.1'
19 | s.add_dependency 'nokogiri', '>= 1.6.1'
20 | s.add_runtime_dependency 'rails', '>= 3.1'
21 |
22 | s.add_dependency 'multi_xml', '>= 0.5.2'
23 | s.add_dependency 'roxml'
24 |
25 | end
26 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware.rb:
--------------------------------------------------------------------------------
1 | require 'roxml'
2 | require 'multi_xml'
3 | require 'ostruct'
4 |
5 | require "weixin_rails_middleware/configuration"
6 | require "weixin_rails_middleware/engine"
7 |
8 | require "weixin_rails_middleware/models/encrypt_message"
9 | require "weixin_rails_middleware/models/message"
10 | require "weixin_rails_middleware/models/reply_message"
11 |
12 | require "weixin_rails_middleware/helpers/prpcrypt"
13 |
14 | require "weixin_rails_middleware/helpers/reply_weixin_message_helper"
15 | require "weixin_rails_middleware/helpers/unique_token_helper"
16 | require "weixin_rails_middleware/helpers/auto_generate_weixin_token_secret_key"
17 |
18 | module WeixinRailsMiddleware
19 |
20 | autoload(:WexinAdapter, "weixin_rails_middleware/adapter/weixin_adapter")
21 | autoload(:SinglePublicAccount, "weixin_rails_middleware/adapter/single_public_account")
22 | autoload(:MultiplePublicAccount, "weixin_rails_middleware/adapter/multiple_public_account")
23 |
24 | DEFAULT_TOKEN_COLUMN_NAME = "weixin_token"
25 | DEFAULT_WEIXIN_SECRET_KEY = "weixin_secret_key"
26 |
27 | end
28 |
29 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2014 YOURNAME
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware/adapter/single_public_account.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | module WeixinRailsMiddleware
3 | class SinglePublicAccount < WexinAdapter
4 |
5 | def check_weixin_legality
6 | return render_authorize_result if !is_weixin_secret_key_valid?
7 | super
8 | end
9 |
10 | def is_weixin_secret_key_valid?
11 | weixin_secret_key == self.class.weixin_secret_string
12 | end
13 |
14 | def current_weixin_token
15 | self.class.weixin_token_string
16 | end
17 |
18 | def current_weixin_public_account
19 | @current_weixin_public_account ||= OpenStruct.new(
20 | weixin_secret_string: self.class.weixin_secret_string,
21 | weixin_token_string: self.class.weixin_token_string,
22 | app_id: self.class.app_id)
23 | @current_weixin_public_account.instance_eval do
24 | def aes_key
25 | WexinAdapter.decode64(WexinAdapter.encoding_aes_key)
26 | end
27 | end
28 | @current_weixin_public_account
29 | end
30 |
31 | def error_msg
32 | "#{__FILE__}:#{__LINE__}: Weixin secret string NotMatch."
33 | end
34 |
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware/adapter/multiple_public_account.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | module WeixinRailsMiddleware
3 | class MultiplePublicAccount < WexinAdapter
4 |
5 | def check_weixin_legality
6 | return render_authorize_result(404) if !is_weixin_secret_key_valid?
7 | super
8 | end
9 |
10 | # check the token from Weixin Service is exist in local store.
11 | def is_weixin_secret_key_valid?
12 | current_weixin_public_account.present?
13 | end
14 |
15 | def current_weixin_token
16 | current_weixin_public_account.try(DEFAULT_TOKEN_COLUMN_NAME)
17 | end
18 |
19 | # TODO: handle Exception
20 | def current_weixin_public_account
21 | @current_weixin_public_account ||= self.class.token_model_class.where("#{DEFAULT_WEIXIN_SECRET_KEY}" => weixin_secret_key).first
22 | @current_weixin_public_account.instance_eval do
23 | def aes_key
24 | WexinAdapter.decode64(encoding_aes_key)
25 | end
26 | end
27 | @current_weixin_public_account
28 | end
29 |
30 | def error_msg
31 | "#{__FILE__}:#{__LINE__}: RecordNotFound - Couldn't find #{self.class.token_model} with weixin_secret_key=#{weixin_secret_key}"
32 | end
33 |
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/generators/weixin_rails_middleware/encrypt_migration_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators/active_record'
2 |
3 | module WeixinRailsMiddleware
4 | module Generators
5 | class EncryptMigrationGenerator < ActiveRecord::Generators::Base
6 | source_root File.expand_path('../../templates', __FILE__)
7 |
8 | desc 'Adds encrypt message config for your application.'
9 | def create_migration_file
10 | if !migration_exists?(table_name)
11 | migration_template "add_encrypt_message_config_migration.rb", "db/migrate/add_encrypt_message_config_columns_to_#{plural_name}.rb"
12 | end
13 | end
14 |
15 | private
16 |
17 | def model_exists?
18 | File.exists?(File.join(destination_root, model_path))
19 | end
20 |
21 | def model_path
22 | @model_path ||= File.join("app", "models", "#{file_path}.rb")
23 | end
24 |
25 | def migration_exists?(table_name)
26 | Dir.glob("#{File.join(destination_root, migration_path)}/[0-9]*_*.rb").grep(/\d+_add_encrypt_message_config_columns_to_#{table_name}.rb/).first
27 | end
28 |
29 | def migration_path
30 | @migration_path ||= File.join("db", "migrate")
31 | end
32 |
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/generators/weixin_rails_middleware/install_generator.rb:
--------------------------------------------------------------------------------
1 | # Rails::Generators::Base dont need a name
2 | # Rails::Generators::NamedBase need a name
3 | module WeixinRailsMiddleware
4 | module Generators
5 | class InstallGenerator < Rails::Generators::Base
6 | source_root File.expand_path('../../templates', __FILE__)
7 |
8 | desc 'Creates a WeixinRailsMiddleware initializer for your application.'
9 |
10 | def install
11 | route 'mount WeixinRailsMiddleware::Engine, at: "/"'
12 | end
13 |
14 | def copy_initializer
15 | template 'install_weixin_rails_middleware.rb', 'config/initializers/weixin_rails_middleware.rb'
16 | end
17 |
18 | def configure_application
19 | application <<-APP
20 | config.to_prepare do
21 | # Load application's model / class decorators
22 | Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")) do |c|
23 | Rails.configuration.cache_classes ? require(c) : load(c)
24 | end
25 | end
26 | APP
27 | end
28 |
29 | def copy_decorators
30 | template 'weixin_controller.rb', 'app/decorators/controllers/weixin_rails_middleware/weixin_controller_decorator.rb'
31 | end
32 |
33 | def show_readme
34 | readme "README" if behavior == :invoke
35 | end
36 |
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware/configuration.rb:
--------------------------------------------------------------------------------
1 | module WeixinRailsMiddleware
2 |
3 | class << self
4 |
5 | attr_accessor :configuration
6 |
7 | def config
8 | self.configuration ||= Configuration.new
9 | end
10 |
11 | def configure
12 | yield config if block_given?
13 | end
14 |
15 | end
16 |
17 | class Configuration
18 | attr_accessor :public_account_class
19 | attr_accessor :weixin_secret_string, :weixin_token_string
20 | # 加密参数配置
21 | attr_accessor :encoding_aes_key, :app_id
22 |
23 | # 自定义场景
24 | attr_accessor :custom_adapter
25 | end
26 |
27 | module ConfigurationHelpers
28 | extend ActiveSupport::Concern
29 |
30 | [:weixin_secret_string, :weixin_token_string, :encoding_aes_key, :app_id].each do |attr_name|
31 | define_method attr_name do
32 | WeixinRailsMiddleware.config.send(attr_name).to_s
33 | end
34 | end
35 |
36 | def token_model
37 | @public_account_class ||= WeixinRailsMiddleware.config.public_account_class
38 | end
39 |
40 | def token_model_class
41 | if token_model.blank?
42 | raise "You need to config `public_account_class` in 'config/initializers/weixin_rails_middleware.rb'"
43 | end
44 | @token_model_class_name ||= token_model.to_s.constantize
45 | end
46 |
47 | def custom_adapter
48 | @custom_adapter ||= WeixinRailsMiddleware.config.custom_adapter.to_s
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/generators/templates/install_weixin_rails_middleware.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | # Use this hook to configure WeixinRailsMiddleware bahaviors.
3 | WeixinRailsMiddleware.configure do |config|
4 |
5 | ## NOTE:
6 | ## If you config all them, it will use `weixin_token_string` default
7 |
8 | ## Config public_account_class if you SAVE public_account into database ##
9 | # Th first configure is fit for your weixin public_account is saved in database.
10 | # +public_account_class+ The class name that to save your public_account
11 | # config.public_account_class = "PublicAccount"
12 |
13 | ## Here configure is for you DON'T WANT TO SAVE your public account into database ##
14 | # Or the other configure is fit for only one weixin public_account
15 | # If you config `weixin_token_string`, so it will directly use it
16 | # config.weixin_token_string = '<%= SecureRandom.hex(12) %>'
17 | # using to weixin server url to validate the token can be trusted.
18 | # config.weixin_secret_string = '<%= WeiXinUniqueToken.generate(generator: :urlsafe_base64, size: 24) %>'
19 | # 加密配置,如果需要加密,配置以下参数
20 | # config.encoding_aes_key = '<%= WeiXinUniqueToken.generate(generator: :hex, size: 22)[1..43] %>'
21 | # config.app_id = "your app id"
22 |
23 | ## You can custom your adapter to validate your weixin account ##
24 | # Wiki https://github.com/lanrion/weixin_rails_middleware/wiki/Custom-Adapter
25 | # config.custom_adapter = "MyCustomAdapter"
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware/helpers/prpcrypt.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require "weixin_rails_middleware/helpers/pkcs7_encoder"
3 | module WeixinRailsMiddleware
4 | module Prpcrypt
5 | extend self
6 |
7 | # 对密文进行解密.
8 | # text 需要解密的密文
9 | def decrypt(aes_key, text, app_id)
10 | status = 200
11 | text = Base64.decode64(text)
12 | text = handle_cipher(:decrypt, aes_key, text)
13 | result = PKCS7Encoder.decode(text)
14 | content = result[16...result.length]
15 | len_list = content[0...4].unpack("N")
16 | xml_len = len_list[0]
17 | xml_content = content[4...4 + xml_len]
18 | from_app_id = content[xml_len + 4...content.size]
19 | # TODO: refactor
20 | if app_id != from_app_id
21 | Rails.logger.debug("#{__FILE__}:#{__LINE__} Failure because app_id != from_app_id")
22 | status = 401
23 | end
24 | [xml_content, status]
25 | end
26 |
27 | # 加密
28 | def encrypt(aes_key, text, app_id)
29 | text = text.force_encoding("ASCII-8BIT")
30 | random = SecureRandom.hex(8)
31 | msg_len = [text.length].pack("N")
32 | text = "#{random}#{msg_len}#{text}#{app_id}"
33 | text = PKCS7Encoder.encode(text)
34 | text = handle_cipher(:encrypt, aes_key, text)
35 | Base64.encode64(text)
36 | end
37 |
38 | private
39 | def handle_cipher(action, aes_key, text)
40 | cipher = OpenSSL::Cipher.new('AES-256-CBC')
41 | cipher.send(action)
42 | cipher.padding = 0
43 | cipher.key = aes_key
44 | cipher.iv = aes_key[0...16]
45 | cipher.update(text) + cipher.final
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/generators/weixin_rails_middleware/migration_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators/active_record'
2 |
3 | module WeixinRailsMiddleware
4 | module Generators
5 | class MigrationGenerator < ActiveRecord::Generators::Base
6 | source_root File.expand_path('../../templates', __FILE__)
7 |
8 | desc 'Adds a Wexin Secret Key for your application.'
9 | def create_migration_file
10 | if !migration_exists?(table_name)
11 | migration_template "add_weixin_secret_key_and_weixin_token_migration.rb", "db/migrate/add_weixin_secret_key_and_weixin_token_to_#{plural_name}.rb"
12 | end
13 | end
14 |
15 | def inject_model_content
16 |
17 | content = <<-CONTENT
18 | # It will auto generate weixin token and secret
19 | include WeixinRailsMiddleware::AutoGenerateWeixinTokenSecretKey
20 |
21 | CONTENT
22 |
23 | class_path = if namespaced?
24 | class_name.to_s.split("::")
25 | else
26 | [class_name]
27 | end
28 |
29 | indent_depth = class_path.size - 1
30 | content = content.split("\n").map { |line| " " * indent_depth + line } .join("\n") << "\n"
31 |
32 | inject_into_class(model_path, class_path.last, content) if model_exists?
33 | end
34 |
35 | private
36 |
37 | def model_exists?
38 | File.exists?(File.join(destination_root, model_path))
39 | end
40 |
41 | def model_path
42 | @model_path ||= File.join("app", "models", "#{file_path}.rb")
43 | end
44 |
45 | def migration_exists?(table_name)
46 | Dir.glob("#{File.join(destination_root, migration_path)}/[0-9]*_*.rb").grep(/\d+_add_weixin_secret_key_and_weixin_token_to_#{table_name}.rb/).first
47 | end
48 |
49 | def migration_path
50 | @migration_path ||= File.join("db", "migrate")
51 | end
52 |
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/app/controllers/weixin_rails_middleware/weixin_controller.rb:
--------------------------------------------------------------------------------
1 | module WeixinRailsMiddleware
2 | class WeixinController < ActionController::Base
3 | include ReplyWeixinMessageHelper
4 |
5 | skip_before_action :verify_authenticity_token
6 |
7 | before_action :check_is_encrypt, only: [:index, :reply]
8 | before_action :initialize_adapter, :check_weixin_legality, only: [:index, :reply]
9 | before_action :set_weixin_public_account, :set_weixin_message, only: :reply
10 | before_action :set_keyword, only: :reply
11 |
12 | def index
13 | end
14 |
15 | def reply
16 | end
17 |
18 | protected
19 |
20 | # 如果url上无encrypt_type或者其值为raw,则回复明文,否则按照上述的加密算法加密回复密文。
21 | def check_is_encrypt
22 | if params[:encrypt_type].blank? || params[:encrypt_type] == "raw"
23 | @is_encrypt = false
24 | else
25 | @is_encrypt = true
26 | end
27 | end
28 |
29 | def initialize_adapter
30 | @weixin_adapter ||= WexinAdapter.init_with(params)
31 | end
32 |
33 | def check_weixin_legality
34 | check_result = @weixin_adapter.check_weixin_legality
35 | valid = check_result.delete(:valid)
36 | render check_result if action_name == "index"
37 | return valid
38 | end
39 |
40 | ## Callback
41 | # e.g. will generate +@weixin_public_account+
42 | def set_weixin_public_account
43 | @weixin_public_account ||= @weixin_adapter.current_weixin_public_account
44 | end
45 |
46 | def set_weixin_message
47 | param_xml = request.body.read
48 | if @is_encrypt
49 | hash = MultiXml.parse(param_xml)['xml']
50 | @body_xml = OpenStruct.new(hash)
51 | param_xml = Prpcrypt.decrypt(@weixin_public_account.aes_key,
52 | @body_xml.Encrypt,
53 | @weixin_public_account.app_id
54 | )[0]
55 | end
56 | # Get the current weixin message
57 | @weixin_message ||= Message.factory(param_xml)
58 | end
59 |
60 | def set_keyword
61 | @keyword = @weixin_message.Content || # 文本消息
62 | @weixin_message.EventKey || # 事件推送
63 | @weixin_message.Recognition # 接收语音识别结果
64 | end
65 |
66 | # http://apidock.com/rails/ActionController/Base/default_url_options
67 | def default_url_options(options={})
68 | { weichat_id: @weixin_message.FromUserName }
69 | end
70 |
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | weixin_rails_middleware (1.0.5)
5 | multi_json (>= 1.9.0)
6 | multi_xml (>= 0.5.2)
7 | nokogiri (>= 1.6.1)
8 | rails (>= 3.1)
9 | railties (>= 3.1)
10 | roxml
11 |
12 | GEM
13 | remote: http://rubygems.org/
14 | specs:
15 | actionmailer (4.0.3)
16 | actionpack (= 4.0.3)
17 | mail (~> 2.5.4)
18 | actionpack (4.0.3)
19 | activesupport (= 4.0.3)
20 | builder (~> 3.1.0)
21 | erubis (~> 2.7.0)
22 | rack (~> 1.5.2)
23 | rack-test (~> 0.6.2)
24 | activemodel (4.0.3)
25 | activesupport (= 4.0.3)
26 | builder (~> 3.1.0)
27 | activerecord (4.0.3)
28 | activemodel (= 4.0.3)
29 | activerecord-deprecated_finders (~> 1.0.2)
30 | activesupport (= 4.0.3)
31 | arel (~> 4.0.0)
32 | activerecord-deprecated_finders (1.0.3)
33 | activesupport (4.0.3)
34 | i18n (~> 0.6, >= 0.6.4)
35 | minitest (~> 4.2)
36 | multi_json (~> 1.3)
37 | thread_safe (~> 0.1)
38 | tzinfo (~> 0.3.37)
39 | arel (4.0.2)
40 | atomic (1.1.16)
41 | builder (3.1.4)
42 | erubis (2.7.0)
43 | hike (1.2.3)
44 | i18n (0.6.9)
45 | mail (2.5.4)
46 | mime-types (~> 1.16)
47 | treetop (~> 1.4.8)
48 | mime-types (1.25.1)
49 | mini_portile (0.5.2)
50 | minitest (4.7.5)
51 | multi_json (1.9.2)
52 | multi_xml (0.5.5)
53 | nokogiri (1.6.1)
54 | mini_portile (~> 0.5.0)
55 | polyglot (0.3.4)
56 | rack (1.5.2)
57 | rack-test (0.6.2)
58 | rack (>= 1.0)
59 | rails (4.0.3)
60 | actionmailer (= 4.0.3)
61 | actionpack (= 4.0.3)
62 | activerecord (= 4.0.3)
63 | activesupport (= 4.0.3)
64 | bundler (>= 1.3.0, < 2.0)
65 | railties (= 4.0.3)
66 | sprockets-rails (~> 2.0.0)
67 | railties (4.0.3)
68 | actionpack (= 4.0.3)
69 | activesupport (= 4.0.3)
70 | rake (>= 0.8.7)
71 | thor (>= 0.18.1, < 2.0)
72 | rake (10.1.1)
73 | roxml (3.3.1)
74 | activesupport (>= 2.3.0)
75 | nokogiri (>= 1.3.3)
76 | sprockets (2.11.0)
77 | hike (~> 1.2)
78 | multi_json (~> 1.0)
79 | rack (~> 1.0)
80 | tilt (~> 1.1, != 1.3.0)
81 | sprockets-rails (2.0.1)
82 | actionpack (>= 3.0)
83 | activesupport (>= 3.0)
84 | sprockets (~> 2.8)
85 | thor (0.18.1)
86 | thread_safe (0.2.0)
87 | atomic (>= 1.1.7, < 2)
88 | tilt (1.4.1)
89 | treetop (1.4.15)
90 | polyglot
91 | polyglot (>= 0.3.1)
92 | tzinfo (0.3.39)
93 |
94 | PLATFORMS
95 | ruby
96 |
97 | DEPENDENCIES
98 | weixin_rails_middleware!
99 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware/adapter/weixin_adapter.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | module WeixinRailsMiddleware
3 | class WexinAdapter
4 | extend ConfigurationHelpers
5 |
6 | attr_accessor :signature, :timestamp, :nonce, :echostr
7 | attr_accessor :weixin_secret_key
8 |
9 | def initialize(weixin_params)
10 | @weixin_secret_key = weixin_params[:weixin_secret_key]
11 | # 以下参数为什么加空字符串默认值的原因:
12 | # 微信偶尔会再重新发一次get请求,但是不会带上signature,timestamp,nonce的参数
13 | @signature = weixin_params[:signature] || ''
14 | @timestamp = weixin_params[:timestamp] || ''
15 | @nonce = weixin_params[:nonce] || ''
16 | @echostr = weixin_params[:echostr] || ''
17 | end
18 |
19 | def self.init_with(weixin_params)
20 | if custom_adapter.present?
21 | if custom_adapter.constantize.superclass != self
22 | raise "#{custom_adapter.to_s} must inherite WexinAdapter"
23 | end
24 | return custom_adapter.constantize.new(weixin_params)
25 | end
26 | if weixin_token_string.present?
27 | SinglePublicAccount.new(weixin_params)
28 | else
29 | MultiplePublicAccount.new(weixin_params)
30 | end
31 | end
32 |
33 | def check_weixin_legality
34 | return render_authorize_result(401, self.class.error_msg) if !is_signature_valid?
35 | render_authorize_result(200, echostr, true)
36 | end
37 |
38 | def is_signature_valid?
39 | sort_params = [current_weixin_token, timestamp, nonce].sort.join
40 | current_signature = Digest::SHA1.hexdigest(sort_params)
41 | return true if current_signature == signature
42 | false
43 | end
44 |
45 | def current_weixin_public_account
46 | raise NotImplementedError, "Subclasses must implement current_weixin_public_account method"
47 | end
48 |
49 | def current_weixin_token
50 | raise NotImplementedError, "Subclasses must implement current_weixin_token method"
51 | end
52 |
53 | def is_weixin_secret_key_valid?
54 | raise NotImplementedError, "Subclasses must implement is_weixin_secret_key_valid? method"
55 | end
56 |
57 | class << self
58 | def error_msg
59 | "#{__FILE__}:#{__LINE__}: Weixin signature NotMatch"
60 | end
61 |
62 | def decode64(encoding_aes)
63 | Base64.decode64("#{encoding_aes}=")
64 | end
65 | end
66 |
67 | private
68 |
69 | # render weixin server authorize results
70 | def render_authorize_result(status=401, text=nil, valid=false)
71 | text = text || error_msg
72 | Rails.logger.error(text) if status != 200
73 | {plain: text, status: status, valid: valid}
74 | end
75 |
76 | def error_msg
77 | self.class.error_msg
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WeixinRailsMiddleware
2 |
3 | [](http://badge.fury.io/rb/weixin_rails_middleware)
4 |
5 | 该项目使用MIT-LICENSE.
6 |
7 | https://rubygems.org/gems/weixin_rails_middleware
8 |
9 | **已经实现消息体签名及加解密,升级与使用,详见[Wiki 实现消息体签名及加解密](https://github.com/lanrion/weixin_rails_middleware/wiki/msg-encryption-decipher)**
10 |
11 | ## 微信企业版本
12 |
13 | https://github.com/lanrion/qy_wechat
14 |
15 | https://github.com/lanrion/qy_wechat_api
16 |
17 | ## 使用特别说明
18 |
19 | ### 支持Rails版本
20 |
21 | 已经支持 Rails 3,Rails 4,Rails 5,Rails 6
22 |
23 | ## 参考示例
24 |
25 | Rails 4: https://github.com/lanrion/weixin_rails_middleware_example
26 |
27 | Rails 3: https://github.com/lanrion/weixin_rails_3
28 |
29 | ### 相关gem推荐使用
30 |
31 | * **微信高级功能** 请务必结合高级API实现:[weixin_authorize](https://github.com/lanrion/weixin_authorize)
32 |
33 | * **Wap Ratchet 框架** 推荐使用: [twitter_ratchet_rails](https://github.com/lanrion/twitter_ratchet_rails)
34 |
35 | ## [查看 Wiki:](https://github.com/lanrion/weixin_rails_middleware/wiki)
36 |
37 | * [Getting Start](https://github.com/lanrion/weixin_rails_middleware/wiki/Getting-Start)
38 |
39 | * [实现自定义菜单](https://github.com/lanrion/weixin_rails_middleware/wiki/DIY-menu)
40 |
41 | * [生成微信信息使用方法](https://github.com/lanrion/weixin_rails_middleware/wiki/Generate-message-helpers)
42 |
43 | ## 使用公司列表
44 |
45 | 如果您或者您的公司正在使用当中,欢迎加入此列表:
46 |
47 | https://github.com/lanrion/weixin_rails_middleware/wiki/gem-users-list
48 |
49 | ## 实现功能
50 |
51 | * 自动验证微信请求。
52 |
53 | * 无需拼接XML格式,只需要使用 `ReplyWeixinMessageHelper` 辅助方法,即可快速回复。
54 | 使用方法: ` render xml: reply_text_message("Your Message: #{current_message.Content}") `
55 |
56 | * 支持自定义token,适合一个用户使用。
57 |
58 | * 支持多用户token: 适合多用户注册网站,每个用户有不同的token,通过 `weixin_rails_middleware.rb` 配置好存储token的Model与字段名,即可。
59 |
60 | * 文本回复: `reply_text_message(content)`。
61 |
62 | * 音乐回复: `reply_music_message(music)`, `generate_music(title, desc, music_url, hq_music_url)`。
63 |
64 | * 图文回复: `reply_news_message(articles)`, `generate_article(title, desc, pic_url, link_url)`。
65 |
66 | * 视频回复: `reply_video_message(video)`。
67 |
68 | * 语音回复: `reply_voice_message(voice)`。
69 |
70 | * 图片回复: `reply_image_message(image)`。
71 |
72 | * 地理位置回复: 自定义需求。
73 |
74 | * 其他高级API实现:[weixin_authorize](https://github.com/lanrion/weixin_authorize)
75 |
76 | ## 如何测试?
77 |
78 | 安装 [ngrok](https://ngrok.com),解压后跑 `ngrok 4000`
79 |
80 | 然后会产生以下信息:
81 |
82 | ```
83 | Tunnel Status online
84 | Version 1.6/1.5
85 | Forwarding http://e0ede89.ngrok.com -> 127.0.0.1:4000
86 | Forwarding https://e0ede89.ngrok.com -> 127.0.0.1:4000
87 | Web Interface 127.0.0.1:4040
88 | # Conn 67
89 | Avg Conn Time 839.50ms
90 |
91 | ```
92 |
93 | 域名为 `http://e0ede89.ngrok.com`。 注意非付费版本域名每次会随机生成,不是固定的。
94 |
95 |
96 | **Ngrok已墙,你懂得的**,ngrok 已墙,请使用localtunnel.me,使用方法:
97 |
98 | `npm install -g localtunnel`
99 | ```sh
100 | $ lt --port 8000
101 | # your url is: https://gqgh.localtunnel.me
102 | ```
103 |
104 | ## 贡献你的代码
105 |
106 | 1. Fork 项目
107 | 2. 创建自己的功能分支 (`git checkout -b my-new-feature`).
108 | 3. 提交你的修改 (`git commit -am 'Add some feature'`).
109 | 4. 推荐到远程分支 (`git push origin my-new-feature`).
110 | 5. 提交PR审核.
111 | 6. 可使用 [weixin_rails_middleware_example](https://github.com/lanrion/weixin_rails_middleware_example), 来测试
112 |
113 | ## Bugs 和反馈
114 |
115 | 如果你发现有出现任何的bug,请在 https://github.com/lanrion/weixin_rails_middleware/issues 记录你的bug详细信息,
116 |
117 | 或者在 [Ruby China](http://ruby-china.org/) 开帖 [@ruby_sky](http://ruby-china.org/ruby_sky), 个人邮箱回复速度相对慢.
118 |
119 | ## 推荐阅读
120 |
121 | * [浅析微信信息信息接收与信息回复](https://gist.github.com/lanrion/9479631)
122 |
123 | ## 参考致谢
124 | 在微信回复信息XML的封装方法,借鉴了 [rack-weixin](https://github.com/wolfg1969/rack-weixin) 实现,特此感谢!
125 |
126 | ## 捐赠支持
127 |
128 | 如果你觉得我的gem对你有帮助,欢迎打赏支持,:smile:
129 |
130 | 
131 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware/models/message.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | # multi_xml will use Nokogiri if it is available
3 | MultiXml.parser = :nokogiri
4 |
5 | module WeixinRailsMiddleware
6 |
7 | class Message
8 |
9 | def initialize(hash)
10 | @source = OpenStruct.new(hash)
11 | end
12 |
13 | def method_missing(method, *args, &block)
14 | @source.send(method, *args, &block)
15 | end
16 |
17 | def CreateTime
18 | @source.CreateTime.to_i
19 | end
20 |
21 | def MsgId
22 | @source.MsgId.to_i
23 | end
24 |
25 | def self.factory(xml)
26 | hash = MultiXml.parse(xml)['xml']
27 | case hash['MsgType']
28 | when 'text'
29 | TextMessage.new(hash)
30 | when 'image'
31 | ImageMessage.new(hash)
32 | when 'location'
33 | LocationMessage.new(hash)
34 | when 'link'
35 | LinkMessage.new(hash)
36 | when 'event'
37 | EventMessage.new(hash)
38 | when 'voice'
39 | VoiceMessage.new(hash)
40 | when 'video'
41 | VideoMessage.new(hash)
42 | when 'shortvideo'
43 | ShortVideo.new(hash)
44 | else
45 | raise ArgumentError, 'Unknown Weixin Message'
46 | end
47 | end
48 |
49 | end
50 |
51 | #
52 | #
53 | #
54 | # 1348831860
55 | #
56 | #
57 | # 1234567890123456
58 | #
59 | TextMessage = Class.new(Message)
60 |
61 | #
62 | #
63 | #
64 | # 1348831860
65 | #
66 | #
67 | # 1234567890123456
68 | #
69 | ImageMessage = Class.new(Message)
70 |
71 | #
72 | #
73 | #
74 | # 1351776360
75 | #
76 | #
77 | #
78 | #
79 | # 1234567890123456
80 | #
81 | LinkMessage = Class.new(Message)
82 |
83 | #
84 | #
85 | #
86 | # 123456789
87 | #
88 | #
89 | #
90 | #
91 | EventMessage = Class.new(Message)
92 |
93 | #
94 | #
95 | #
96 | # 1351776360
97 | #
98 | # 23.134521
99 | # 113.358803
100 | # 20
101 | #
102 | # 1234567890123456
103 | #
104 | class LocationMessage < Message
105 |
106 | def Location_X
107 | @source.Location_X.to_f
108 | end
109 |
110 | def Location_Y
111 | @source.Location_Y.to_f
112 | end
113 |
114 | def Scale
115 | @source.Scale.to_i
116 | end
117 | end
118 |
119 | #
120 | #
121 | #
122 | # 1376632760
123 | #
124 | #
125 | #
126 | # 5912592682802219078
127 | #
128 | #
129 | class VoiceMessage < Message
130 |
131 | def MediaId
132 | @source.MediaId
133 | end
134 |
135 | def Format
136 | @source.Format
137 | end
138 | end
139 |
140 | #
141 | #
142 | #
143 | # 1376632994
144 | #
145 | #
146 | #
147 | # 5912593687824566343
148 | #
149 | class VideoMessage < Message
150 |
151 | def MediaId
152 | @source.MediaId
153 | end
154 |
155 | def ThumbMediaId
156 | @source.ThumbMediaId
157 | end
158 | end
159 |
160 | #
161 | #
162 | #
163 | # 1357290913
164 | #
165 | #
166 | #
167 | # 1234567890123456
168 | #
169 |
170 | class ShortVideo < Message
171 |
172 | def MediaId
173 | @source.MediaId
174 | end
175 |
176 | def ThumbMediaId
177 | @source.ThumbMediaId
178 | end
179 | end
180 |
181 | end
182 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware/helpers/reply_weixin_message_helper.rb:
--------------------------------------------------------------------------------
1 | module WeixinRailsMiddleware
2 | module ReplyWeixinMessageHelper
3 |
4 | # e.g.
5 | # reply_text_message(@weixin_message.ToUserName, @weixin_message.FromUserName, "Your Message: #{@weixin_message.Content}")
6 | # Or reply_text_message("Your Message: #{@weixin_message.Content}")
7 | def reply_text_message(from=nil, to=nil, content)
8 | message = TextReplyMessage.new
9 | message.FromUserName = from || @weixin_message.ToUserName
10 | message.ToUserName = to || @weixin_message.FromUserName
11 | message.Content = content
12 | encrypt_message message.to_xml
13 | end
14 |
15 | def generate_music(title, desc, music_url, hq_music_url)
16 | music = Music.new
17 | music.Title = title
18 | music.Description = desc
19 | music.MusicUrl = music_url
20 | music.HQMusicUrl = hq_music_url
21 | music
22 | end
23 |
24 | # music = generate_music
25 | def reply_music_message(from=nil, to=nil, music)
26 | message = MusicReplyMessage.new
27 | message.FromUserName = from || @weixin_message.ToUserName
28 | message.ToUserName = to || @weixin_message.FromUserName
29 | message.Music = music
30 | encrypt_message message.to_xml
31 | end
32 |
33 | def generate_article(title, desc, pic_url, link_url)
34 | item = Article.new
35 | item.Title = title
36 | item.Description = desc
37 | item.PicUrl = pic_url
38 | item.Url = link_url
39 | item
40 | end
41 |
42 | # articles = [generate_article]
43 | def reply_news_message(from=nil, to=nil, articles)
44 | message = NewsReplyMessage.new
45 | message.FromUserName = from || @weixin_message.ToUserName
46 | message.ToUserName = to || @weixin_message.FromUserName
47 | message.Articles = articles
48 | message.ArticleCount = articles.count
49 | encrypt_message message.to_xml
50 | end
51 |
52 | def generate_video(media_id, desc, title)
53 | video = Video.new
54 | video.MediaId = media_id
55 | video.Title = title
56 | video.Description = desc
57 | video
58 | end
59 |
60 | #
61 | #
62 | #
63 | # 12345678
64 | #
65 | #
70 | #
71 |
72 | def reply_video_message(from=nil, to=nil, video)
73 | message = VideoReplyMessage.new
74 | message.FromUserName = from || @weixin_message.ToUserName
75 | message.ToUserName = to || @weixin_message.FromUserName
76 | message.Video = video
77 | encrypt_message message.to_xml
78 | end
79 |
80 | def generate_voice(media_id)
81 | voice = Voice.new
82 | voice.MediaId = media_id
83 | voice
84 | end
85 |
86 | def reply_voice_message(from=nil, to=nil, voice)
87 | message = VoiceReplyMessage.new
88 | message.FromUserName = from || @weixin_message.ToUserName
89 | message.ToUserName = to || @weixin_message.FromUserName
90 | message.Voice = voice
91 | encrypt_message message.to_xml
92 | end
93 |
94 | def generate_image(media_id)
95 | image = Image.new
96 | image.MediaId = media_id
97 | image
98 | end
99 |
100 | def reply_image_message(from=nil, to=nil, image)
101 | message = ImageReplyMessage.new
102 | message.FromUserName = from || @weixin_message.ToUserName
103 | message.ToUserName = to || @weixin_message.FromUserName
104 | message.Image = image
105 | encrypt_message message.to_xml
106 | end
107 |
108 | # 指定会话接入的客服账号
109 | def generate_kf_trans_info(kf_account)
110 | trans_info = KfTransInfo.new
111 | trans_info.KfAccount = kf_account
112 | trans_info
113 | end
114 |
115 | # 消息转发到多客服
116 | # 消息转发到指定客服
117 | def reply_transfer_customer_service_message(from=nil, to=nil, kf_account=nil)
118 | if kf_account.blank?
119 | message = TransferCustomerServiceReplyMessage.new
120 | else
121 | message = TransferCustomerServiceWithTransInfoReplyMessage.new
122 | message.TransInfo = generate_kf_trans_info(kf_account)
123 | end
124 | message.FromUserName = from || @weixin_message.ToUserName
125 | message.ToUserName = to || @weixin_message.FromUserName
126 | encrypt_message message.to_xml
127 | end
128 |
129 | private
130 |
131 | def encrypt_message(msg_xml)
132 | return msg_xml if !@is_encrypt
133 | # 加密回复的XML
134 | encrypt_xml = Prpcrypt.encrypt(@weixin_public_account.aes_key, msg_xml, @weixin_public_account.app_id).gsub("\n","")
135 | # 标准的回包
136 | generate_encrypt_message(encrypt_xml)
137 | end
138 |
139 | def generate_encrypt_message(encrypt_xml)
140 | msg = EncryptMessage.new
141 | msg.Encrypt = encrypt_xml
142 | msg.TimeStamp = Time.now.to_i.to_s
143 | msg.Nonce = SecureRandom.hex(8)
144 | msg.MsgSignature = generate_msg_signature(encrypt_xml, msg)
145 | msg.to_xml
146 | end
147 |
148 | # dev_msg_signature=sha1(sort(token、timestamp、nonce、msg_encrypt))
149 | # 生成企业签名
150 | def generate_msg_signature(encrypt_msg, msg)
151 | sort_params = [encrypt_msg, @weixin_adapter.current_weixin_token,
152 | msg.TimeStamp, msg.Nonce].sort.join
153 | Digest::SHA1.hexdigest(sort_params)
154 | end
155 | end
156 | end
157 |
--------------------------------------------------------------------------------
/lib/weixin_rails_middleware/models/reply_message.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | # ref: https://github.com/wolfg1969/rack-weixin/lib/weixin/model.rb
3 | require 'roxml'
4 |
5 | module WeixinRailsMiddleware
6 |
7 | class ReplyMessage
8 | include ROXML
9 | xml_name :xml
10 |
11 | xml_accessor :ToUserName, :cdata => true
12 | xml_accessor :FromUserName, :cdata => true
13 | xml_reader :CreateTime, :as => Integer
14 | xml_reader :MsgType, :cdata => true
15 |
16 | def initialize
17 | @CreateTime = Time.now.to_i
18 | end
19 |
20 | def to_xml
21 | super.to_xml(:encoding => 'UTF-8', :indent => 0, :save_with => 0)
22 | end
23 | end
24 |
25 | #
26 | #
27 | #
28 | # 12345678
29 | #
30 | #
31 | #
32 |
33 | class TextReplyMessage < ReplyMessage
34 | xml_accessor :Content, :cdata => true
35 | def initialize
36 | super
37 | @MsgType = 'text'
38 | end
39 | end
40 |
41 | class Music
42 | include ROXML
43 | xml_accessor :Title, :cdata => true
44 | xml_accessor :Description, :cdata => true
45 | xml_accessor :MusicUrl, :cdata => true
46 | xml_accessor :HQMusicUrl, :cdata => true
47 | end
48 |
49 | #
50 | #
51 | #
52 | # 12345678
53 | #
54 | #
55 | #
56 | #
57 | #
58 | #
59 | #
60 | #
61 | #
62 |
63 | class MusicReplyMessage < ReplyMessage
64 | xml_accessor :Music, :as => Music
65 | def initialize
66 | super
67 | @MsgType = 'music'
68 | end
69 | end
70 |
71 | class Article
72 | include ROXML
73 | xml_accessor :Title, :cdata => true
74 | xml_accessor :Description, :cdata => true
75 | xml_accessor :PicUrl, :cdata => true
76 | xml_accessor :Url, :cdata => true
77 | end
78 |
79 | #
80 | #
81 | #
82 | # 12345678
83 | #
84 | # 2
85 | #
86 | # -
87 | #
88 | #
89 | #
90 | #
91 | #
92 | # -
93 | #
94 | #
95 | #
96 | #
97 | #
98 | #
99 | #
100 |
101 | class NewsReplyMessage < ReplyMessage
102 | xml_accessor :ArticleCount, :as => Integer
103 | xml_accessor :Articles, :as => [Article], :in => 'Articles', :from => 'item'
104 | def initialize
105 | super
106 | @MsgType = 'news'
107 | end
108 | end
109 |
110 | #
111 | #
112 | #
113 | # 12345678
114 | #
115 | #
120 | #
121 |
122 | class Video
123 | include ROXML
124 | xml_accessor :MediaId, :cdata => true
125 | xml_accessor :Description, :cdata => true
126 | xml_accessor :Title, :cdata => true
127 | end
128 |
129 | class VideoReplyMessage < ReplyMessage
130 | xml_accessor :Video, :as => Video
131 | def initialize
132 | super
133 | @MsgType = 'video'
134 | end
135 | end
136 |
137 | #
138 | #
139 | #
140 | # 12345678
141 | #
142 | #
143 | #
144 | #
145 | #
146 | class Voice
147 | include ROXML
148 | xml_accessor :MediaId, :cdata => true
149 | end
150 |
151 | class VoiceReplyMessage < ReplyMessage
152 | xml_accessor :Voice, :as => Voice
153 | def initialize
154 | super
155 | @MsgType = 'voice'
156 | end
157 | end
158 |
159 | #
160 | #
161 | #
162 | # 12345678
163 | #
164 | #
165 | #
166 | #
167 | #
168 |
169 | class Image
170 | include ROXML
171 | xml_accessor :MediaId, :cdata => true
172 | end
173 |
174 | class ImageReplyMessage < ReplyMessage
175 | xml_accessor :Image, :as => Image
176 | def initialize
177 | super
178 | @MsgType = 'image'
179 | end
180 | end
181 |
182 | #
183 | #
184 | #
185 | # 1399197672
186 | #
187 | #
188 | class TransferCustomerServiceReplyMessage < ReplyMessage
189 | def initialize
190 | super
191 | @MsgType = 'transfer_customer_service'
192 | end
193 | end
194 |
195 | # 指定会话接入的客服账号
196 | class KfTransInfo
197 | include ROXML
198 | xml_accessor :KfAccount, :cdata => true
199 | end
200 |
201 | #
202 | #
203 | #
204 | # 1399197672
205 | #
206 | #
207 | # test1@test
208 | #
209 | #
210 | class TransferCustomerServiceWithTransInfoReplyMessage < ReplyMessage
211 | xml_accessor :TransInfo, :as => KfTransInfo
212 | def initialize
213 | super
214 | @MsgType = 'transfer_customer_service'
215 | end
216 | end
217 |
218 | end
219 |
--------------------------------------------------------------------------------
/lib/generators/templates/weixin_controller.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | # 1, @weixin_message: 获取微信所有参数.
3 | # 2, @weixin_public_account: 如果配置了public_account_class选项,则会返回当前实例,否则返回nil.
4 | # 3, @keyword: 目前微信只有这三种情况存在关键字: 文本消息, 事件推送, 接收语音识别结果
5 |
6 | # 兼容Rails6.0的目录加载机制
7 | # https://github.com/fxn/zeitwerk
8 | begin
9 | class Controllers::WeixinRailsMiddleware::WeixinControllerDecorator;end
10 | rescue
11 | end
12 |
13 | WeixinRailsMiddleware::WeixinController.class_eval do
14 |
15 | def reply
16 | result = send("response_#{@weixin_message.MsgType}_message", {})
17 | render result.is_a?(String) ? {xml: result} : {nothing: true}
18 | end
19 |
20 | private
21 |
22 | def response_text_message(options={})
23 | reply_text_message("Your Message: #{@keyword}")
24 | end
25 |
26 | # 23.134521
27 | # 113.358803
28 | # 20
29 | #
30 | def response_location_message(options={})
31 | @lx = @weixin_message.Location_X
32 | @ly = @weixin_message.Location_Y
33 | @scale = @weixin_message.Scale
34 | @label = @weixin_message.Label
35 | reply_text_message("Your Location: #{@lx}, #{@ly}, #{@scale}, #{@label}")
36 | end
37 |
38 | #
39 | #
40 | def response_image_message(options={})
41 | @media_id = @weixin_message.MediaId # 可以调用多媒体文件下载接口拉取数据。
42 | @pic_url = @weixin_message.PicUrl # 也可以直接通过此链接下载图片, 建议使用carrierwave.
43 | reply_image_message(generate_image(@media_id))
44 | end
45 |
46 | #
47 | #
48 | #
49 | def response_link_message(options={})
50 | @title = @weixin_message.Title
51 | @desc = @weixin_message.Description
52 | @url = @weixin_message.Url
53 | reply_text_message("回复链接信息")
54 | end
55 |
56 | #
57 | #
58 | def response_voice_message(options={})
59 | @media_id = @weixin_message.MediaId # 可以调用多媒体文件下载接口拉取数据。
60 | @format = @weixin_message.Format
61 | # 如果开启了语音翻译功能,@keyword则为翻译的结果
62 | # reply_text_message("回复语音信息: #{@keyword}")
63 | reply_voice_message(generate_voice(@media_id))
64 | end
65 |
66 | #
67 | #
68 | def response_video_message(options={})
69 | @media_id = @weixin_message.MediaId # 可以调用多媒体文件下载接口拉取数据。
70 | # 视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据。
71 | @thumb_media_id = @weixin_message.ThumbMediaId
72 | reply_text_message("回复视频信息")
73 | end
74 |
75 | def response_event_message(options={})
76 | event_type = @weixin_message.Event
77 | method_name = "handle_#{event_type.downcase}_event"
78 | if self.respond_to? method_name, true
79 | send(method_name)
80 | else
81 | send("handle_undefined_event")
82 | end
83 | end
84 |
85 | # 关注公众账号
86 | def handle_subscribe_event
87 | if @keyword.present?
88 | # 扫描带参数二维码事件: 1. 用户未关注时,进行关注后的事件推送
89 | return reply_text_message("扫描带参数二维码事件: 1. 用户未关注时,进行关注后的事件推送, keyword: #{@keyword}")
90 | end
91 | reply_text_message("关注公众账号")
92 | end
93 |
94 | # 取消关注
95 | def handle_unsubscribe_event
96 | Rails.logger.info("取消关注")
97 | end
98 |
99 | # 扫描带参数二维码事件: 2. 用户已关注时的事件推送
100 | def handle_scan_event
101 | reply_text_message("扫描带参数二维码事件: 2. 用户已关注时的事件推送, keyword: #{@keyword}")
102 | end
103 |
104 | def handle_location_event # 上报地理位置事件
105 | @lat = @weixin_message.Latitude
106 | @lgt = @weixin_message.Longitude
107 | @precision = @weixin_message.Precision
108 | reply_text_message("Your Location: #{@lat}, #{@lgt}, #{@precision}")
109 | end
110 |
111 | # 点击菜单拉取消息时的事件推送
112 | def handle_click_event
113 | reply_text_message("你点击了: #{@keyword}")
114 | end
115 |
116 | # 点击菜单跳转链接时的事件推送
117 | def handle_view_event
118 | Rails.logger.info("你点击了: #{@keyword}")
119 | end
120 |
121 | # 帮助文档: https://github.com/lanrion/weixin_authorize/issues/22
122 |
123 | # 由于群发任务提交后,群发任务可能在一定时间后才完成,因此,群发接口调用时,仅会给出群发任务是否提交成功的提示,若群发任务提交成功,则在群发任务结束时,会向开发者在公众平台填写的开发者URL(callback URL)推送事件。
124 |
125 | # 推送的XML结构如下(发送成功时):
126 |
127 | #
128 | #
129 | #
130 | # 1394524295
131 | #
132 | #
133 | # 1988
134 | #
135 | # 100
136 | # 80
137 | # 75
138 | # 5
139 | #
140 | def handle_masssendjobfinish_event
141 | Rails.logger.info("回调事件处理")
142 | end
143 |
144 | #
145 | #
146 | #
147 | # 1395658920
148 | #
149 | #
150 | # 200163836
151 | #
152 | #
153 | # 推送模板信息回调,通知服务器是否成功推送
154 | def handle_templatesendjobfinish_event
155 | Rails.logger.info("回调事件处理")
156 | end
157 |
158 | #
159 | #
160 | #
161 | # 123456789
162 | #
163 | # //不通过为card_not_pass_check
164 | #
165 | #
166 | # 卡券审核事件,通知服务器卡券已(未)通过审核
167 | def handle_card_pass_check_event
168 | Rails.logger.info("回调事件处理")
169 | end
170 |
171 | def handle_card_not_pass_check_event
172 | Rails.logger.info("回调事件处理")
173 | end
174 |
175 | #
176 | #
177 | #
178 | #
179 | # 123456789
180 | #
181 | #
182 | #
183 | # 1
184 | #
185 | # 0
186 | #
187 | # 卡券领取事件推送
188 | def handle_user_get_card_event
189 | Rails.logger.info("回调事件处理")
190 | end
191 |
192 | #
193 | #
194 | #
195 | # 123456789
196 | #
197 | #
198 | #
199 | #
200 | #
201 | # 卡券删除事件推送
202 | def handle_user_del_card_event
203 | Rails.logger.info("回调事件处理")
204 | end
205 |
206 | #
207 | #
208 | #
209 | # 123456789
210 | #
211 | #
212 | #
213 | #
214 | #
215 | #
216 | # 卡券核销事件推送
217 | def handle_user_consume_card_event
218 | Rails.logger.info("回调事件处理")
219 | end
220 |
221 | #
222 | #
223 | #
224 | # 123456789
225 | #
226 | #
227 | #
228 | #
229 | #
230 | # 卡券进入会员卡事件推送
231 | def handle_user_view_card_event
232 | Rails.logger.info("回调事件处理")
233 | end
234 |
235 | #
236 | #
237 | #
238 | # 123456789
239 | #
240 | #
241 | #
242 | #
243 | #
244 | # 从卡券进入公众号会话事件推送
245 | def handle_user_enter_session_from_card_event
246 | Rails.logger.info("回调事件处理")
247 | end
248 |
249 | #
250 | #
251 | #
252 | # 1408622107
253 | #
254 | #
255 | #
256 | #
257 | #
258 | #
259 | #
260 | # 门店审核事件推送
261 | def handle_poi_check_notify_event
262 | Rails.logger.info("回调事件处理")
263 | end
264 |
265 | # 未定义的事件处理
266 | def handle_undefined_event
267 | Rails.logger.info("回调事件处理")
268 | end
269 |
270 | end
271 |
--------------------------------------------------------------------------------