Rails: 如何有效验证链接(URLs)?

150

我想知道在Rails中如何最好地验证URL。 我考虑使用正则表达式,但不确定这是否是最佳实践。

如果我要使用正则表达式,有人可以向我建议一个吗? 我对正则表达式还很陌生。


相关:https://dev59.com/7HI-5IYBdhLWcg3wiY3a - Jon Schneider
23个回答

174

验证URL是一项棘手的工作。这也是一个非常广泛的请求。

你想要做什么,确切地说?你想要验证URL的格式、存在性还是其他什么?根据你想要做什么,有几种可能性。

正则表达式可以验证URL的格式。但即使是复杂的正则表达式也不能保证你正在处理一个有效的URL。

例如,如果你使用一个简单的正则表达式,它可能会拒绝以下主机:

http://invalid##host.com

但它将允许

http://invalid-host.foo

这是一个有效的主机,但如果考虑到现有的顶级域名,则不是有效的域名。事实上,如果您想要验证主机名而不是域名,那么以下解决方案将起作用,因为以下内容是有效的主机名。

http://host.foo

还有下面这个

http://localhost

现在,让我给你一些解决方案。

如果你想验证一个域名,那么你需要忘记正则表达式。目前最好的解决方案是Public Suffix List(由Mozilla维护的列表)。我创建了一个Ruby库来解析和验证域名是否在Public Suffix List中,并且它叫做PublicSuffix

如果你想验证URI/URL的格式,那么你可能需要使用正则表达式。不要搜索一个,而是使用内置的Ruby URI.parse方法。

require 'uri'

def valid_url?(uri)
  uri = URI.parse(uri) && uri.host.present?
rescue URI::InvalidURIError
  false
end

你甚至可以决定让它更加严格。例如,如果你想要URL是一个HTTP/HTTPS URL,那么你可以使验证更加准确。

require 'uri'

def valid_url?(url)
  uri = URI.parse(url)
  uri.is_a?(URI::HTTP) && uri.host.present?
rescue URI::InvalidURIError
  false
end

当然,你可以对这种方法进行大量的改进,包括检查路径或方案。

最后但并非最不重要的是,你还可以将这段代码打包成一个验证器:

class HttpUrlValidator < ActiveModel::EachValidator

  def self.compliant?(value)
    uri = URI.parse(value)
    uri.is_a?(URI::HTTP) && uri.host.present?
  rescue URI::InvalidURIError
    false
  end

  def validate_each(record, attribute, value)
    unless value.present? && self.class.compliant?(value)
      record.errors.add(attribute, "is not a valid HTTP URL")
    end
  end

end

# in the model
validates :example_attribute, http_url: true

针对新的URI版本(即0.12.1)的注意事项

.present? / .blank? 是验证主机更准确的方式,而不是使用uri.host.nil?或之前的if uri.host(即URI v 0.11)。

例如,对于URI.parse("https:///394"):

  • 新的URI版本(0.12),host将返回一个空字符串,/394成为路径。#<URI::HTTPS https:///394>
  • 旧的URI版本(0.11),host将返回一个空字符串,/394也成为路径。#<URI::HTTPS https:/394>

15
URI::HTTPS 继承自 URI::HTTP,这就是我使用 kind_of? 的原因。 - Simone Carletti
2
目前最完整的解决方案,可以安全地验证URL。 - Fabrizio Regini
4
URI.parse('http://invalid-host.foo') 返回 true,因为该 URI 是一个有效的 URL。另外请注意,.foo 现在是一个有效的顶级域名。http://www.iana.org/domains/root/db/foo.html - Simone Carletti
1
@jmccartie 请阅读整篇文章。如果您关心这个方案,应该使用包含类型检查的最终代码,而不仅是那一行代码。您在文章结尾之前停止了阅读。 - Simone Carletti
2
www.google 是一个有效的域名,尤其是现在 .GOOGLE 是一个有效的顶级域:https://github.com/whois/ianawhois/blob/master/GOOGLE。如果您想要验证器明确验证特定的顶级域名,则必须添加任何您认为适当的业务逻辑。 - Simone Carletti
显示剩余10条评论

119
我在我的模型中使用一行代码:
``` validates :url, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]) ```
我认为这足够好用且简单。此外,它在理论上应该与Simone的方法等效,因为它在内部使用完全相同的正则表达式。

17
不幸的是,'http://' 与上述模式匹配。请参见:URI::regexp(%w(http https))=~'http:// ' - David J.
17
也就是说,像 http:fake 这样的网址也是有效的。 - nathanvda

57

按照 Simone 的想法,你可以轻松地创建自己的验证器。

class UrlValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if value.blank?
    begin
      uri = URI.parse(value)
      resp = uri.kind_of?(URI::HTTP)
    rescue URI::InvalidURIError
      resp = false
    end
    unless resp == true
      record.errors[attribute] << (options[:message] || "is not an url")
    end
  end
end
并且之后使用

validates :url, :presence => true, :url => true
在你的模型中。

1
我应该把这个类放在哪里?在初始化器中吗? - deb
3
我引用 @gbc 的话:"如果您将自定义验证器放置在 app/validators 目录中,则无需更改 config/application.rb 文件即可自动加载它们。"(https://dev59.com/4G435IYBdhLWcg3wtSUV#6610270)。请注意,下面 Stefan Pettersson 的答案也显示了他将类似的文件保存在 "app/validators" 目录中。 - bergie3000
4
这只是检查URL是否以http://或https://开头,它并不能完全验证URL的有效性。 - maggix
1
如果您可以让URL是可选的,那么请使用以下类进行验证:class OptionalUrlValidator < UrlValidator def validate_each(record, attribute, value) return true if value.blank? return super endend - Dirty Henry
1
这不是一个好的验证方式:URI("http:").kind_of?(URI::HTTP) #=> true - smathy
显示剩余2条评论

32

还有一个validate_url gem(只是对Addressable::URI.parse方法的良好封装)。

只需添加

gem 'validate_url'

在你的Gemfile中添加,然后你就可以在模型中使用了


validates :click_through_url, url: true

@ЕвгенийМасленков,这可能很好,因为它符合规范,但您可能需要检查https://github.com/sporkmonger/addressable/issues。此外,在一般情况下,我们发现没有人遵循标准,而是使用简单的格式验证。 - Ev Dolzhenko

14

这个问题已经有答案了,但是无论如何,我提出我正在使用的解决方案。

正则表达式对我遇到的所有url都有效。 setter方法是为了处理没有提及协议的情况(假设为http://)。

最后,我们尝试获取页面。也许我应该接受重定向而不仅仅是HTTP 200 OK。

# app/models/my_model.rb
validates :website, :allow_blank => true, :uri => { :format => /(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$)/ix }

def website= url_str
  unless url_str.blank?
    unless url_str.split(':')[0] == 'http' || url_str.split(':')[0] == 'https'
        url_str = "http://" + url_str
    end
  end  
  write_attribute :website, url_str
end

和...

# app/validators/uri_vaidator.rb
require 'net/http'

# Thanks Ilya! http://www.igvita.com/2006/09/07/validating-url-in-ruby-on-rails/
# Original credits: http://blog.inquirylabs.com/2006/04/13/simple-uri-validation/
# HTTP Codes: http://www.ruby-doc.org/stdlib/libdoc/net/http/rdoc/classes/Net/HTTPResponse.html

class UriValidator < ActiveModel::EachValidator
  def validate_each(object, attribute, value)
    raise(ArgumentError, "A regular expression must be supplied as the :format option of the options hash") unless options[:format].nil? or options[:format].is_a?(Regexp)
    configuration = { :message => I18n.t('errors.events.invalid_url'), :format => URI::regexp(%w(http https)) }
    configuration.update(options)

    if value =~ configuration[:format]
      begin # check header response
        case Net::HTTP.get_response(URI.parse(value))
          when Net::HTTPSuccess then true
          else object.errors.add(attribute, configuration[:message]) and false
        end
      rescue # Recover on DNS failures..
        object.errors.add(attribute, configuration[:message]) and false
      end
    else
      object.errors.add(attribute, configuration[:message]) and false
    end
  end
end

真的很棒!感谢您的贡献,解决问题通常有许多方法;当人们分享自己的想法时,这是非常棒的。 - jay
7
根据Rails安全指南(http://guides.rubyonrails.org/security.html#regular-expressions),您应该在正则表达式中使用\A和\z而不是$^。请注意这一点。 - Jared
1
我喜欢这个。为了让代码更加干净,快速建议将正则表达式移到验证器中,因为我想您希望在不同模型之间保持一致。附加奖励:它将允许您删除验证每个下面的第一行。 - Paul Pettengill
如果URL加载时间过长并超时,应该怎么办?最好的选择是显示超时错误消息还是无法打开页面? - user588324
这样做永远不会通过安全审计,因为你让你的服务器访问一个任意的URL。 - Mauricio

13

对我有用的解决方案是:

validates_format_of :url, :with => /\A(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w\.-]*)*\/?\Z/i

我尝试使用您附加的示例,但我支持的url格式为:

请注意使用A和Z,因为如果您使用^和$,则会看到Rails验证器的安全警告。

 Valid ones:
 'www.crowdint.com'
 'crowdint.com'
 'http://crowdint.com'
 'http://www.crowdint.com'

 Invalid ones:
  'http://www.crowdint. com'
  'http://fake'
  'http:fake'

1
尝试使用"https://portal.example.com/portal/#"。在Ruby 2.1.6中,评估会挂起。 - Old Pro
你是对的,在某些情况下,这个正则表达式需要很长时间才能解决 :( - Heriberto Magaña
2
显然,并没有一个正则表达式可以涵盖所有情况,这就是为什么我最终只使用了一个简单的验证:validates :url, format: { with: URI.regexp }, if: Proc.new { |a| a.url.present? } - Heriberto Magaña

12

您还可以尝试valid_url宝石,它允许没有方案的URL,检查域区和IP主机名。

将其添加到Gemfile中:

gem 'valid_url'

然后在模型中:

class WebSite < ActiveRecord::Base
  validates :url, :url => true
end

这太好了,特别是没有方案的URL,这与URI类惊人地相关。 - Paul Pettengill
我对这个宝石挖掘基于IP的URL并检测伪造的能力感到惊讶。谢谢! - The Whiz of Oz

10

只是我的个人意见:

before_validation :format_website
validate :website_validator

private

def format_website
  self.website = "http://#{self.website}" unless self.website[/^https?/]
end

def website_validator
  errors[:website] << I18n.t("activerecord.errors.messages.invalid") unless website_valid?
end

def website_valid?
  !!website.match(/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-=\?]*)*\/?$/)
end

编辑:更改正则表达式以匹配参数URL。


1
感谢您的输入,总是很高兴看到不同的解决方案。 - jay
顺便提一下,你的正则表达式会拒绝带有查询字符串的有效网址,例如 http://test.com/fdsfsdf?a=b - mikdiet
2
我们将这段代码投入生产并一直在.match正则表达式行上遇到无限循环的超时。不确定原因,只是对某些边缘情况保持谨慎,并希望听取其他人对此发生的原因的想法。 - toobulkeh

5

最近我遇到了同样的问题(我需要在Rails应用程序中验证url),但我必须应对使用unicode url的额外要求(例如http://кц.рф)...

我研究了几种解决方案,发现以下方法:


是的,但是从Addressable的角度来看,Addressable :: URI.parse('http:///').scheme#=>“http”Addressable :: URI.parse('Съешь[же]ещёэтихмягкихфранцузскихбулокдавыпейчаю')都是完全可以接受的 :( - smileart

4
这是由David James发布的验证器的更新版本,由Benjamin Fleischer发布。同时,我推送了一个更新的分支,可以在这里找到。
require 'addressable/uri'

# Source: http://gist.github.com/bf4/5320847
# Accepts options[:message] and options[:allowed_protocols]
# spec/validators/uri_validator_spec.rb
class UriValidator < ActiveModel::EachValidator

  def validate_each(record, attribute, value)
    uri = parse_uri(value)
    if !uri
      record.errors[attribute] << generic_failure_message
    elsif !allowed_protocols.include?(uri.scheme)
      record.errors[attribute] << "must begin with #{allowed_protocols_humanized}"
    end
  end

private

  def generic_failure_message
    options[:message] || "is an invalid URL"
  end

  def allowed_protocols_humanized
    allowed_protocols.to_sentence(:two_words_connector => ' or ')
  end

  def allowed_protocols
    @allowed_protocols ||= [(options[:allowed_protocols] || ['http', 'https'])].flatten
  end

  def parse_uri(value)
    uri = Addressable::URI.parse(value)
    uri.scheme && uri.host && uri
  rescue URI::InvalidURIError, Addressable::URI::InvalidURIError, TypeError
  end

end

...

require 'spec_helper'

# Source: http://gist.github.com/bf4/5320847
# spec/validators/uri_validator_spec.rb
describe UriValidator do
  subject do
    Class.new do
      include ActiveModel::Validations
      attr_accessor :url
      validates :url, uri: true
    end.new
  end

  it "should be valid for a valid http url" do
    subject.url = 'http://www.google.com'
    subject.valid?
    subject.errors.full_messages.should == []
  end

  ['http://google', 'http://.com', 'http://ftp://ftp.google.com', 'http://ssh://google.com'].each do |invalid_url|
    it "#{invalid_url.inspect} is a invalid http url" do
      subject.url = invalid_url
      subject.valid?
      subject.errors.full_messages.should == []
    end
  end

  ['http:/www.google.com','<>hi'].each do |invalid_url|
    it "#{invalid_url.inspect} is an invalid url" do
      subject.url = invalid_url
      subject.valid?
      subject.errors.should have_key(:url)
      subject.errors[:url].should include("is an invalid URL")
    end
  end

  ['www.google.com','google.com'].each do |invalid_url|
    it "#{invalid_url.inspect} is an invalid url" do
      subject.url = invalid_url
      subject.valid?
      subject.errors.should have_key(:url)
      subject.errors[:url].should include("is an invalid URL")
    end
  end

  ['ftp://ftp.google.com','ssh://google.com'].each do |invalid_url|
    it "#{invalid_url.inspect} is an invalid url" do
      subject.url = invalid_url
      subject.valid?
      subject.errors.should have_key(:url)
      subject.errors[:url].should include("must begin with http or https")
    end
  end
end

请注意,仍有奇怪的HTTP URI被解析为有效地址。
http://google  
http://.com  
http://ftp://ftp.google.com  
http://ssh://google.com

这里有一个与 addressable gem 相关的 问题,其中包含了一些例子。

上面链接的问题中,仓库的所有者详细解释了为什么“奇怪的HTTP URI”是有效的,以及对于他的库的工作来说,失败的有效URI比允许无效的URI更具破坏性。 - notapatch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接