内容简介:How can you make sure your server confirms a user’s identity at every request while keeping high standards of performance and security?In this tutorial, you’ll learn how to leverageVapor 4’s authentication model to identify and persist users. When you’re d
Authentication is the process of identifying your users and verifying they are who they claim to be. This is separate from authorization , which sounds similar but actually handles protecting user data and making sure other users can’t access it.
How can you make sure your server confirms a user’s identity at every request while keeping high standards of performance and security?
In this tutorial, you’ll learn how to leverageVapor 4’s authentication model to identify and persist users. When you’re done, you’ll know what the most common authentication techniques are for server-side apps and how to implement two of those techniques with Vapor 4.
Getting Started
Use the Download Materials button at the top or bottom of this page to download everything you need for this tutorial.
The sample app for this tutorial is DiningIn , which lets people host dinners and invite friends to join them.
In the download materials, you’ll find two folders containing the starter and final projects. There are also two files, which contain calls to all the endpoints the server will have at the end of this tutorial. They’ll help you test the calls when you add the methods to the starter project.
DiningIn-API.paw works with Paw , a native API tool for macOS. If you’re used to Postman or you develop on a Linux machine, then the other file is for you.
Additionally, a SQLite browser will help you see the data the server saves. SQLite Browser is an open-source option that works well for this tutorial.
Setting up the Authentication Project
Open the DiningInStarter directory and either double-click Package.swift or enter, in your favorite terminal app, the following command:
open Package.swift
Xcode will open your project and automatically use the Swift Package Manager (SwiftPM) to start fetching the dependencies declared in Package.swift .
Looking at the Project
While SwiftPM does its job, take a few moments to familiarize yourself with the files located in the Sources/App directory.
The DiningIn starter project’s file tree
Start by looking at the classes defined in the Models directory:
- User : Represents a user entry in your server’s database.
- Dinner : Represents a scheduled dinner.
- DinnerInviteePivot : Links dinners and invited users.
When looking into these class properties, notice how Vapor 4 uses property wrappers, a new feature in Swift 5.1. The @ID
, @Field
and @Timestampable
wrappers, among others, play an essential role for Fluent, Vapor’s database framework.
Now, open the files in the Controllers folder to get a sense of how Vapor 4 handles routing.
Next, open the Migrations directory and check how you add a new table and its columns to the database. Also, pay attention to how each property has additional attributes, like constraints or references to other tables.
Finally, open configure.swift to see the initial configuration of the server: database initialization, middleware and configuring the database migrations described in the previous paragraph.
Note : For the sake of simplicity, this tutorial uses an on-disk SQLite database. In later stages of development, and certainly in production, you should use a scalable database running on a dedicated server. Good choices include PostgreSQL and MySQL.
Running the Starter Project
By now, SwiftPM has finished downloading all the dependencies. Click the Run button in Xcode or press Command-R to make sure the project compiles successfully.
Before moving to the hands-on part of the tutorial, it’s important to go over some theory. There are fundamental differences between a client-side app and a server app when dealing with users.
Later, you’ll learn how to model the data to allow the server to accept user registrations while also identifying the current user making the request.
Why Authentication and Authorization Are Essential on the Server
When you make client-side apps, you’re usually dealing with a single logged-in user. The app needs to be aware of only one current user and execute requests for that user alone.
However, when designing a server’s architecture, you need to think differently. Your server receives requests from all your users, so it’s aware of all users, and it has access to all their data.
Therefore, it must have a way to certify, for every request, the following two things:
- Authentication: Each user is who they claim to be.
- Authorization: Users can only access their own data and no one else’s.
You can compare the client app to a building where each resident has a storage room inside their own apartment. They own their own unit, and they’re only aware of their own storage, because they’re physically separated. They’re on different floors and in their own apartments.
The same is valid for a client-side app — the database and related files run in different installations, and they’re not necessarily aware of other users.
Each client has its own database to store user’s data
On the other hand, your server is like a shared mail room. An employee receives packages and writes down the recipient’s ID. Whenever anyone wants to get their packages, the employee checks the ID, verifies they are who they claim to be, checks where the package is stored and delivers it.
The same is true for the server’s role when dealing with users’ data.
The server stores all users’ data in a larger and more scalable database. Only the server should access it.
Note : This tutorial only deals with authentication — how to verify the identity of a user.
Authentication Mechanisms
It’s important to have an overview of the available authentication options, even though this tutorial will only cover two of them.
- Session Authentication : Upon login, the server generates a Session ID that the browser stores in a cookie. Every request the browser issues contains the Session ID; the server validates this cookie on every request. This mechanism is “stateful” — the server needs to keep track of the state of the session to validate it.
-
Basic Authentication
: This consists of sending the username and the password encoded in base 64 as the
Authorization
header. The value of the header looks likeBasic <encoded username:password>
. In theory, the server could use this method for every request. However, the client shouldn’t send the password over the network, even encoded, unless necessary. A good practice is to use Basic Authentication to log the user in, then to request a token. -
Bearer
: In this format, every request has an
Authorization
header where the value isBearer <token value>
. The client sends the token in every request to identify the user. Each logged-in device, even for the same user, will have a different token. The advantage of this approach is that the requests don’t contain the password. Plus, the server or client can revoke the tokens, which logs out the sessions related to that token. This is useful if someone steals a device or compromises the tokens. - JSON Web Tokens (JWTs) : A stateless version of the Bearer token. The server doesn’t keep a record of the token. Instead, it uses the encrypted data the JWT provides for every request.
- OAuth : An open standard for authorization. It allows an authorization server, which might be a third party like Facebook, Google or GitHub, to provide access to another server’s resources on behalf of a user.
This tutorial will not cover sessions, JWTs or OAuth. It will focus on basic and bearer authentication.
Adding Support for Token-Based Sessions
The starter project already contains the model and routes that allow users to sign up and fetch dinner information. The sign-up routes are ready, but you still need to implement user creation and token-based sessions.
Adding the Token Model
To start, select the Models folder, press Command-N to add a new Swift file , and name it Token.swift . Then replace the boilerplate with the following code:
import Vapor import Fluent enum SessionSource: Int, Content { case signup case login } //1 final class Token: Model { //2 static let schema = "tokens" @ID(key: "id") var id: UUID? //3 @Parent(key: "user_id") var user: User //4 @Field(key: "value") var value: String //5 @Field(key: "source") var source: SessionSource //6 @Field(key: "expires_at") var expiresAt: Date? @Timestamp(key: "created_at", on: .create) var createdAt: Date? init() {} }
Here’s what you’re doing in this code:
-
You declare the
Token
class, which implements Fluent’sModel
protocol. -
You implement the static variable
schema
, which Fluent uses to find the correct table in the database. You also implementid
, which is the object’s ID in the table. -
You add a field to store the user ID, with a relationship to the
User
table, to link every token to a user. - You add a field to store the value of the token itself, which clients will store and send in later requests.
-
To save the source of this session, you add a field for the
SessionSource
enum declared a few lines above when the user signs up and logs in. Eventually, this will work for other social logins as well. -
To allow expiring tokens, you add an
expiresAt
date field and another date field for the token’s creation date.
Note : The last two properties are not strictly required. However, it’s a good security practice to expire tokens and save their creation date.
Adding Initializers to Tokens
To allow users to create new tokens, this model needs an initializer. You’ll add this next.
Add the following below the empty init()
:
init(id: UUID? = nil, userId: User.IDValue, token: String, source: SessionSource, expiresAt: Date?) { self.id = id self.$user.id = userId self.value = token self.source = source self.expiresAt = expiresAt }
This is a standard initializer, which sets all the properties defined at creation. Notice the usage of the $
sign when setting the user. By doing this, you access the wrapper itself instead of the wrapped value and set the token’s user_id
based on the userId
, which is a UUID
.
Build and run the server. When the server starts, Vapor prints the path of the working directory, where you’ll find the .sqlite file, to the console. Using Finder or the terminal command line, open this location, then open DiningIn.sqlite in your SQLite browser:
The four tables present in the starter project.
There are four tables: users , dinners , a table linking them and another table that Fluent uses to manage migrations.
However, the tokens table doesn’t exist yet. It’s time to create it.
Creating the Migration
To create the tokens
table in the database, you need to create a Migration
and run it. One option is to run it manually via the command line, while the other is to automatically run migrations when the server starts. The latter option is already present in configure.swift
.
Add a file named CreateTokens.swift to the Migrations folder. Then replace the boilerplate with the following code:
import Fluent // 1 struct CreateTokens: Migration { func prepare(on database: Database) -> EventLoopFuture<Void> { // 2 database.schema(Token.schema) // 3 .field("id", .uuid, .identifier(auto: true)) .field("user_id", .uuid, .references("users", "id")) .field("value", .string, .required) .unique(on: "value") .field("source", .int, .required) .field("created_at", .datetime, .required) .field("expires_at", .datetime) // 4 .create() } // 5 func revert(on database: Database) -> EventLoopFuture<Void> { database.schema(Token.schema).delete() } }
Here’s what you’re doing in this code:
-
You declare a struct that conforms to the
Migration
protocol. -
In
prepare(on:)
, you create aSchemaBuilder
from thedatabase
with theschema
declared by theToken
. -
You add the fields based on their key, type and constraints. Notice how the
user_id
field has a reference to theid
field in the users table. -
You call
create()
to execute the operation of adding the table to the database. Notice the lack of thereturn
key in this method, which is new from Swift 5.1 in single expression function bodies.create()
returns a future of typeVoid
, which is exactly whatprepare(on:)
expects. -
The second required method,
revert(on:)
, should do what its name says: Apply the opposite changes from those you made in the previous method. You create the sameSchemaBuilder
, but now calldelete()
and return this operation.
Note
: You declare another property in the Migration
protocol: name
, of type String
. The default implementation generates a string from the struct or class name, but having it as a property allows you to set a custom name. The name should be unique for each migration.
Running the Migration
Once you’ve defined the migration, you need to add it.
Open configure.swift
and, after you call the last app.migrations.add(...)
, add the freshly-created migration:
app.migrations.add(CreateTokens())
Now, build and run, then open or refresh the SQLite browser. You’ll now see the tokens table!
The tokens table is now present, with all the fields defined in the migration.
Allowing Users to Sign up
Now that the server is ready for token-based sessions, you can start writing the methods that handle signup and login requests. The signup request returns a User
, which needs to include the session token generated upon signup.
Worry not, you’ll add this now!
Creating a New User
The response to a signup or login request should include both the user information and the generated token. This is exactly why the NewSession
struct exists.
Open Controllers/UserController.swift
and scroll down to create(req:)
.
The first thing to do is to change the return type. At the end of the line, replace EventLoopFuture<User.Public>
with this:
EventLoopFuture<NewSession>
The first three lines of this method do a validation of the request body, decode the body into UserSignup
, then create a new, not saved, user object by calling User.create(from: userSignup)
.
Jump to the definition of User.create(from:)
( Command-click
or Control-Command-click
create
) or just open Models/User.swift
and scroll to the bottom. You’ll see this method currently throws an Abort(.notImplemented)
error.
Replace this line with the following code:
User(username: userSignup.username, passwordHash: try Bcrypt.hash(userSignup.password))
You’re creating a new user object with a username and a password.
As a basic security practice, servers must never store the passwords as plain text, but should hash them instead. This is what the second line does, using Bcrypt
.
Note : This tutorial doesn’t enforce lowercase usernames so that you can keep your focus on authentication. You might consider storing and querying usernames in their lowercase variants to make it easy for your users to log in and to not confuse usernames.
Creating Tokens for a User
Next, you need to make it possible to create a new token for a given user.
Below User.create(from:)
, add the following method:
// 1 func createToken(source: SessionSource) throws -> Token { let calendar = Calendar(identifier: .gregorian) // 2 let expiryDate = calendar.date(byAdding: .year, value: 1, to: Date()) // 3 return try Token(userId: requireID(), //4 token: [UInt8].random(count: 16).base64, source: source, expiresAt: expiryDate) }
Here’s what you’re doing in this function:
-
This is a throwing function (more on that in step 3) on
User
, which receives aSessionSource
and returns a newToken
. -
You use a
Calendar
to generate a date a year ahead of the current date, which you’ll use as the expiry date. Change this line if you want a shorter or longer expiry date. -
Using the initializer you created in the previous section, you generate the token.
requireID()
throws an error when the user isn’t in the database and doesn’t have an ID yet. - Generate the token value itself by creating 16 random bytes and getting the token’s Base 64-encoded string representation.
Including the Token in the Response
Once you’ve implemented createToken(source:)
, it’s time to activate it in the user controller.
Go back to Controllers/UserController.swift
‘s create(req:)
. Add a declaration for the token below the user
property:
var token: Token!
Now, replace the .flatMapThrowing
closure after return user.save(on: req.db)
with the following:
.flatMap { // 1 guard let newToken = try? user.createToken(source: .signup) else { return req.eventLoop.future(error: Abort(.internalServerError)) } // 2 token = newToken return token.save(on: req.db) }.flatMapThrowing { // 3 NewSession(token: token.value, user: try user.asPublic()) }
This is what you’re doing in this chunk of code:
- You create a new token from the fresh user you just saved. If it fails, it throws an error.
- You keep a reference to the token and save it.
-
Upon completion, you initialize the
NewSession
with thetoken
value and theUser.Public
from theuser
. TheUser.Public
struct is a technique used to send information to the clients without exposing fields that are internal to the server.
It’s finally time to test the first API you created with this tutorial!
Build and run, then open the Paw or Postman API file from this tutorial’s download materials. Select the (1) Sign up request, set the server URL to localhost:8080 , then send the request.
You’ll get a JSON response that includes both the token and the user:
{ "token": "t+oHBUwU2Rv+qp7O2Ed0UQ==", "user": { "username": "NatanTheChef", "id": "138191B9-445D-442D-9F70-B858081A661B", "updated_at": "2020-03-07T19:40:54Z", "created_at": "2020-03-07T19:40:54Z" } }
Note
: Your token
and id
values will be different.
Congratulations! You’ve created a new user and token, and returned that token in your server response. Next, you’ll use that token to authenticate the user.
Authenticating the User With a Token
There are two ways to confirm the server saved the user: The first is to use the SQLite browser by opening the users table to see the new record — select the users table, then select the Browse Data tab. The second is to add an endpoint allowing users to fetch their own information.
Your next step will be to add that endpoint.
Supporting Basic Authentication on the User Model
In order for Vapor to know that a User
instance can be used for authentication, you need to conform User
to ModelAuthenticatable
. Vapor uses this protocol to perform all the steps around authenticating a user with a username and a password in the request headers and to link it to tokens.
Open User.swift and add the following extension:
extension User: ModelAuthenticatable { // 1 static let usernameKey = \User.$username static let passwordHashKey = \User.$passwordHash // 2 func verify(password: String) throws -> Bool { try Bcrypt.verify(password, created: self.passwordHash) } }
Fluent’s ModelAuthenticatable
protocol is succinct. First, it requires two KeyPath
s, where the value is a field of type String
. This tells Vapor which fields to look for when querying the user upon authentication.
Secondly, it requires your user model to implement verify(password:)
, which determines whether the received password matches the password hash and returns the result. As with the registration, this function also uses Bcrypt
to perform this check.
Conforming a Token to the ModelTokenAuthenticatable Protocol
Next, Vapor needs to know that it can use the Token
for authenticating users. This lets it provide the authentication middleware, which is able to find the user for a token supplied in the request authentication header.
Open Models/Token.swift and add this extension:
extension Token: ModelTokenAuthenticatable { //1 static let valueKey = \Token.$value static let userKey = \Token.$user //2 var isValid: Bool { guard let expiryDate = expiresAt else { return true } return expiryDate > Date() } }
Fluent’s ModelTokenAuthenticatable
protocol is also very concise. First, it needs two KeyPath
s — one for the token value field and another for the user relationship. Next, it checks the token to see if it’s valid at a specific moment. If your tokens don’t expire, simply return true
.
In this case, you take expiresAt
and compare it to the current date. If the current date is later than the expiry date, it’s invalid, causing Vapor to delete the token from the database and return 401 unauthorized
.
Adding the Me Endpoint
Now Vapor can provide the token authenticator middleware. Go back to UserController.swift
and add the following lines to the end of boot(routes:)
:
let tokenProtected = usersRoute.grouped(Token.authenticator()) tokenProtected.get("me", use: getMyOwnUser)
The first line creates a router using the /users
path defined in the first line of this method, and also wraps them in an authentication middleware. This means that every request going through the middleware requires an authenticated user.
The second line makes the /users/me
endpoint use getMyOwnUser(req:)
. Scroll down to this function and replace the thrown error with the following line:
try req.auth.require(User.self).asPublic()
This returns the user information by accessing the request’s authentication cache, fetches the user who’s performing the request and converts it to a public user. If the request isn’t authenticated, then it throws a 401 unauthorized
error.
Whenever you need to get the current user from within a request, simply use req.auth.require(User.self)
, as long as the request came through an authentication middleware, as shown above.
Build and run, then send the (2) Me request in the API file. The response should contain the user object you just signed up:
{ "username": "NatanTheChef", "id": "138191B9-445D-442D-9F70-B858081A661B", "updated_at": "2020-03-07T19:40:54Z", "created_at": "2020-03-07T19:40:54Z" }
Note : If you get an Unauthorized error, run the Sign up request again, to reset the token.
Adding the Login Endpoint
So far, users can sign up but cannot log in, so the server is only doing half its job. But don’t worry: Part of the code you added in the previous section makes dealing with login requests much easier.
Implementing the Login Route
Because User
conforms to ModelAuthenticatable
, you can use the User
basic authentication middleware that Vapor provides out of the box.
Begin by adding the route in the boot(routes:)
method of UserController
:
let passwordProtected = usersRoute.grouped(User.authenticator().middleware()) passwordProtected.post("login", use: login)
This is similar to the /users/me
endpoint, but instead of using the Token
authenticator, or bearer, it uses the User
basic authenticator. Now, scroll down to the login(req:)
method, delete the thrown error and add the code below:
// 1 let user = try req.auth.require(User.self) // 2 let token = try user.createToken(source: .login) return token .save(on: req.db) // 3 .flatMapThrowing { NewSession(token: token.value, user: try user.asPublic()) }
Step-by-step, this is what you’re doing:
-
Similarly to the
me
function, you get theuser
from the request authentication cache. Although the authentication mechanism is different, the approach is the same. Vapor works behind the curtains to authenticate and provide the user. -
Using the same
createToken(source:)
onuser
, you generate a new token, this time passingSessionSource.login
. You save the new token to the database. -
Once you save the token, you wrap the token’s value and the user in a
NewSession
and return it in the response.
You’re now ready to test the /users/login
endpoint.
Build and run one last time, then open the API file and send the (3) Login request. You should see a response similar to what you got from the (1) Sign up request, but with a new token, fresh from the oven:
{ "token": "5/2BdXtsAZaLBPOCKCDgow==", "user": { "username": "NatanTheChef", "id": "138191B9-445D-442D-9F70-B858081A661B", "updated_at": "2020-03-07T19:40:54Z", "created_at": "2020-03-07T19:40:54Z" } }
Note : If you changed the username or password when you signed up, select the Authorization tab, then Basic Auth and insert the username and password you used to sign up.
Congratulations! You’re ready to implement user authentication for your app using both bearer tokens and basic authentication headers.
Where to Go From Here?
Download the final project using the Download Materials button at the top or bottom of the page. Here are a few challenges you could try to tackle going forward:
- Make a logout function that invalidates, revokes or deletes a token.
-
Add methods allowing users to reset their password. One way to do this is by creating a
ResetPasswordToken
Model
which has an expiration date, an identifier and is linked to a user. Then send this unique link via email. - Allow logging in with Magic Links sent via email.
-
If you want a challenge, try to implement authorization in
DinnerController.swift
, making sure that only a host can invite users to aDinner
, and that only the invitees and the host can fetch a dinner’s information.
If you work with Amazon Web Services, take a look at my other tutorial SMS user authentication with Vapor and AWS
We hope you enjoyed this tutorial. If you have any questions or comments, feel free to join in the forum discussion below!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
智能优化算法及其应用
王凌 / 清华大学出版社 / 2001-10 / 22.00元
智能优化算法及其应用,ISBN:9787302044994,作者:王凌著一起来看看 《智能优化算法及其应用》 这本书的介绍吧!