本文为 Perfect 官方投稿,作者 Rocky,版权归原作者所有,未经作者同意,请勿转载。
原文地址: http://mp.weixin.qq.com/s/_p5oNEQkSZ_qTnHlM9xlEA
技术之路,共同进步,欢迎投稿、约稿,给文章纠错,请发送邮件至mobilehub@csdn.net。
GSSAPI / SPNEGO 是一个很有意思的互联网标准,可能习惯 OAuth 的新生代程序员都不是很熟悉,但是可能大家都不知道所有的 Windows 服务器、苹果的 Apple TV,以及常见的局域网内文件共享、以及银行的各种“盾”,其实都在默默地使用这个协议。
具体放在 HTTP 服务器上的时候,究竟这个协议在干什么呢?
其实很简单。比如,你希望把这个服务器放到公网上,但是希望只有真正授权的用户能够访问,而且授权不是一个简单的用户名密码;而且这个服务器的用户后台很复杂,你根本不想去动这些包含了上百万个用户账号的老服务器,该怎么办呢?答案当然是 SPNEGO。
SPNEGO 最显著的特点,就是用 Kerberos 去登录,你自己开发的服务器中根本就不需要包含任何关于登录的页面和后台数据库!!!唯一需要考虑的就是 ACL 安全访问控制列表——SPNEGO 交给你的,只有一个用户名,即通过 Kerberos 安全审查的用户名称,然后再自行核对一下该用户是否有权读取当前页面即可,就这么简单!Kerberos 会自动根据用户登录结果发放访问凭证,如果超时凭证就过期,所以用户会感觉在局域网内开发程序那样省事儿。麻省理工的技术不是吹的,对吧?
让我们看看 Perfect 官网的程序吧:
本项目为包含有 SPNEGO 功能的 Perfect 项目模板
本项目是基于 PerfectTemplate 服务器模板。如果您还不熟悉 PerfectTemplate,请最好先尝试一下。
本项目使用 Swift 3.0.2 工具链,支持 Ubuntu 和 Mac OS X
如果您希望使用 Xcode 编译该项目,请确保将下列编译标识正确传递给 SPM 软件包管理器:
$ swift package -Xlinker -framework -Xlinker GSS generate-xcodeproj
编译本项目之前请确保 libkrb5-dev 函数库已经正确安装。
$ sudo apt-get install libkrb5-dev
请配置好您的应用服务器/etc/krb5.conf,以便于该服务器能够正常连接到目标的 KDC。请参考下面的例子,在这个例子中,应用服务器希望连接到控制区域 KRB5.CA,并且该控制区域的 KDC 控制中心域名为 nut.krb5.ca:
[realms] KRB5.CA = { kdc = nut.krb5.ca admin_server = nut.krb5.ca } [domain_realm] .krb5.ca = KRB5.CA krb5.ca = KRB5.CA
下一步需要联系您的 KDC 管理员,获得应用服务器将使用的 keytab 钥匙文件。
参考下面的例子的示范配置,:warning:下列所有主机必须注册到同一个 DNS:warning:
如果处于上述环境,则 KDC 管理员需要登录 nut.krb5.ca 控制区域并采取下列操作:
kadmin.local: addprinc -randkey HTTP/apple.krb5.ca@KRB5.CA kadmin.local: ktadd -k /tmp/krb5.keytab HTTP/apple.krb5.ca@KRB5.CA
生成钥匙文件 krb5.keytab 后,请将该钥匙安全地传输到您的应用服务器 apple.krb5.ca
并将文件移动到目录 /etc
下,然后赋予其适当权限,以便于您的服务器可以访问到。
运行下列命令可以编译本项目并在 8080 端口上启动包含 SPNEGO 机制的服务器。
git clone https://github.com/PerfectExamples/Perfect-SPNEGO-Demo.git cd Perfect-SPNEGO-Demo swift build .build/debug/PerfectTemplate
可以看到服务器运行后的启动信息:
SPNEGO IS READY [INFO] Starting HTTP server localhost on 0.0.0.0:8080
证明服务器已经准备好。
现在您可以尝试访问一下 http://apple.krb5.ca:8080
看看会发生什么事情。
如果您正在使用兼容 SPNEGO 的浏览器,那么可能会看到一个登录框,提示输入用户名密码。 或者,您可以使用下列命令行用于更清楚地理解整个验证过程:
$ kinit $ curl -v --negotiate -u : http://apple.krb5.ca:8080
此时,应该可以注意到,如果没有使用 kinit 登录,则 curl 返回的是禁止访问。
请注意如果希望能够保证该范例能够正确运行,请使用 FQDN (完全限定域名) 来配置 URL 路径,而不是使用 localhost
或 IP 地址,并且服务器和客户端都需要配置为这种域名。
示范代码在原有 PerfectTemplate 基础上追加了几行程序,用于验证用户身份:
首先,接收到客户请求后,初始化了一个 SPNEGO 对象。 随后,处理器会将 HTTP 响应设置为 .unauthorized
表示需要授权,即指示客户端必须提供 SPNEGO 的有效凭证进行身份验证。 如果 HTTP 请求中已经包含了一个 Base64 编码的票据,那么服务器应该尝试使用这个方法接收票据并获取用户身份信息: let (username, reply_token) = try spnego.accept(base64Token: inputToken)
如果返回的用户名非空,则确认用户身份已经验证。此时您可能需要额外的 ACL(访问控制列表)进一步验证用户是否有权访问当前 URL 指向的资源,否则应该拒绝该请求。
请注意返回的另外一个参数 reply_token
如果非空,则需要发回给客户端以完成整个验证过程。
import PerfectSPNEGO import Darwin // 注意,主机名必须是完全限制名称 FQDN,并且已经在 KDC 上注册登记并获取合法 keytab 文件。 let hostname = "apple.krb5.ca"// 收到安全保护的请求处理器 func handler(data: [String:Any]) throws -> RequestHandler { return { request, response in // 首先将返回状态设置为禁止访问 response.status = .unauthorized // 服务器和客户机时间必须同步,因此返回一个 GMT 时间 response.setHeader(.date, value: GMTNow()) // 提示客户机必须进行协议协商 response.setHeader(.wwwAuthenticate, value: "Negotiate") response.setHeader(.contentType, value: "text/html") // 如果客户机无有效凭证则拒绝访问 guard let auth = request.header(.custom(name: "Authorization")) else { response.appendBody(string: "<html><H1>ACCESS DENIED</H1></html>/n") response.completed() return }//end auth // 从客户机请求中提取身份验证票据凭证 let negotiate = "Negotiate " guard auth.hasPrefix(negotiate) else { response.appendBody(string: "<html><H1>INVALID TOKEN FORMAT</H1></html>/n") response.completed() return }//end auth.prefix // 将凭证转化为标准格式 let inputToken = String(auth.characters.dropFirst(negotiate.utf8.count)) do { let spnego = try Spnego("HTTP@/(hostname)") // 尝试接收申请 let (username, outputToken) = try spnego.accept(base64Token: inputToken) // 检查服务器是否需要返回收条 if let reply = outputToken { response.setHeader(.custom(name: "Authorization"), value:"Negotiate /(reply)") }//end if // 检查是否已经完成身份验证 if let user = username { // 用户登录已经成功 // 注意,在生产服务器中,您可能需要进一步使用 ACL 安全访问列表来检查已验证身份的用户是否有足够的权限访问当前 URL 资源 response.status = .accepted // 返回受保护的资源内容 response.appendBody(string: "<html><title>Hello, world!</title><body>Welcome, /(user)</body></html>/n") }else { // 验证失败,有可能是 DNS 错误造成,请务必使用 FQDN 完全限制域名 response.appendBody(string: "<html><H1>GSS REQUEST TO CALL IT ONCE MORE</H1></html>/n") }//end if }catch (let err) { // 用户身份验证失败 response.appendBody(string: "<html><H1>AUTHENTICATION FAILED: /(err)</H1></html>/n") }//end du // 完成身份验证 response.completed() } }