Vapor(四)验证篇

  1. JWT
  2. Sessions
  3. Stateless
  4. Cookie 与 Session
  5. Token 与 Session

常用的服务端的验证方式有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>

实现的步骤一般是

  1. 客户端通过用户名和密码登录服务器;
  2. 服务端对客户端身份进行验证;
  3. 服务端对该用户生成Token,返回给客户端;
  4. 客户端将Token保存到本地浏览器,一般保存到cookie中;
  5. 客户端发起请求,需要携带该Token;
  6. 服务端收到请求后,首先验证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)."
}
  1. Cookie数据存放在客户的浏览器上,Session数据放在服务器上。
  2. Cookie不是很安全,别人可以分析存放在本地的Cookie并进行Cookie欺骗,考虑到安全应当使用Session。
  3. Session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能。考虑到减轻服务器性能方面的话,应当使用Cookie。
  4. 单个Cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个Cookie。
  5. 一般情况,将登陆信息等重要信息存放为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" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏

宝贝回家