F

Flask Rest API -第三部分-身份验证和授权

2025-06-07

Flask Rest API -第三部分-身份验证和授权

你好!在本系列的上一篇中我们学习了如何使用Flask REST API,Blueprint以及Flask-Restful如何以更易于维护的方式构建它。

目前,任何人都可以读取、添加、删除和更新我们应用中的电影。现在,让我们学习如何限制任何不受信任的人员(Authentication)创建电影。此外,我们还将学习如何实现Authorization,以便只有在应用中添加电影的人员才能删除/修改它。

要实现这些功能,首先我们必须创建一个新的文档模型来存储用户信息。那就让我们开始吧。

与创建Movie文档模型类似,我们将创建一个User文档模型。在文档模型后添加以下代码Movie



#~/movie-bag/database/models.py

...

class User(db.Document):
 email = db.EmailField(required=True, unique=True)
 password = db.StringField(required=True, min_length=6)


Enter fullscreen mode Exit fullscreen mode

在这里,我们创建了这个,以便当用户注册时,会创建一个包含字段email和的新用户文档password

但是将密码明文保存StringField是个糟糕的主意。如果有人访问了你的数据库,所有用户的密码都会暴露。为了防止这种情况发生,我们会将hash密码保存为某种cryptic格式,这样就没有人能轻易地找到真正的密码了。

为了对密码进行哈希处理,我们将使用一个流行的哈希函数bcrypt。你可能已经猜到了,我们将使用一个名为flask-bcrypt的 Flask 扩展来实现这一点。

让我们安装吧flask-bcrypt



pipenv install flask-bcrypt


Enter fullscreen mode Exit fullscreen mode

让我们在我们的中初始化 flask-bcrypt app.py



#~/movie-bag/app.py

 from flask import Flask
+from flask_bcrypt import Bcrypt
 from database.db import initialize_db
 from flask_restful import Api
 from resources.routes import initialize_routes

 app = Flask(__name__)
 api = Api(app)
+bcrypt = Bcrypt(app)

 app.config['MONGODB_SETTINGS'] = {
 'host': 'mongodb://localhost/movie-bag'




Enter fullscreen mode Exit fullscreen mode

现在我们将创建两种方法:一种用于创建密码哈希generate_password_hash(),另一种用于检查用户登录使用的密码是否生成与数据库中保存的密码相同的哈希check_password_hash()

让我们更新一下,models.py使其看起来像这样:



#~/movie-bag/database/models.py

 from .db import db
+from flask_bcrypt import generate_password_hash, check_password_hash

...

class User(db.Document):
 email = db.EmailField(required=True, unique=True)
 password = db.StringField(required=True, min_length=6)
+ 
+ def hash_password(self):
+   self.password = generate_password_hash(self.password).decode('utf8')
+ 
+ def check_password(self, password):
+   return check_password_hash(self.password, password)


Enter fullscreen mode Exit fullscreen mode

现在让我们为创建一个 API 端点signup使用以下代码添加到文件夹auth.py内。resources



#~/movie-bag/resources/auth.py

from flask import request
from database.models import User
from flask_restful import Resource

class SignupApi(Resource):
 def post(self):
   body = request.get_json()
   user = User(**body)
   user.hash_password()
   user.save()
   id = user.id
   return {'id': str(id)}, 200


Enter fullscreen mode Exit fullscreen mode

该端点使用用户发送的对象创建用户文档emailpassword接收该对象。JSON

让我们在我们的中注册这个端点routes.py



 from .movie import MoviesApi, MovieApi
+from .auth import SignupApi

 def initialize_routes(api):
   api.add_resource(MoviesApi, '/api/movies')
   api.add_resource(MovieApi, '/api/movies/<id>')
+
+  api.add_resource(SignupApi, '/api/auth/signup')


Enter fullscreen mode Exit fullscreen mode

让我们测试一下用户注册。将正文JSON发送emailpasswordhttp://localhost:5000/api/auth/signup

Postman 注册请求

如果我们查看数据库,我们可以看到,与我们在 API 请求中发送的密码相比,我们的密码被散列为一些随机密码。

Mongo Compass 数据库条目

注意:为了查看存储在数据库中的信息,我使用了mongo compass

好的,我们已经创建了通过创建用户的功能signup,现在我们需要能够login以该用户的身份。

为了帮助用户登录网站,我们需要提供验证用户身份的功能。这样一来,用户每次需要在网站上执行操作时都可以发送emailpassword,这从安全角度来看并非良策。因此,我们需要提供这样的功能:用户登录网站后,可以使用令牌访问网站的其他部分。

有很多方法可以使用基于令牌的身份验证,在本部分中,我们将学习JWT也称为JSON Web 令牌

要使用 JWT,让我们安装另一个名为flask-jwt-extended的 flask 扩展,它使用我们想要保存为令牌的值(在我们的例子中是userid)并将其与salt(密钥)结合起来创建一个令牌。



pipenv instll flask-jwt-extended


Enter fullscreen mode Exit fullscreen mode

由于secret-key我们用来创建 JWT 的内容需要保存在代码库的其他地方,因此我们将使用.env文件来保存机密,并.env使用环境变量将文件的位置提供给我们的应用程序。

为此,我们在文件夹.env中创建一个新文件movie-bag并向其中添加以下内容。



JWT_SECRET_KEY = 't1NP63m4wnBg6nyHYKfmc2TpCOGI4nss'


Enter fullscreen mode Exit fullscreen mode

的值JWT_SECRET_KEY可以是任意值,但不能使其更难猜测。

让我们更新我们的app.py以使用.env文件中的配置并初始化JWT



#~/movie-bag/app.py

from flask import Flask
 from flask_bcrypt import Bcrypt
+from flask_jwt_extended import JWTManager
+
 from database.db import initialize_db
 from flask_restful import Api
 from resources.routes import initialize_routes

 app = Flask(__name__)
+app.config.from_envvar('ENV_FILE_LOCATION')
+
 api = Api(app)
 bcrypt = Bcrypt(app)
+jwt = JWTManager(app)

 app.config['MONGODB_SETTINGS'] = {
 'host': 'mongodb://localhost/movie-bag'


Enter fullscreen mode Exit fullscreen mode

这是环境变量,它应该存储相对于文件ENV_FILE_LOCATION的位置.envapp.py

要设置此值 mac/linux 可以运行以下命令:



export ENV_FILE_LOCATION=./.env


Enter fullscreen mode Exit fullscreen mode

Windows 用户可以运行以下命令:



set ENV_FILE_LOCATION=./.env


Enter fullscreen mode Exit fullscreen mode

现在,我们终于可以实现login端点了。让我们更新文件夹auth.py内部resources




-from flask import request
+from flask import Response, request
+from flask_jwt_extended import create_access_token
 from database.models import User
 from flask_restful import Resource 
+import datetime
+
 class SignupApi(Resource):
 def post(self):
 body = request.get_json()
@@ -9,4 +11,16 @@ class SignupApi(Resource):
 user.hash_password()
 user.save()
 id = user.id
 return {'id': str(id)}, 200
+
+class LoginApi(Resource):
+ def post(self):
+   body = request.get_json()
+   user = User.objects.get(email=body.get('email'))
+   authorized = user.check_password(body.get('password'))
+   if not authorized:
+     return {'error': 'Email or password invalid'}, 401
+ 
+   expires = datetime.timedelta(days=7)
+   access_token = create_access_token(identity=str(user.id), expires_delta=expires)
+   return {'token': access_token}, 200



Enter fullscreen mode Exit fullscreen mode

这里我们通过给定的邮箱地址搜索用户,并检查发送的密码是否与数据库中保存的哈希密码相同。
如果密码和邮箱地址正确,我们将创建访问令牌,并将create_access_token()其用作user.id标识符。该令牌的有效期7 days.为 7 天,这意味着用户在 7 天后将无法使用此令牌访问网站。

让我们在我们的routes.py



 from .movie import MoviesApi, MovieApi
-from .auth import SignupApi
+from .auth import SignupApi, LoginApi

 def initialize_routes(api):
 api.add_resource(MoviesApi, '/api/movies')
 api.add_resource(MovieApi, '/api/movies/<id>')

 api.add_resource(SignupApi, '/api/auth/signup')
+ api.add_resource(LoginApi, '/api/auth/login')



Enter fullscreen mode Exit fullscreen mode

现在,我们需要限制未经授权的用户在我们的应用程序中添加、编辑和删除电影。为此,我们需要@jwt_required在端点中添加装饰器。这可以保护我们的端点免受无效或过期 JWT 的影响。

更新movie.py为:



#~/movie-bag/resources/movie.py

 from flask import Response, request
+from flask_jwt_extended import jwt_required
 from database.models import Movie
 from flask_restful import Resource

class MoviesApi(Resource):
 ...
+ 
+ @jwt_required
 def post(self):
   body = request.get_json()
   movie = Movie(**body).save()
 ...


 class MovieApi(Resource):
+ @jwt_required
 def put(self, id):
   body = request.get_json()
   Movie.objects.get(id=id).update(**body)
   return '', 200
+ 
+ @jwt_required
 def delete(self, id):
   movie = Movie.objects.get(id=id).delete()
   return '', 200


Enter fullscreen mode Exit fullscreen mode

现在让我们测试一下。
首先,我们必须以之前创建的用户身份登录signup

Postman 登录请求

我们从服务器获取了令牌,现在尝试从 API 端点 创建一个影片http://localhost:5000/api/movies。正如你所见,由于 API 端点受 保护,因此无法执行此操作并会报错jwt

注意:我们将在本系列的后续部分学习如何使错误消息更友好。

login现在让我们在标题中使用我们之前获得的令牌Authorization

要在 Postman 中使用授权标头,请按照以下步骤操作:
1)转到Authorization选项卡。2
)选择Bearer Token表单TYPE下拉菜单。3
)粘贴您之前从 4)获得的令牌/login
,最后发送请求。

带有授权标头的 Postman 请求


让我们添加一项功能,以便只有创建电影的用户才能删除或编辑电影。

让我们更新models.py并创建用户和电影之间的关系。



#~/movie-bag/database/models.py

class Movie(db.Document):
 name = db.StringField(required=True, unique=True)
 casts = db.ListField(db.StringField(), required=True)
 genres = db.ListField(db.StringField(), required=True)
+ added_by = db.ReferenceField('User')

 class User(db.Document):
   email = db.EmailField(required=True, unique=True)
   password = db.StringField(required=True, min_length=6) 
+  movies = db.ListField(db.ReferenceField('Movie', reverse_delete_rule=db.PULL))



+
+User.register_delete_rule(Movie, 'added_by', db.CASCADE)


Enter fullscreen mode Exit fullscreen mode

user我们在和之间创建了一对多关系movie。这意味着一个用户可以拥有一部或多部电影,并且一部电影只能由一个用户创建。此处reverse_delete_rule的电影字段User表示如果删除了该电影,则应从用户文档中提取该电影。
同样,User.register_delete_rule(Movie, 'added_by', db.CASCADE)创建了另一条删除规则,这意味着如果删除了用户,则该用户创建的电影也会被删除。

注意:我必须added_by单独注册 的删除规则,因为User在定义 时尚未定义Movie

现在,让我们更新movie.py以应用授权。




 from flask import Response, request
-from flask_jwt_extended import jwt_required
-from database.models import Movie
+from database.models import Movie, User
+from flask_jwt_extended import jwt_required, get_jwt_identity
 from flask_restful import Resource

 class MoviesApi(Resource):
   def get(self):
     movies = Movie.objects().to_json()
     return Response(movies, mimetype="application/json", status=200)

   @jwt_required
   def post(self):
+    user_id = get_jwt_identity()
     body = request.get_json()
-    movie = Movie(**body).save()
+    user = User.objects.get(id=user_id)
+    movie = Movie(**body, added_by=user)
+    movie.save()
+    user.update(push__movies=movie)
+    user.save()
     id = movie.id
     return {'id': str(id)}, 200

 class MovieApi(Resource):
   @jwt_required
   def put(self, id):
+    user_id = get_jwt_identity()
+    movie = Movie.objects.get(id=id, added_by=user_id)
     body = request.get_json()
     Movie.objects.get(id=id).update(**body)
     return '', 200

   @jwt_required
   def delete(self, id):
-    movie = Movie.objects.get(id=id).delete()
+    user_id = get_jwt_identity()
+    movie = Movie.objects.get(id=id, added_by=user_id)
+    movie.delete()
     return '', 200

 def get(self, id):
...


Enter fullscreen mode Exit fullscreen mode

这里的get_jwt_identity()方法返回的值create_access_token()在我们的例子中是user.id。因此,我们只删除/更新由向应用程序发送请求的用户添加的电影。


您可以在此处找到此部分的完整代码

我们从本系列的这一部分中学到了什么?

  • 如何使用哈希用户密码flask-bcrypt
  • 如何使用创建 JSON 令牌flask-jwt-extended
  • 如何保护 API 端点免受未经授权的访问。
  • 如何实现授权,以便只有添加电影的用户才能删除/更新电影。

由于我们的应用程序中存在许多不友好的错误和异常,因此在下一部分中我们将学习如何处理 REST API 中的错误和异常。



如果您遇到任何困难,请告诉我,以便我为您提供指导。另外,如果您希望我在下一部分/系列中介绍某些内容,请不要忘记在下方提及。

祝您编码愉快😊

文章来源:https://dev.to/paurakhsharma/flask-rest-api-part-3-authentication-and-authorization-5935
PREV
Flask Rest API -第 6 部分- 测试 REST API
NEXT
为你的作品集创建炫酷的打字动画