Rails: Cách tốt để xác thực các liên kết (URL) là gì?


125

Tôi đã tự hỏi làm thế nào tôi sẽ xác nhận tốt nhất các URL trong Rails. Tôi đã nghĩ đến việc sử dụng một biểu thức thông thường, nhưng không chắc đây có phải là cách thực hành tốt nhất không.

Và, nếu tôi sử dụng regex, ai đó có thể gợi ý cho tôi không? Tôi vẫn chưa quen với Regex.


Câu trả lời:


151

Xác thực một URL là một công việc khó khăn. Đó cũng là một yêu cầu rất rộng.

Chính xác thì bạn muốn làm gì? Bạn có muốn xác thực định dạng của URL, sự tồn tại hoặc những gì? Có một số khả năng, tùy thuộc vào những gì bạn muốn làm.

Biểu thức chính quy có thể xác thực định dạng của URL. Nhưng ngay cả một biểu thức chính quy phức tạp cũng không thể đảm bảo bạn đang xử lý một URL hợp lệ.

Chẳng hạn, nếu bạn lấy một biểu thức chính quy đơn giản, nó có thể sẽ từ chối máy chủ sau

http://invalid##host.com

nhưng nó sẽ cho phép

http://invalid-host.foo

đó là một máy chủ hợp lệ, nhưng không phải là một miền hợp lệ nếu bạn xem xét các TLD hiện có. Thật vậy, giải pháp sẽ hoạt động nếu bạn muốn xác thực tên máy chủ, không phải tên miền vì tên sau là tên máy chủ hợp lệ

http://host.foo

cũng như sau đây

http://localhost

Bây giờ, hãy để tôi cung cấp cho bạn một số giải pháp.

Nếu bạn muốn xác thực một tên miền, thì bạn cần quên đi các biểu thức thông thường. Giải pháp tốt nhất hiện có là Danh sách Suffix công khai, một danh sách được duy trì bởi Mozilla. Tôi đã tạo một thư viện Ruby để phân tích và xác thực các tên miền theo Danh sách Suffix công khai và nó được gọi là PublicSuffix .

Nếu bạn muốn xác thực định dạng của URI / URL, thì bạn có thể muốn sử dụng các biểu thức thông thường. Thay vì tìm kiếm một cái, hãy sử dụng URI.parsephương thức Ruby tích hợp .

require 'uri'

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

Bạn thậm chí có thể quyết định làm cho nó hạn chế hơn. Ví dụ: nếu bạn muốn URL là URL HTTP / HTTPS, thì bạn có thể xác thực chính xác hơn.

require 'uri'

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

Tất nhiên, có rất nhiều cải tiến bạn có thể áp dụng cho phương pháp này, bao gồm kiểm tra đường dẫn hoặc sơ đồ.

Cuối cùng nhưng không kém phần quan trọng, bạn cũng có thể đóng gói mã này vào trình xác nhận:

class HttpUrlValidator < ActiveModel::EachValidator

  def self.compliant?(value)
    uri = URI.parse(value)
    uri.is_a?(URI::HTTP) && !uri.host.nil?
  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

1
Lưu ý rằng lớp học sẽ URI::HTTPSdành cho https uris (ví dụ:URI.parse("https://yo.com").class => URI::HTTPS
tee

12
URI::HTTPSkế thừa từ URI:HTTP, đó là lý do tại sao tôi sử dụng kind_of?.
Simone Carletti

1
Cho đến nay, giải pháp đầy đủ nhất để xác thực URL một cách an toàn.
Fabrizio Regini

4
URI.parse('http://invalid-host.foo')trả về true vì URI đó là một URL hợp lệ. Cũng lưu ý rằng .foobây giờ là một TLD hợp lệ. iana.org/domains/root/db/foo.html
Simone Carletti

1
@jmccartie vui lòng đọc toàn bộ bài. Nếu bạn quan tâm đến lược đồ, bạn nên sử dụng mã cuối cùng bao gồm kiểm tra loại, không chỉ dòng đó. Bạn đã ngừng đọc trước khi kết thúc bài viết.
Simone Carletti

101

Tôi sử dụng một lớp lót bên trong các mô hình của mình:

validates :url, format: URI::regexp(%w[http https])

Tôi nghĩ là đủ tốt và đơn giản để sử dụng. Hơn nữa, về mặt lý thuyết nó phải tương đương với phương pháp của Simone, vì nó sử dụng cùng một biểu thức chính quy trong nội bộ.


17
Thật không may 'http://'phù hợp với mô hình trên. Xem:URI::regexp(%w(http https)) =~ 'http://'
David J.

15
Ngoài ra một url như http:fakesẽ hợp lệ.
nathanvda

54

Theo ý tưởng của Simone, bạn có thể dễ dàng tạo trình xác nhận của riêng mình.

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

và sau đó sử dụng

validates :url, :presence => true, :url => true

trong mô hình của bạn.


1
Tôi nên đặt lớp học này ở đâu? Trong một khởi tạo?
deb

3
Tôi trích dẫn từ @gbc: "Nếu bạn đặt trình xác nhận tùy chỉnh của mình vào ứng dụng / trình xác nhận, chúng sẽ được tải tự động mà không cần thay đổi tệp config / application.rb của bạn." ( stackoverflow.com/a/6610270/839847 ). Lưu ý rằng câu trả lời dưới đây từ Stefan Pettersson cho thấy rằng anh ta cũng đã lưu một tệp tương tự trong "ứng dụng / trình xác nhận".
bergie3000

4
điều này chỉ kiểm tra nếu url bắt đầu bằng http: // hoặc https: //, đó không phải là xác thực URL thích hợp
maggix

1
Kết thúc nếu bạn có thể cho phép URL trở thành tùy chọn: class OptionsUrlValidator <UrlValidator def validate_each (record, property, value) trả về true nếu value.blank? trở lại siêu kết thúc
Dirty Henry

1
Đây không phải là một xác nhận tốt:URI("http:").kind_of?(URI::HTTP) #=> true
smathy

28

Ngoài ra còn có gemateate_url (chỉ là một trình bao bọc đẹp cho Addressable::URI.parsegiải pháp).

Chỉ cần thêm

gem 'validate_url'

cho bạn Gemfile, và sau đó trong các mô hình bạn có thể

validates :click_through_url, url: true

@ ВгенийÓAасленков có thể cũng như vậy bởi vì nó hợp lệ theo thông số kỹ thuật, nhưng bạn có thể muốn kiểm tra github.com/sporkmonger/addressable/issues . Ngoài ra trong trường hợp chung, chúng tôi đã thấy rằng không ai tuân theo tiêu chuẩn và thay vào đó đang sử dụng xác nhận định dạng đơn giản.
dolzenko

13

Câu hỏi này đã được trả lời, nhưng cái quái gì, tôi đề xuất giải pháp tôi đang sử dụng.

Regrec hoạt động tốt với tất cả các url tôi đã gặp. Phương thức setter là cẩn thận nếu không có giao thức nào được đề cập (giả sử http: //).

Và cuối cùng, chúng tôi cố gắng tìm nạp trang. Có lẽ tôi nên chấp nhận chuyển hướng và không chỉ 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

và ...

# 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

thực sự gọn gàng! cảm ơn cho đầu vào của bạn, thường có nhiều cách tiếp cận cho một vấn đề; thật tuyệt khi mọi người chia sẻ chúng.
jay

6
Chỉ muốn chỉ ra rằng theo hướng dẫn bảo mật đường ray, bạn nên sử dụng \ A và \ z thay vì $ ^ trong regrec đó
Jared

1
Tôi thích nó. Gợi ý nhanh để làm khô mã một chút bằng cách chuyển regex vào trình xác nhận hợp lệ, như tôi tưởng tượng bạn muốn nó nhất quán trên các mô hình. Phần thưởng: Nó sẽ cho phép bạn bỏ dòng đầu tiên dưới dạngateate_each.
Paul Pettengill

Điều gì nếu url mất nhiều thời gian và thời gian chờ? Điều gì sẽ là tùy chọn tốt nhất để hiển thị thông báo lỗi hết thời gian hoặc nếu trang không thể được mở?
dùng588324

điều này sẽ không bao giờ vượt qua kiểm toán bảo mật, bạn đang làm cho máy chủ của mình chọc một url tùy ý
Mauricio

12

Bạn cũng có thể thử gem_url gem cho phép URL mà không cần lược đồ, kiểm tra tên miền và tên máy chủ ip.

Thêm nó vào Gemfile của bạn:

gem 'valid_url'

Và sau đó trong mô hình:

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

Điều này thật tuyệt vời, đặc biệt là các URL không có lược đồ, có liên quan đáng ngạc nhiên với lớp URI.
Paul Pettengill

Tôi đã rất ngạc nhiên bởi khả năng của viên ngọc này trong việc tìm kiếm các URL dựa trên IP và phát hiện các URL không có thật. Cảm ơn!
The Whiz of Oz

10

Chỉ 2 xu của tôi:

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

EDIT: đã thay đổi regex để khớp các url tham số.


1
Cảm ơn thông tin của bạn, luôn luôn tốt để xem các giải pháp khác nhau
jay

Btw, http://test.com/fdsfsdf?a=b
regrec

2
Chúng tôi đưa mã này vào sản xuất và tiếp tục hết thời gian trên các vòng lặp vô hạn trên dòng regex .match. Không chắc chắn tại sao, chỉ cần thận trọng đối với một số cornercase và rất thích nghe suy nghĩ của người khác về lý do tại sao điều này sẽ xảy ra.
toobulkeh

10

Giải pháp hiệu quả với tôi là:

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

Tôi đã thử sử dụng một số ví dụ mà bạn đã đính kèm nhưng tôi đang hỗ trợ url như vậy:

Lưu ý việc sử dụng A và Z vì nếu bạn sử dụng ^ và $, bạn sẽ thấy bảo mật cảnh báo này từ trình xác nhận 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
Hãy thử điều này với "https://portal.example.com/portal/#". Trong Ruby 2.1.6 việc đánh giá bị treo.
Old Pro

bạn có vẻ như trong một số trường hợp, biểu thức chính quy này sẽ mất mãi mãi để giải quyết :(
heriberto perez

1
rõ ràng, không có một biểu thức nào bao trùm mọi kịch bản, đó là lý do tại sao tôi kết thúc chỉ bằng một xác nhận đơn giản: xác thực: url, định dạng: {với: URI.regapi}, if: Proc.new {| a | a.url.present? }
heriberto perez

5

Gần đây tôi gặp vấn đề tương tự (tôi cần xác thực các url trong ứng dụng Rails) nhưng tôi phải đối phó với yêu cầu bổ sung của các url unicode (ví dụ http://кц.рф) ...

Tôi đã nghiên cứu một vài giải pháp và tìm thấy những điều sau đây:

  • Điều đầu tiên và được đề xuất nhất là sử dụng URI.parse. Kiểm tra câu trả lời của Simone Carletti để biết chi tiết. Điều này hoạt động tốt, nhưng không cho các url unicode.
  • Phương pháp thứ hai tôi thấy là phương pháp của Ilya Grigorik: http://www.igvita.com/2006/09/07/validating-url-in-ruby-on-rails/ Về cơ bản, anh ta cố gắng yêu cầu url; nếu nó hoạt động, nó hợp lệ ...
  • Phương pháp thứ ba tôi tìm thấy (và phương pháp tôi thích) là một cách tiếp cận tương tự URI.parsenhưng sử dụng addressableđá quý thay vì URIstdlib. Cách tiếp cận này được trình bày chi tiết tại đây: http://rawsyntax.com/blog/url-validation-in-rails-3-and-ruby-in-general/

Vâng, nhưng Addressable::URI.parse('http:///').scheme # => "http"hoặc Addressable::URI.parse('Съешь [же] ещё этих мягких французских булок да выпей чаю')hoàn toàn ổn theo quan điểm của Địa chỉ :(
smileart

4

Dưới đây là phiên bản cập nhật của trình xác nhận được đăng bởi David James . Nó đã được xuất bản bởi Benjamin Fleischer . Trong khi đó, tôi đã đẩy một ngã ba cập nhật có thể được tìm thấy ở đây .

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

Xin lưu ý rằng vẫn còn các URI HTTP lạ được phân tích cú pháp làm địa chỉ hợp lệ.

http://google  
http://.com  
http://ftp://ftp.google.com  
http://ssh://google.com

Đây là một vấn đề cho addressableđá quý bao gồm các ví dụ.


3

Tôi sử dụng một biến thể nhỏ trên giải pháp lafeber ở trên . Nó không cho phép các dấu chấm liên tiếp trong tên máy chủ (ví dụ như trong www.many...dots.com):

%r"\A(https?://)?[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]{2,6}(/.*)?\Z"i

URI.parsedường như bắt buộc tiền tố của lược đồ, trong một số trường hợp không phải là điều bạn có thể muốn (ví dụ: nếu bạn muốn cho phép người dùng của mình nhanh chóng đánh vần URL trong các biểu mẫu như twitter.com/username)


2

Tôi đã sử dụng đá quý 'activevalidators' và nó hoạt động khá tốt (không chỉ để xác thực url)

bạn có thể tìm thấy nó ở đây

Tất cả đều được ghi lại nhưng về cơ bản một khi đá quý được thêm vào, bạn sẽ muốn thêm một vài dòng sau vào trình khởi tạo, nói: /config/envirments/initialulators/active_validators_activation.rb

# Activate all the validators
ActiveValidators.activate(:all)

(Lưu ý: bạn có thể thay thế: tất cả bằng: url hoặc: bất cứ điều gì nếu bạn chỉ muốn xác thực các loại giá trị cụ thể)

Và sau đó trở lại trong mô hình của bạn một cái gì đó như thế này

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

Bây giờ Khởi động lại máy chủ và nó sẽ là nó


2

Nếu bạn muốn xác nhận đơn giản và thông báo lỗi tùy chỉnh:

  validates :some_field_expecting_url_value,
            format: {
              with: URI.regexp(%w[http https]),
              message: 'is not a valid URL'
            }

1

Bạn có thể xác thực nhiều url bằng cách sử dụng cái gì đó như:

validates_format_of [:field1, :field2], with: URI.regexp(['http', 'https']), allow_nil: true

1
Làm thế nào bạn có thể xử lý URL mà không có chương trình (ví dụ: www.bar.com/foo)?
craig


1

Gần đây tôi có vấn đề tương tự và tôi đã tìm thấy một công việc xung quanh cho các url hợp lệ.

validates_format_of :url, :with => URI::regexp(%w(http https))
validate :validate_url
def validate_url

  unless self.url.blank?

    begin

      source = URI.parse(self.url)

      resp = Net::HTTP.get_response(source)

    rescue URI::InvalidURIError

      errors.add(:url,'is Invalid')

    rescue SocketError 

      errors.add(:url,'is Invalid')

    end



  end

Phần đầu tiên của phương thức validate_url là đủ để xác thực định dạng url. Phần thứ hai sẽ đảm bảo url tồn tại bằng cách gửi yêu cầu.


Điều gì xảy ra nếu url trỏ đến một tài nguyên rất lớn (giả sử, nhiều gigabyte)?
Jon Schneider

@JonSchneider người ta có thể sử dụng yêu cầu đầu http (như ở đây ) thay vì nhận.
wvengen

1

Tôi thích gắn mô-đun URI để thêm hợp lệ? phương pháp

phía trong config/initializers/uri.rb

module URI
  def self.valid?(url)
    uri = URI.parse(url)
    uri.is_a?(URI::HTTP) && !uri.host.nil?
  rescue URI::InvalidURIError
    false
  end
end

0

Và như một mô-đun

module UrlValidator
  extend ActiveSupport::Concern
  included do
    validates :url, presence: true, uniqueness: true
    validate :url_format
  end

  def url_format
    begin
      errors.add(:url, "Invalid url") unless URI(self.url).is_a?(URI::HTTP)
    rescue URI::InvalidURIError
      errors.add(:url, "Invalid url")
    end
  end
end

Và sau đó chỉ cần include UrlValidatortrong bất kỳ mô hình mà bạn muốn url validate là cho. Chỉ bao gồm cho các tùy chọn.


0

Xác thực URL không thể được xử lý đơn giản bằng cách sử dụng Biểu thức chính quy vì số lượng trang web tiếp tục phát triển và các chương trình đặt tên miền mới tiếp tục xuất hiện.

Trong trường hợp của tôi, tôi chỉ cần viết một trình xác nhận tùy chỉnh để kiểm tra phản hồi thành công.

class UrlValidator < ActiveModel::Validator
  def validate(record)
    begin
      url = URI.parse(record.path)
      response = Net::HTTP.get(url)
      true if response.is_a?(Net::HTTPSuccess)   
    rescue StandardError => error
      record.errors[:path] << 'Web address is invalid'
      false
    end  
  end
end

Tôi đang xác nhận paththuộc tính của mô hình của tôi bằng cách sử dụng record.path. Tôi cũng đang đẩy lỗi đến tên thuộc tính tương ứng bằng cách sử dụng record.errors[:path].

Bạn chỉ có thể thay thế bằng tên bất kỳ.

Sau đó, tôi chỉ cần gọi trình xác nhận tùy chỉnh trong mô hình của tôi.

class Url < ApplicationRecord

  # validations
  validates_presence_of :path
  validates_with UrlValidator

end

Điều gì xảy ra nếu url trỏ đến một tài nguyên rất lớn (giả sử, nhiều gigabyte)?
Jon Schneider

0

Bạn có thể sử dụng regex cho việc này, đối với tôi hoạt động tốt điều này:

(^|[\s.:;?\-\]<\(])(ftp|https?:\/\/[-\w;\/?:@&=+$\|\_.!~*\|'()\[\]%#,]+[\w\/#](\(\))?)(?=$|[\s',\|\(\).:;?\-\[\]>\)])
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.