├── 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 | [![Gem Version](https://badge.fury.io/rb/weixin_rails_middleware.png)](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 | ![](https://raw.githubusercontent.com/lanrion/my_config/master/imagex/donation_me_wx.jpg) 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 | # <![CDATA[公众平台官网链接]]> 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 | # <![CDATA[TITLE]]> 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 | # <![CDATA[title1]]> 88 | # 89 | # 90 | # 91 | # 92 | # 93 | # <![CDATA[title]]> 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 | # <![CDATA[公众平台官网链接]]> 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 | --------------------------------------------------------------------------------