Teapot:Web编程入门





5.00/5 (6投票s)
使用您能想象到的最简单的编程语言和框架创建Web应用程序的技巧
引言
这是对原文章《Smalltalk编程语言简介》的后续。假设您熟悉Pharo,现代Smalltalk。
Teapot是一个漂亮、简单、易用的微Web框架,用于创建Web服务和Web应用程序。它非常类似于Python的Flask、Ruby的Sinatra和Java的Spark。
(Teapot基于Zinc HTTP Components框架,Sven Van Caekenberghe为此框架编写了优秀的教程)。
我们的第一个Teapot应用程序将是一个相当全面的登录管理前端。为此,我们需要四样东西:
- 密码加密机制(使用Pierce Ng的PasswordCrypt)
- 用于存储登录凭证的数据库(使用
MongoDB
和VoyageMongo
) - 发送电子邮件机制(使用Zodiac的
#ZdcSecureSMTPClient
) - 生成UUID的方法(使用
#UUIDGenerator
)
安装必要的组件
要安装Teapot,请在Playground中执行此操作:
Gofer it
smalltalkhubUser: 'zeroflag' project: 'Teapot';
configuration;
loadStable.
我们将使用PBKDF2进行密码加密。我们将使用FFI调用C库来实现这一点,因为C代码速度更快。如果您的网站需要每分钟处理数十次登录,性能将是一个真正的问题。
我们将需要OpenSSL C库。
sudo apt-get install libssl-dev
PasswordCrypt的C库必须编译。使用提供的Makefile。(取决于您的开发系统,您可能能够也可能无法使用‘-m32
’标志。如果不能,只需删除该标志。)将C文件放在同一文件夹中,然后执行:
make
将库文件放入Pharo文件夹(或Pharo VM所在的位置)。
将PasswordCrypt
加载到Pharo
中:
Metacello new
baseline: 'PasswordCrypt';
repository: 'github://PierceNg/PasswordCrypt/src-st';
load
我们将使用流行的MongoDB
作为我们的数据库存储。例如,要在Debian Linux中设置MongoDB
,请执行以下操作:
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install mongodb-serve
mongo # run mongo shell
sudo service mongodb start # start mongodb as a service
sudo service mongodb stop # stop the service
Voyage是一个对象持久性抽象层,可以与Mongo一起使用。从Catalog Browser安装VoyageMongo
(单击绿色复选标记以安装稳定版本)。这里有一些关于VoyageMongo
的入门材料。
要通知Voyage有关Mongo的信息,请在Playground中执行此操作:
|repo|
repo := VOMongoRepository
host: VOMongoRepository defaultHost
database: 'NCTDB'.
VORepository setRepository: repo.
编写代码
我们的数据库名为‘NCTDB
’,数据库模型由#NCTUser
类表示。我们这样创建模型:
Object subclass: #NCTUser
instanceVariableNames: 'name user pwdHash pwdSalt uuid
creationDate accessDate'
classVariableNames: ''
poolDictionaries: ''
category: 'NCT-Tutorial'
NCTUser class>>isVoyageRoot "a class-side method"
^ true
NCTUser class>>voyageCollectionName "a class-side method"
^ 'NCTUsers'
同样,我们希望为实例变量创建所有“访问器”。
我们希望以下信息存储在数据库“集合”(由“文档组成,使用Mongo术语)中:
name
— 用户的全名(可选)user
— 这是用户的电子邮件地址,保证是唯一的pwdHash
和pwdSalt
— 加密后的密码及其关联的saltuuid
— UUID是一个128位数字,用于(几乎)唯一标识某物或某人(在我们的例子中是用户)creationDate
— 用户注册的日期;可能用于审计或帐户过期accessDate
— 用户上次登录的日期;可能用于确定帐户的“陈旧”程度
创建新用户文档就像这样简单:
pwd := 'Amber2018'. "Amber Heard will be my girlfriend in 2018!"
salt := 'et6jm465sdf9b1sd'.
(NCTUser new)
name: 'Richard Eng';
user: 'horrido.hobbies@protonmail.com';
pwdHash: PasswordCrypt sha256Crypt: pwd withSalt: salt;
pwdSalt: salt;
uuid: UUID new hex asUppercase;
creationDate: DateAndTime today;
save.
要使用#ZdcSecureSMTPClient
,您必须为用于发送电子邮件的Gmail帐户启用“允许不安全的应用程序”。这用于用户帐户的电子邮件验证。
茶马古道
Teapot基于路由的概念。一个路由包含三个部分:
- HTTP 方法
- URL模式
- 操作 — 它可以是一个块、一个消息发送或一个对象。
路由列表基本上构成了您的Web应用程序。
initialize
Teapot stopAll. "reset everything"
Teapot on
GET: '/register' -> [ self registerPage: 0 name: ''
user: '' pwd: '' pwd2: '' ];
POST: '/register' -> [ :req | self verifyRegistration: req ];
GET: '/verify/<uuid>' -> [ :req | self verifyUUID: req ];
GET: '/login' -> [ self loginPage: 0 user: '' pwd: '' ];
POST: '/login' -> [ :req | self verifyLogin: req ];
before: '/welcome/*' -> [ :req |
req session attributeAt: #user
ifAbsent: [ req abort: (TeaResponse redirect
location: '/login') ] ];
GET: '/welcome/<name>' -> [ :req | self mainPage: req ];
GET: '/forgot' -> [ self forgotPage: '' ];
POST: '/forgot' -> [ :req | self handleForgot: req ];
before: '/profile/*' -> [ :req |
req session attributeAt: #user
ifAbsent: [ req abort: (TeaResponse redirect
location: '/login') ] ];
GET: '/profile' -> [ :req | self profilePage: req ];
POST: '/profile' -> [ :req | self handleProfile: req ];
GET: '/logout' -> [ :req | self logout: req ];
GET: '/books' -> [ :req | 'Check ',(req at: #title),' and ',
(req at: #limit) ]; "this route demonstrates how to pass
parameters in the URL, eg,
/books?title=The Expanse&limit=8"
start
例如,当您在Web浏览器中访问(虚构URL的)登录页面时……
http://nct.gov/login "nct.gov is a fictitious domain;
normally, you will register your own domain
and configure your web app to use it"
……您将看到一个用于输入用户名和密码的Web表单。这会激活路由:
GET: '/login' -> [ self loginPage: 0 user: '' pwd: '' ]
方法#loginPage:user:pwd:
呈现HTML代码以渲染网页。当您将表单信息提交给Web服务器时,这会激活路由……
POST: '/login' -> [ :req | self verifyLogin: req ]
……其中#verifyLogin:
处理表单信息,并在成功验证后,您将被带到主Web页面。
GET: '/welcome/<name>' -> [ :req | self mainPage: req ]
方法#mainPage:
呈现HTML代码以渲染“欢迎”页面。
注册页面(‘/register
’)、“忘记密码?”页面(‘/forgot
’)和用户配置文件页面(‘/profile
’)也适用类似的流程。
有些路由不呈现网页,例如‘/logout
’,它只是让您退出登录并重定向到登录页面。
参数‘req
’是与HTTP方法关联的HTTP请求。它包含一个‘session
’,可用于存储“全局
”信息。在我们的例子中,我们将存储‘user
’(或电子邮件地址),以便确定用户何时已登录。我们还将存储‘uuid
’,因为这很有趣!
路由#before:
是一个过滤器,在紧随其后的GET
:请求之前进行评估。该过滤器用于确保用户在访问网页之前已登录。
用户看到的内容
各种网页代表我们应用程序的面向公众的视图。它包括一个“样式表”(包含CSS指令)和大量的HTML代码。以下是Login
页面:
loginPage: code user: user pwd: pwd
^ '<html> <head>',self stylesheet,'</head>
<body>
<h2>Login</h2>
<div>
<form method="POST">
Email:<br>', (self errCode: (code bitAnd:
self class ErrBadEmail)), '
<input type="text" name="user" value="',user,'"><br>
Password:<br>', (self errCode: (code bitAnd:
self class ErrBadPassword)), '
<input type="password" name="pwd" value="',pwd,'"><br><br>
<input type="submit" value="Submit">
</form>
<p><a href="/forgot">Forgot your password?</a></p>
<p><a href="/register">Sign up now!!</a></p>
</div>
</body>
</html>'
这是注册页面:
registerPage: code name: name user: user pwd: pwd pwd2: pwd2
^ '<html> <head>',self stylesheet,'</head>
<body>
<h2>Register</h2>
<div>
<form method="POST">
Fullname:<br>
<input type="text" name="name" value="',name,'"><br>
Email:<br>', (self errCode: (code bitAnd:
self class ErrBadEmail)), '
<input type="text" name="user" value="',user,'"><br>
Password:<br>', (self errCode: (code bitAnd:
self class ErrBadPassword)), '
<input type="password" name="pwd" value="',pwd,'"><br>
Password (confirm):<br>', (self errCode: (code bitAnd:
self class ErrNoPasswordMatch)), '
<input type="password" name="pwd2" value="',pwd2,'">
<br><br>
<input type="submit" value="Submit">
</form>
</div>
</body>
</html>'
个人资料页面和“忘记密码?”页面类似。关于#bitAnd:
,它是一个按位运算符,将数字视为二进制数字序列。对于操作数中每个对应的位对,‘and’(或‘&’)运算符会产生以下结果:
- 0 & 0 -> 0
- 0 & 1 -> 0
- 1 & 0 -> 0
- 1 & 1 -> 1
对于‘or’(或‘|’)、‘xor’(或‘^’)和‘not’(或‘~’)也有类似的运算符。
但是请注意,Smalltalk将位从1编号到16,而不是0到15!(这类似于Smalltalk从元素1开始的数组,而不是元素0。)顺便说一句,在Smalltalk中,整数不限于32位或64位,因此这些按位运算符可用于非常大的数字!
变量‘code
’是编码多个错误消息的便捷方法。
这是样式表:
stylesheet
^ '<style>
body {
background-image:
url(https://cdn-images-1.medium.com/max/2000/1*QVTC39_gW_wMXKwNxUvooA.jpeg);
background-size: 100%;
/*font-family: arial, helvetica, sans-serif;*/
text-align: center;
}
/* from https://w3schools.org.cn/howto/howto_js_sidenav.asp */
body {
font-family: "Lato", sans-serif;
}
.sidenav {
height: 100%;
width: 0;
position: fixed;
z-index: 1;
top: 0;
left: 0;
background-color: #111;
overflow-x: hidden;
transition: 0.5s;
padding-top: 60px;
}
.sidenav a {
padding: 8px 8px 8px 32px;
text-decoration: none;
font-size: 25px;
color: #818181;
display: block;
transition: 0.3s;
}
.sidenav a:hover, .offcanvas a:focus{
color: #f1f1f1;
}
.sidenav .closebtn {
position: absolute;
top: 0;
right: 25px;
font-size: 36px;
margin-left: 50px;
}
<a data-href="http://twitter.com/media" href="http://twitter.com/media"
rel="nofollow noopener" target="_blank" title="Twitter profile for @media">@media</a>
screen and (max-height: 450px) {
.sidenav {padding-top: 15px;}
.sidenav a {font-size: 18px;}
}
</style>'
HTML和CSS超出了本教程的范围,但网上有很多关于它们的学习资源。
处理POST请求和特殊请求
Web应用程序最重要的功能是处理HTTP请求,而不仅仅是呈现网页。我们的应用程序有几种需要处理的请求类型:
- 登录 — 用户已提交用户名和密码
- 登出 — 用户想终止其登录会话
- 注册 — 潜在用户已提交用户名和密码
- 帐户验证 — 潜在用户点击了通过电子邮件发送给他们的验证链接
- 用户配置文件更新 — 用户想更改密码
- 忘记密码后的恢复 — 用户需要一个临时密码通过电子邮件发送给他们
例如,这是用户登录的处理程序:
verifyLogin: req
| code name user pwd doc tries |
user := req at: #user.
pwd := req at: #pwd.
code := 0.
(self validateEmail: user)
ifFalse: [ code := code + self class ErrBadEmail ].
(self validatePassword: pwd)
ifFalse: [ code := code + self class ErrBadPassword ].
code > 0 ifTrue: [ ^ self loginPage: code user: user pwd: pwd ].
doc := NCTUser selectOne: [ :each | each user = user ].
doc ifNil: [ ^ req abort: (TeaResponse redirect
location: '/register') ].
(PCPasswordCrypt sha256Crypt: pwd withSalt: doc pwdSalt) ~=
doc pwdHash ifTrue: [
tries := req session attributeAt: #tries
ifAbsentPut: [ tries := 0 ].
tries = 3 ifTrue: [ ^ self messagePage: 'Login'
msg: 'Exceeded limit. You''ve been locked out.' ].
tries := tries + 1.
req session attributeAt: #tries put: tries.
^ self messagePage: 'Login' msg: 'Wrong password.' ].
req session attributeAt: #user ifAbsentPut: user.
req session attributeAt: #uuid ifAbsentPut: doc uuid.
doc accessDate: DateAndTime today; save.
name := doc name.
^ TeaResponse redirect location: '/welcome/',
(name substrings = #() ifTrue: [ 'friend' ]
ifFalse: [ name ])
基本伪代码是:
Extract username and password from the HTTP request.
Validate username and password. If this fails, report the error(s)
back to the user.
Query the database for the user document.
Compare the password hash from the database to the hash of the
submitted password. If they don't match, report the error back
to the user. If this happens three times in succession, lock the
user out (presumably, a hacker is trying to breach security).
The passwords match, so keep track of login status by storing #user
and #uuid in the HTTP session.
Update the access date for the user in the database.
Redirect the user to the main page after successful login. If the
user didn't provide a full name, use the name "friend" instead.
Registration
的基本伪代码类似:
Extract username and password from the HTTP request.
Validate username and password. If this fails, report the error(s)
back to the potential user.
Query the database for the user document. If it exists, report to
the potential user that the user already exists.
We're ready to create a new user document, so generate a UUID and
encrypt the password. Create a new database document for the
user, storing the creation date, too.
Send an account verification email to the new user.
这是Registration
处理程序:
verifyRegistration: req
| code name user pwd pwd2 uuid salt |
name := req at: #name.
user := req at: #user.
pwd := req at: #pwd.
pwd2 := req at: #pwd2.
code := 0.
(self validateEmail: user)
ifFalse: [ code := code + self class ErrBadEmail ].
(self validatePassword: pwd)
ifFalse: [ code := code + self class ErrBadPassword ].
pwd = pwd2
ifFalse: [ code := code + self class ErrNoPasswordMatch ].
code > 0 ifTrue: [ ^ self registerPage: code
name: name user: user pwd: pwd pwd2: pwd2 ].
(NCTUser selectOne: [ :each | each user = user ]) ifNotNil: [
^ req abort: (self messagePage: 'Register'
msg: 'User already exists.') ].
uuid := UUID new hex asUppercase.
salt := self generateSalt.
(NCTUser new)
name: name;
user: user;
pwdHash: (PCPasswordCrypt sha256Crypt: pwd withSalt: salt);
pwdSalt: salt;
uuid: uuid;
creationDate: DateAndTime today;
save.
self sendEmail: user subject: 'NCTDB Account Verification'
content: 'Please click on the following link to verify your email: http://nct.gov/verify/',uuid.
^ self messagePage: 'Register'
msg: 'Check your email for account verification.'
实用程序
我们使用正则表达式来验证密码和电子邮件地址。例如:
validatePassword: aPassword
(aPassword size >= 8) & "at least 8 characters"
(aPassword matchesRegex: '^.*[A-Z].*$') &
(aPassword matchesRegex: '^.*[a-z].*$') &
(aPassword matchesRegex: '^.*\d.*$')
ifFalse: [ ^ false ].
^ true
这里,我们想确保密码至少有8个字符,至少包含一个大写字母,至少包含一个小写字母,并且至少包含一个数字字符。
我们还有一个生成“salt”的方法:
generateSalt
^ (String new: 16) collect: [ :each |
'0123456789abcdefghijklmnopqrstuvwxyz' atRandom ]
这会分配一个大小为16
的新String
对象,对于string
中的每个字符,我们都会插入一个从数字和字母组成的string
中随机选择的字符。
查看NCTDB的实际操作
编写完Web应用程序后,您可以运行它来查看它的外观。从Playground,执行此指令:
NCTDB new
然后从您的本地Web浏览器中,输入此URL:
https://:1701/login
您可以通过端口转发‘localhost:1701
’来使您的Web应用程序供朋友和家人使用(如果您的计算机位于路由器后面)。只需登录您的路由器,找到端口转发页面,然后添加一个HTTP条目(包括您的内部IP地址,例如192.168.0.5
,以及私有端口号1701
)。
然后,当您的朋友访问您路由器的IP地址‘http://aaa.bbb.ccc.ddd/login’时,他们将看到NCTDB登录页面!
在生产环境中,您将需要一台专用的服务器机器来运行您的Web应用程序,并在Linux上使用Apache或Nginx,或在Windows上使用IIS。这超出了本教程的范围。
摘要
如您所见,Teapot是一种漂亮、简单的编写Web应用程序的方式。从Pharo方面来说,我们的教程应用程序确实没什么复杂的(除了您仍然需要学习HTML、CSS、一点JavaScript以及如何使用MongoDB!)。
我们的Teapot应用程序是一个相当常见的登录管理前端,几乎可以用于任何Web应用程序。您可以随意修改、扩展它以满足您自己的需求。
您可能想借此机会为它编写一个“admin”模块,一个简单的CRUD(创建、读取、更新、删除)功能,用于浏览、添加、修改和删除数据库中的用户。此功能可以通过Internet安全地完成(确保管理员使用非常强的密码)。
说到安全,您的Teapot应用程序可以通过获取SSL证书并通过执行类似的操作来通过HTTPS提供服务(这是一个Raspberry Pi Linux示例):
initialize
| secureServer teapot |
Teapot stopAll.
secureServer := (ZnSecureServer on: 1443)
certificate: '/home/pi/server.pem';
logToTranscript;
start;
yourself.
teapot := Teapot configure: { #znServer -> secureServer }.
teapot
GET: '/register' -> [ self registerPage: 0 name: ''
user: '' pwd: '' pwd2: '' ];
POST: '/register' -> [ :req | self verifyRegistration: req ];
GET: '/verify/<uuid>' -> [ :req | self verifyUUID: req ];
" ... "
start
此代码创建了一个基于HTTPS和SSL的安全服务器。然后,它将Teapot配置为使用安全服务器而不是常规HTTP服务器。可选的#logToTranscript
消息允许您在Transcript中跟踪所有服务器活动以进行诊断。(确保您在注册处理程序中将验证链接从‘http:’更改为‘https:’。)
希望您觉得本教程有用。
源代码
zip文件包含用于‘FileIn
’的Pharo代码,以及System Browser。首先解压。然后将文件拖放到Pharo窗口中。FileIn整个文件。