当一个请求离开authenticate()中间件,认证通过后,请求上下文可能存在两种状态:
-
请求上下文中包含User结构体(表示有效、已认证用户)。
-
或者请求上下文包含一个AnonymousUser结构体。
接下来的内容,我们将携带这些用户信息到下一阶段,看看如何执行不同授权检查来限制对API接口的访问。具体来说,你将学习如何:
-
添加检查,只允许激活用户才能访问不同的/v1/movies**接口。
-
实现一种基于许可的授权模式,提供细粒度控制用户对接口的访问。
要求用户激活
不久前我们提到,就授权而言,我们要做的第一件事是限制对/v1/movies**接口的访问,因此,它们只能被通过身份验证(而不是匿名)的用户访问,并且这些用户已经激活了自己的帐户。
中间件可以完美地执行这类检查,因此我们可以创建一个requireActivatedUser()中间件方法来完成检查。在这个中间件中,我们从请求上下文中提取出User结构体,然后通过调用IsAnonymous()方法,检查Activated字段来确定请求是否继续执行。
-
如果是匿名用户的话,我们应该返回401 Unauthorized返回码和“you must be authenticated to access this resource”错误消息给客户端。
-
如果不是匿名用户(例如经过认证的已知用户),但是没有激活的话就返回403 Forbidden返回码和“your user account must be activated to access this resource”错误消息给客户端。
记住: 如果没有认证的话,应该使用401 Unauthorized响应,当用户通过身份验证但不允许执行所请求的操作时,应该使用403 Forbidden响应。
因此,我们先在cmd/api/error.go文件中添加一些帮助函数来发送这些错误信息。如下所示:
package main
...
func (app *application)authenticationRequiredResponse(w http.ResponseWriter, r *http.Request) {
message := "you must be authenticated to access this resource"
app.errorResponse(w, r, http.StatusUnauthorized, message)
}
func (app *application)inactiveAccountResponse(w http.ResponseWriter, r *http.Request) {
message := "your user account must be activated to access this resource"
app.errorResponse(w, r, http.StatusForbidden, message)
}
然后创建requireActivatedUser()中间件来执行用户权限检查。需要写的代码很简洁:
File: cmd/api/middleware.go
package main
...
func (app *application)requireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//使用前面创建的contextGetUser()帮助函数来从请求上下文提取用户信息
user := app.contextGetUser(r)
//如果是匿名用户,调用authenticationRequiredResponse()提醒客户端用户需要认证
if user.IsAnonymous() {
app.authenticationRequiredResponse(w, r)
return
}
//如果用户未激活,使用inactiveAccountResponse()帮助函数提醒客户端用户账号需要激活
if !user.Activated {
app.inactiveAccountResponse(w, r)
return
}
//调用接口处理程序继续处理请求
next.ServeHTTP(w, r)
})
}
注意到requireActivatedUser()中间件签名和本书中其他中间件稍微不一样。接收的参数和返回值不是http.Handler,而是http.HandlerFunc。这只是一个小的改变,但使用这个中间件可以对/v1/movie**处理程序进行封装,不需要做其他的转换。
下面更新cmd/api/routes.go文件,如下所示:
package main
...
func (app *application) routes() http.Handler {
router := httprouter.New()
router.NotFound = http.HandlerFunc(app.notFoundResponse)
router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)
router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.requireActivatedUser(app.healthcheckHandler))
router.HandlerFunc(http.MethodGet, "/v1/movies", app.requireActivatedUser(app.listMoviesHandler))
router.HandlerFunc(http.MethodPost, "/v1/movies", app.requireActivatedUser(app.createMovieHandler))
router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.requireActivatedUser(app.showMovieHandler))
router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.requireActivatedUser(app.updateMovieHandler))
router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.requireActivatedUser(app.deleteMovieHandler))
router.HandlerFunc(http.MethodPost, "/v1/users", app.registerUserHandler)
router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUserHandler)
router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler)
return app.recoverPanic(app.rateLimit(app.authenticate(router)))
}
演示
先以匿名用户测试GET /v1/movies/:id接口。当执行这个请求的时候,你将收到401 Unauthorized响应,如下所示:
$ curl -i localhost:4000/v1/movies/1
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Vary: Authorization
Date: Fri, 07 Jan 2022 12:41:34 GMT
Content-Length: 66
{
"error": "you must be authenticated to access this resource"
}
$ BODY='{"email": "[email protected]", "password": "pa55word"}'
$ curl -d "$BODY" localhost:4000/v1/tokens/authentication
{
"authentication_token": {
"token": "FFB2PFHNVFCRPYRGEQSWEIAODQ",
"expiry": "2022-01-08T20:44:48.715949+08:00"
}
}
$ curl -i -H "Authorization: Bearer FFB2PFHNVFCRPYRGEQSWEIAODQ" localhost:4000/v1/movies/1
HTTP/1.1 403 Forbidden
Content-Type: application/json
Vary: Authorization
Date: Fri, 07 Jan 2022 12:45:40 GMT
Content-Length: 76
{
"error": "your user account must be activated to access this resource"
}
很好,我们发现使用未激活的用户,收到的inactiveAccountResopnse帮助函数返回的403 Forbidden响应。
最后,我们试试使用激活的用户发送请求。如果你跟随本系列文章操作,可以快速连接到PostgreSQL数据库,然后检查下哪个用户已经激活了。
psql $GREENLIGHT_DB_DSN
psql (13.4)
Type "help" for help.
greenlight=> select email from users where activated = true;
email
-------------------
[email protected]
(1 row)
在我们的例子中,只有[email protected]用户是激活的,因此我们使用该用户来发起请求。当使用激活的用户发起请求时,所有requireActivateUser()中间件的检查都能正常通过,请求可以正常处理,如下所示:
$ BODY='{"email": "[email protected]", "password": "pa55word"}'
$ curl -d "$BODY" localhost:4000/v1/tokens/authentication
{
"authentication_token": {
"token": "KWDXFAGWRG4DM7DN45LXYG6PLQ",
"expiry": "2022-01-08T20:56:27.47269+08:00"
}
}
$ curl -i -H "Authorization: Bearer KWDXFAGWRG4DM7DN45LXYG6PLQ" localhost:4000/v1/movies/1
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Authorization
Date: Fri, 07 Jan 2022 12:57:00 GMT
Content-Length: 156
{
"movie": {
"id": 1,
"title": "Moana",
"year": 2016,
"runtime": "107 mins",
"genres": [
"animation",
"adventure"
],
"Version": 1
}
}
拆分中间件
此时我们用一个中间件来处理两种检查:1、检查用户是否认证(不是匿名用户);2、检查用户是否激活。
但想象有一种情况你只想检查用户是否认证,而不关心有没有激活。要支持这种检查,你除了添加之前的requireActivatedUser(),可能需要引入额外的requireAuthenticatedUser()中间件。
但是,这两个中间件存在重叠功能,因为都包含对用户是否认证的检查。避免这种重叠的简单方法就是在requireActivatedUser()中间件中自动调用requireAuthenticatedUser()中间件。
用文字不好表达其如何工作的,直接代码演示。更新cmd/api/middleware.go文件:
File:cmd/api/middleware.go
package main
...
//创建requireAuthenticatedUser()中间件,检查用户不是匿名用户
func (app *application)requireAuthenticatedUser(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := app.contextGetUser(r)
if user.IsAnonymous(){
app.authenticationRequiredResponse(w, r)
return
}
next.ServeHTTP(w, r)
})
}
//同时检查用户是否认证和激活
func (app *application)requireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
//不是直接返回http.HandlerFunc,而是将其赋值给变量fn
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := app.contextGetUser(r)
//检查用户是否激活
if !user.Activated{
app.inactiveAccountResponse(w, r)
return
}
next.ServeHTTP(w, r)
})
//使用requireAuthenticatedUser()对fn进行封装在返回
return app.requireAuthenticatedUser(fn)
}
上面的中间件设置方式,requireActivatedUser()中间件会自动调用requireAuthenticatedUser()中间件,先检查用户是否认证,然后才执行自己的逻辑检查用户是否激活。在我们的例子中这样处理很有意义,因为我们只有确认用户已经认证了,才会去检查用户是否激活。
你可以重启API服务,程序应该可以正常编译,功能和前面测的一样。
额外信息
处理程序检查用户权限
如果你有很多接口需要执行授权检查,可以在对应的处理程序(handler)中进行检查,而不是使用中间件。如下所示:
func (app *application)exampleHandler(w http.ResponseWriter, r *http.Request) {
user := app.contextGetUser(r)
if user.IsAnonymous(){
app.authenticationRequiredResponse(w, r)
return
}
if !user.Activated {
app.inactiveAccountResponse(w, r)
return
}
//余下代码是处理程序业务逻辑
}
文章评论