Rails生产环境通过LinkedIn注册用户报BCrypt::Errors::InvalidHash错误
生产环境LinkedIn OmniAuth注册500错误排查
错误现象
生产站点通过LinkedIn完成用户授权回调时,直接返回500内部服务错误,本地开发环境全流程运行无异常。生产端输出的错误日志栈如下:
Completed 500 Internal Server Error in 202ms (ActiveRecord: 3.8ms) [f842309d-189a-4edc-bafb-a6a567ea8fcd] [f842309d-189a-4edc-bafb-a6a567ea8fcd] *BCrypt::Errors::InvalidHash (invalid hash)*: [f842309d-189a-4edc-bafb-a6a567ea8fcd] [f842309d-189a-4edc-bafb-a6a567ea8fcd] app/models/user.rb:82:in `from_omniauth' [f842309d-189a-4edc-bafb-a6a567ea8fcd] app/controllers/users/omniauth_callbacks_controller.rb:9:in `linkedin'
现有实现代码
OmniAuth回调控制器
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController def linkedin @user = User.from_omniauth(auth_hash, current_user, session) remember_me(@user) session[:user_id] = @user.id if !(@user.img_url == auth_hash.info.picture_url) @user.update_attributes(:img_url => auth_hash.info.picture_url) end if not @user.persisted? session["devise.linkedin_data"] = auth_hash render body: session.to_json end sign_in_and_redirect(:user, @user) end end
User模型from_omniauth方法核心逻辑
def self.from_omniauth(auth, signed_in=nil, session) user.password = Devise.friendly_token unless user.encrypted_password.present? end
错误产生原因
BCrypt::Errors::InvalidHash 错误的触发机制非常明确:Devise内部在执行密码校验逻辑时,会直接读取用户数据库中encrypted_password字段的值,交给BCrypt做格式合法性校验。如果这个字段的值不符合BCrypt哈希的标准格式,就会直接抛出该错误。
本地开发和生产环境行为不一致的核心诱因有两点:
- 生产数据库存在历史脏数据:比如早期数据导入脚本、手动数据库修改、旧版本兼容逻辑写入的异常值,这些值的格式不满足BCrypt哈希要求(比如长度不足60位、字符串被截断、非哈希格式的明文文本),本地开发库没有这类脏数据,因此测试时不会触发报错。
- 现有
from_omniauth方法的判断逻辑存在缺陷:代码仅通过encrypted_password.present?判断是否需要为OmniAuth注册用户生成随机密码,完全不校验该字段的值是不是合法的BCrypt哈希。只要字段存在值,哪怕是乱码、不合法的字符串,就会跳过生成合法密码的步骤,后续Devise执行登录状态校验、密码比对流程时,就会触发BCrypt解析失败,直接返回500错误。
对应修复方案
- 第一,先清理生产库的异常数据:查询全量用户的
encrypted_password字段,筛选出长度不符合BCrypt固定长度(60位)的脏数据,将这些异常值清空,或为对应用户写入合法的随机Devise密码哈希。 - 第二,修正
from_omniauth方法的判断逻辑,不要仅靠字段是否为空做判断,增加哈希格式校验:
把原有逻辑:
替换为带格式校验的判断:user.password = Devise.friendly_token unless user.encrypted_password.present?# 仅当密码字段为空,或是哈希格式不合法时,才自动生成随机密码 unless user.encrypted_password.present? && Devise::Encryptor.compare(User, user.encrypted_password, "占位校验字符串") user.password = Devise.friendly_token endDevise::Encryptor.compare方法会在检测到哈希格式不合法时直接抛出InvalidHash错误,被判断条件捕获,自动触发合法密码的赋值逻辑,从根源上避免后续BCrypt解析失败。 - 第三,补充流程兜底:在OmniAuth回调流程中,如果检测到匹配用户的加密密码格式不合法,优先在模型层完成合法密码的补全,不要等到登录流程触发底层解析才报错。
内容的提问来源于stack exchange,提问作者Penny
相关产品推荐
相关产品推荐

