D-7. Rails API认证功能 && Find All Numbers Disappeared in an Array

今日会以昨日同份专案继续。
再次提醒,API Only,没有view。


为何需要验证。

总不可能让所有人都可以随意更改别人内容,或随意使用网站功能,所以如一般网页一样,还是要建立一个完整的会员机制。虽然我们可以设定所有资料只有read-only,但在开发途中也可能其他夥伴需要更改内容,或者未来让只有部分串接者能使用,验证的功能是一定需要的。


与一般网站不同处?

透过API与服务器沟通,是无法以session cookie来记忆用户登出登入,通常会在请求的HeaderBody内夹带access token来处理。最简单的想,就像我们串接API一样,需要有API KEY

我们用最简单的作法,让用户在注册後就可以得到一组token也就是API KEY,另外多加一个小功能,让用户登出後,会发一组新的KEY,提高KEY的安全性。
API KEY理所当然需要具有绝对独特性,因为要建立会员系统所以顺便使用Devise,而如果是要生成更好的API KEY,可以再找专门的gem处理。


准备工作。

这边便只简单说明流程,神器等级的gem,请多阅读其首页,解锁更多使用姿势。
Devise安装

#Gemfile
gem 'devise', '~> 4.8'
$ rails g devise:install
$ rails g devise user
$ rails db:migrate

建立取得API token

我们用常见的方式取名吧。

$ rails g migration add_column_token_to_user

#migrate
class AddColumnTokenToUser < ActiveRecord::Migration[6.1]
  def change
    add_column :users, :authentication_token, :string
    # 加索引与要求unique
    add_index :users, :authentication_token, :unique => true

    #如果DB已经有其他user在,请多做这一步。
    # User.find_each do |user|
    #   user.generate_authentication_token
    #   user.save!
    # end
  end
end

$ rails db:migrate

#User Model
class User < ApplicationRecord
  #略...

  before_create :generate_authentication_token

  def generate_authentication_token
    self.authentication_token = Devise.friendly_token
  end
end

authentication_token其实命名好记就好。api_tokenauth_token都可以。
比命名更重要的是User Model里的设定那个方法,这样才可以确保用户取得API KEYDevise.friendly_token很明显是Devise给的方法,也可客制化长度.friendly_token(length = 20) ⇒ Object


建立注册登入登出

app/controllers/application_controller.rb

class ApplicationController < ActionController::API
  before_action :authenticate_user_from_token!

  def authenticate_user_from_token!
    if params[:auth_token].present?
      user = User.find_by_authentication_token( params[:auth_token] )
      sign_in(user, store: false) if user
    end
  end
end

由於一开始用API-Only建立专案,所以原生application_controller内是ActionController::API,非API-Only需手动建立。

与一般专案设定Devise很像,有设定这个才有current_user方法。
store: false是因为不是一般网页式登入,不需要去记忆於session

设定路径routes.rb

Rails.application.routes.draw do
  devise_for :users
  namespace :api do
    namespace :v1 do
      resources :articles
      post "/signup", to: "auth#signup"
      post "/login", to: "auth#login"
      post "/logout", to: "auth#logout"
    end
  end
end

建立auth_controller

$ rails g controller api/v1/auth

class Api::V1::AuthController < ApplicationController
  before_action :authenticate_user!, only: [:logout]
  def signup
    user = User.new( email: params[:email], password: params[:password] )

    if user.save
      render json: { user_id: user.id, email: user.email}, status: 200
    else
      render json: { message: "Signup Failed", errors: user.errors }, status: 400
    end
  end

  def login
    if params[:email] && params[:password]
      user = User.find_by_email( params[:email] )
    end

    if user && user.valid_password?( params[:password] )
      render json: { message: "Login!",
                     auth_token: user.authentication_token,
                     user_id: user.id }, status: 200
    else
      render json: { message: "Email or Password wrong" }, status: 401
    end
  end

  def logout
    # 设计使用户重新登入时,authentication_token会换。
    current_user.generate_authentication_token
    current_user.save!

    render json: { message: "See you!"}
    #加一点回传值,让用户知道他确实登出了。
  end

end

这边可以看到设计上,登入时才秀出KEY
可以之後加工,会员还需要某些认证才可以正确登入,才可以看到KEY


postman上测试吧。

输入HendlerBody方式很多,可以再查询自己觉得便利的方式,这边一样采用Head告知给什麽资料,Body给资料。
Singup
https://ithelp.ithome.com.tw/upload/images/20210923/20135887eUstA3r4xy.png

Login
"auth_token": "BMb-p3usAg3ic6_vRp-V"如果你跟我的一样,请记得通知我买乐透,这边纪录一下待会会用到。
https://ithelp.ithome.com.tw/upload/images/20210923/20135887Rki20Fylvg.png

logout。要输入的有点不同,就是刚刚纪录的"auth_token": "BMb-p3usAg3ic6_vRp-V"
https://ithelp.ithome.com.tw/upload/images/20210923/20135887qj7QOafks5.png

可以再login一次看看,确定是否token有换。


来实作谁建立的,谁才能修改吧。

建立关联性。

Article

$ rails g migration add_column_articles

#migrate
class AddColumnArticles < ActiveRecord::Migration[6.1]
  def change
    add_reference :articles, :user, foreign_key: true
  end
end

$ rails db:migrate

#Article Model
class Article < ApplicationRecord
  belongs_to :user
end

#User Model
class User < ApplicationRecord
  #略...
  has_many :articles

end

记得可以rails c --sandbox测试一下关联性。


改写articles_controller.rb

改写之後,postman上任何动作都开始要加"auth_token": "user_token"

class Api::V1::ArticlesController < ApplicationController
  before_action :find_article, only: [:show, :update, :destroy]
  before_action :authenticate_user!
  #GET
  def index
    #故意测试只看得到自己建立的。
    @articles = current_user.articles
    render json:@articles, status: 200
  end

  #GET
  def show
    begin @article
      render json: @article, status: 200
    rescue
      render json: {error: "article not found!"}
    end
  end

  #PUT/POST
  def create
    #用关联性建立。
    @article = current_user.articles.new(article_params)
      if @article.save
        render json: @article, status: 200
      else
        render json: {erroe: "create failed"}
      end
  end

  #PUT/POST/PATCH
  def update
    if @article.update(article_params)
      render json: @article, status: 200
    else
      render json: {erroe: "update failed"}
    end
  end

  #DELETE
  def destroy
    @article.destroy
    render json: {message: "DELETE Done!"}
  end

  private
  #规定他人不能操作。
  def find_article
    @article = current_user.articles.find(params[:id])
  end

  def article_params
    params.require(:article).permit(
      :title,
      :author,
      :description
    )
  end
end

这边我是用两个帐号互相测试,确认过没问题。
请求的json如下。

GETarticles``articles/id

{"auth_token": "your token"}

POSTarticles
PATCHarticles/id

{"auth_token": "your token",
 "article" : {
      "title" : "兴奋到模糊",
      "author" : "剩最後一个礼拜",
      "description" : "加油!加油!加油!加油!"
             }
}

DELETEarticles/id

{"auth_token": "your token"}

OK!需有认证才能使用的API完成啦。
虽然这个API非常简单,可能只适合避免前後端吵架时用,但是至少也是了解大概怎麽运作了。
文件於此:https://github.com/nauosika/API-TEST


今天的leetcode.448 Find All Numbers Disappeared in an Array
老牌考古题?
题目连结:https://leetcode.com/problemsfind-all-numbers-disappeared-in-an-array/
题目重点:Ruby宝宝应该都是一行解决。

# @param {Integer[]} nums
# @return {Integer[]}
def find_disappeared_numbers(nums)

end

p find_disappeared_numbers([4,3,2,7,8,2,3,1]) #=> [5,6]
p find_disappeared_numbers([1,1]) #=> [2]

一般就。

(1..nums.size).to_a - nums

好看一点就

[*1..nums.size] - nums

但就想要手刻一个。
这题其实Constraints里有说到,元素里没有0,1 <= nums[i] <= n,那这样阵列会发生一个状况。

a = [2, 3, 1, 4]
2.7.3 :026 > (1..a.size).each do |num|
2.7.3 :027 >   puts b.include?(num)
2.7.3 :028 > end
true
true
true
true

b = [2, 2, 1, 4]
2.7.3 :030 > (1..b.size).each do |num|
2.7.3 :031 >   puts b.include?(num)
2.7.3 :032 > end
true
true
false
true
 => 1..4

所以我们只需要把发生false时的num丢进一个新阵列就好。

def find_disappeared_numbers(nums)
  ans = []
  (1..nums.size).each do |num|
      ans << num unless nums.count(num)
  end
  ans
end

可是这个答案错误,会耗时太久,因为要一个一个判断。


那我们先建立一个表单,是纪录哪些数字有出现,就是true,没出现就是false,比照原本nums时,false的才丢进去,减少一个一个判断的时间。

def find_disappeared_numbers(nums)
  nums_present = []
  ans = []

  nums.each do |num|
    nums_present[num] = true
  end

  (1..nums.size).each do |num|
    ans << num unless nums_present[num]
  end
  ans
end

完成!
但我还是当Ruby宝宝就好


<<:  Day17 决策树实作

>>:  DAY08 - 自制MOCK API,让你开发更方便

零基础Javascript,Day 01就不废话,开战。

零基础实战javascript(1)-我直接在B站加上按钮 效果: 在笔记本上加上一个按钮(图中橘色...

[DAY 14] 轮回故事里的那些因果 : RNN 简介

前言 这一场 LOL 的掉排位来自於之前自己已经雷了千万场 ---阿峻20190928 在资料的世界...

Day 1 序言及基本运算元件

我很早就开始接触组合语言,但没有学太久,就没有再碰了,当初学组合语言的原因,是觉得组合语言是人与机器...

{DAY 20} Pandas 学习笔记part.6

前言 这篇文章会进行到更多的资料操作 将会处理 Indexing Values 在标签值的处理很重...

10 | WordPress 图片区块 Image Block | 双色调滤镜 (Duotone Filter)

使用双色调并不是十麽新鲜的事情,最早可以追溯到 60 年代,传统的单色印刷或之後随着数码技术日渐流...