Vapor(四)验证篇
常用的服务端的验证方式有Cookie-Session、JWT,下面主要围绕这两种验证方式说明,顺带也介绍了Cookie的密码和Oauth Token两种验证方式。
JWT
JSON WEB Token(JWT),是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。
头信息:
header = '{"alg":"HS256","typ":"JWT"}'
消息体:
payload = '{"id": 42,"name": "Vapor Developer"}'
未签名的令牌由base64url编码的头信息和消息体拼接而成(使用”.”分隔),签名则通过私有的key计算而成:
key = 'secretkey'
unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload)
signature = HMAC-SHA256(key, unsignedToken)
后在未签名的令牌尾部拼接上base64url编码的签名(同样使用”.”分隔)就是JWT了:
token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)
//<header>.<payload>.<signature>
实现的步骤一般是
- 客户端通过用户名和密码登录服务器;
- 服务端对客户端身份进行验证;
- 服务端对该用户生成Token,返回给客户端;
- 客户端将Token保存到本地浏览器,一般保存到cookie中;
- 客户端发起请求,需要携带该Token;
- 服务端收到请求后,首先验证Token,之后返回数据。
优点是服务端不需要保存Token,只需要对Token中携带的信息进行验证即可;无论客户端访问后台(集群中的)的哪台服务器,只要可以通过用户信息的验证即可。
创建Codable
类型的模型来承载JWT里的Payload
struct User: JWTPayload {
var id: Int
var name: String
func verify(using signer: JWTSigner) throws {
// nothing to verify
}
}
JWT的解析和验证
import JWT
import Vapor
router.get("hello") { req -> String in
// fetches the token from `Authorization: Bearer <token>` header
guard let bearer = req.http.headers.bearerAuthorization else {
throw Abort(.unauthorized)
}
// parse JWT from token string, using HS-256 signer
let jwt = try JWT<User>(from: bearer.token, verifiedUsing: .hs256(key: "secret"))
return "Hello, \(jwt.payload.name)!"
}
创建一个JWT给客户端保存
router.post("login") { req -> String in
// create payload
let user = User(id: 42, name: "Vapor Developer")
// create JWT and sign
let data = try JWT(payload: user).sign(using: .hs256(key: "secret"))
return String(data: data, encoding: .utf8) ?? ""
}
Sessions
Session的中文翻译是“会话”,当用户打开某个web应用时,便与Web服务器产生一次Session。服务器使用Session把用户的信息(Token、账密)临时保存在了服务器上,用户离开网站后Session会被销毁。这种用户信息存储方式相对直接将用户信息存在Cookie来说更安全。可是Session有一个缺陷:如果web服务器做了负载均衡,那么下一个操作请求到了另一台服务器的时候Session会丢失。
或者说客户端在服务端登陆成功之后,服务端会生成一个sessionID返回给客户端,客户端将sessionID保存到Cookie中,再次发起请求的时候携带Cookie中的sessionID到服务端,服务端会缓存该Session(会话),当客户端请求到来的时候,服务端就知道是哪个用户的请求,并将处理(sessionID映射出用户信息进行验证)的结果返回给客户端,完成通信。缺点是访客量大时服务端需要消耗较大空间保存Session,其次是集群服务端下需采用缓存一致性技术保存Session。
一般通过中间件实现逻辑后添加在路由上。验证是检验用户的身份,不要和获取资源权限的授权混淆。
添加鉴定的中间件服务
config.prefer(MemoryKeyedCache.self, for: KeyedCache.self) //优先使用内存缓存
var middlewares = MiddlewareConfig()
middlewares.use(SessionsMiddleware.self)
// ...
services.register(middlewares)
模型遵守鉴定协议
extension User: SessionAuthenticatable { }
具鉴定能力的路由器从请求头中取出sessionId,鉴定请求的用户
// 创建User的验证session中间件
let session = User.authSessionsMiddleware()
// 创建被中间件包裹的路由器
let auth = router.grouped(session)
// 在间接路由器auth中实现路由,并获取供鉴定的信息转为对应的模型
auth.get("hello") { req -> String in
let user = try req.requireAuthenticated(User.self) //require前缀代表在验证失败时会抛出异常
return "Hello, \(user.name)!"
}
登录的路由,也必须使用鉴定中间件所捆绑的路由器中实现,并在响应前将用户model使用.authenticate缓存到服务端,否则后面(同一个用户)通过该路由器的其它请求里的验证会无效
auth.get("login") { req -> Future<String> in
return User.find(1, on: req).map { user in
guard let user = user else {
throw Abort(.badRequest)
}
try req.authenticate(user) return "Logged in"
}
}
登录成功后,会发现一个叫 “vapor-session” 的 Cookie 已经存在于浏览器上。而像/login/hello这样的访问路径,会先登录再访问hello。
Stateless
这是一个最初级的验证方式。为保护接口端点,引入了一种无状态验证方法,同样也是一种验证用户身份的方法,而非获取资源权限的授权。
- Cookie 密码验证(Basic)
实质就是利用Cookie保存用户名和密码在客户端,在每次请求时带上这个Cookie作为请求的验证头。
包含的账号名和密码,格式为
Authorization: Basic base64(username:password)
同时需要可验证的目标模型遵从PasswordAuthenticatable
,给中间件接入(需要注意密码应该使用hash值)
extension User: PasswordAuthenticatable {
/// See `PasswordAuthenticatable`.
static var usernameKey: WritableKeyPath<User, String> {
return \.email
}
/// See `PasswordAuthenticatable`.
static var passwordKey: WritableKeyPath<User, String> {
return \.passwordHash
}
}
创建可验证的模型后,就为要保护的路由添加中间件(在实践中要用basicAuthMiddleware去包裹authSessionsMiddleware包裹的路由器才能验证成功)
// Use user model to create an authentication middleware
let password = User.basicAuthMiddleware(using: BCryptDigest()) //使用 BCryptDigest 来验证存储为 BCrypt hash的密码。
// Create a route closure wrapped by this middleware
router.grouped(password).get("hello") { req in
///
}
- Oauth Token验证(Bearer)
Token的意思是“令牌”,是用户身份的验证方式,最简单的Token组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,由Token的前几位+盐以哈希算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接Token请求服务器)。还可以把不变的参数也放进Token,避免多次查库。
在每一个请求API的验证头上添加一个暂时的token,一般是
Authorization: Bearer hash(uid+timestamp+sign)
首先使可验证模型遵从Authenticatable
协议
struct User: Model {
var id: Int?
var name: String
var email: String
var passwordHash: String
var tokens: Children<User, UserToken> {
return children(\.userID)
}
}
extension User: TokenAuthenticatable {
/// See `TokenAuthenticatable`.
typealias TokenType = UserToken
}
然后需要额外定义一个Token类
struct UserToken: Model {
var id: Int?
var string: String
var userID: User.ID
var user: Parent<UserToken, User> {
return parent(\.userID)
}
}
extension UserToken: Token {
/// See `Token`.
typealias UserType = User
/// See `Token`.
static var tokenKey: WritableKeyPath<UserToken, String> {
return \.string
}
/// See `Token`.
static var userIDKey: WritableKeyPath<UserToken, User.ID> {
return \.userID
}
}
最后为要保护的路由添加中间件
// Use user model to create an authentication middleware
let token = User.tokenAuthMiddleware()
// Create a route closure wrapped by this middleware
router.grouped(token).get("hello") { req in
let user = try req.requireAuthenticated(User.self)
return "Hello, \(user.name)."
}
Cookie 与 Session
- Cookie数据存放在客户的浏览器上,Session数据放在服务器上。
- Cookie不是很安全,别人可以分析存放在本地的Cookie并进行Cookie欺骗,考虑到安全应当使用Session。
- Session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能。考虑到减轻服务器性能方面的话,应当使用Cookie。
- 单个Cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个Cookie。
- 一般情况,将登陆信息等重要信息存放为Session,其他信息如果需要保留,可以放在Cookie中。
Token 与 Session
Session 和 Oauth Token并不矛盾,作为身份认证 Token安全性比Session好,因为每个请求都有签名还能防止监听以及重放攻击,而Session就必须靠链路层来保障通讯安全了。如上所说,如果你需要实现有状态的会话,仍然可以增加Session来在服务器端保存一些状态。
App通常用restful api跟server打交道。Rest是stateless的,也就是App不需要像browser那样用Cookie来保存Session,因此用session token来标示自己就够了,session/state由api server的逻辑处理。 如果你的后端不是stateless的rest api, 那么你可能需要在App里保存Session。可以在app里嵌入webkit,用一个隐藏的browser来管理cookie session。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 mingfungliu@gmail.com
文章标题:Vapor(四)验证篇
文章字数:2.2k
本文作者:Mingfung
发布时间:2018-08-16, 19:06:00
最后更新:2019-09-03, 09:52:56
原始链接:http://blog.ifungfay.com/后端/Vapor(四)验证篇/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。