在先后端分离的项目中,咱们如今多半会使用token认证机制实现登陆权限验证。前端
token一般会给一个过时时间,这样即便token泄露了,危害期也只是在有效时间内,超过这个有效时间,token过时了,就须要从新生成一个新的token。node
如何生成token呢?数据库
一、建立用户数据库,本文会使用flask-SQLAlchemy(ORM)去管理数据库:json
首先建立一个用户模型:包括了用户昵称,帐号(邮箱或者电话号码等),密码及拥有的权限flask
1 class User(Base): 2 id = Column(Integer, primary_key=True) 3 nickname = Column(String(30), nullable=False) 4 account = Column(String(30), nullable=False) 5 _password = Column("password", String(100), nullable=False) 6 auth = Column(SmallInteger, default=1) 7 8 @property 9 def password(self): 10 return self._password 11 12 @password.setter 13 def password(self, row): 14 self._password = generate_password_hash(row) 15 16 @staticmethod 17 def register_by_email(nickname, account, password): 18 with db.auto_commit(): 19 user = User() 20 user.nickname = nickname 21 user.account = account 22 user.password = password 23 db.session.add(user) 24 25 @staticmethod 26 def checkUser(email, password): 27 # 验证用户名是否存在 28 user = User.query.filter_by(account=email).first_or_404() 29 res = user.checkPassword(password) 30 if not res: 31 raise AuthFailed() 32 scope = "adminScope" if user.auth=="2" else "scope" 33 return {"uid":user.id, "scope":scope} 34 35 def checkPassword(self, raw): 36 if not self._password: 37 return False 38 # check_password_hash将raw加密后和_password比较 39 p = generate_password_hash(raw) 40 print(p==self._password) 41 return check_password_hash(self._password, raw) 42 43 def delete(self): 44 self.status = "0"
因为安全缘由,数据库的密码是必定不能明文保存的,因此此处将用户名进行了加密后端
本文使用的werkzeug.security下面的generate_password_hash()对密码进行的加密,咱们定义了password.setter方法,当在设置密码时,会调用generate_password_hash(password)加密密码,并将其赋值给_passwordapi
当验证密码时,会调用werkzeug.security下面的check_password_hash(hashpwd, raw) 对用户传递过来的密码和加密后的密码进行比对,若是正确返回Truepromise
二、注册安全
当前端传递过来用户名,密码时进行注册时,咱们须要对用户名和密码进行以下基本验证session
1)非空性及长度等基本校验
2)用户名是否已经存在
邮箱注册form:
class EmailRegisterForm(RegisterForm): nickname = StringField(validators=[DataRequired(), length(3,30)]) account = StringField(validators=[DataRequired(message="account can not be blank"), length( min=3, max=32, message="account length wrong"), Email(message="format wrong")]) password = StringField(validators=[DataRequired()]) def validate_account(self, value): user = User.query.filter_by(account=value.data).first() if user: raise ParamsError(msg = "用户已存在")
当验证成功后,会调用咱们在User模型下面定义的register_by_email() 方法进行注册。
@api.router("/register", methods=["POST"]) def register(): data = request.json form = RegisterForm(data=data).validate_for_api() promise = { ClientType.REGISTER_EMAIL:_register_by_email, ClientType.REGISTER_MOBILE:_register_by_mobile() } promise[form.type.data]() return Success() def _register_by_email(): form = EmailRegisterForm(data=request.json).validate_for_api() nickname = form.nickname.data account = form.account.data password = form.password.data User.register_by_email(nickname, account,password)
如今咱们使用postman发送一条注册请求
若是用户已经存在,会返回400
三、登陆,生成token
生成token的方式有不少种,如产生一个固定长度的随机字符串,和用户名密码及过时时间一块儿存储在数据库中,这样token就是一个普通的字符串,能够方便的和其余字符串验证比较并能够检查是否过时
比较复杂一点的作法就是,不要将token存储在数据库,而是使用数字签名做为token,这样作的好处是通过用户数字签名的token是能够防止篡改的。
flask使用与数字签名相似的方法去实现加密的token,咱们能够直接使用itsdangerous库去实现。
生成token,须要用到itsdangerous下面的TimedJSONWebSignatureSerializer
首先咱们实例化一个Serializer,并将咱们的秘钥SECRET_KEY和过时时间做为参数,返回一个TimedJSONWebSignatureSerializer类型对象
而后调用TimedJSONWebSignatureSerializer对象的dumps方法,将咱们想要写入到token中的信息以字典形式传递进去便可。
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer def generate_auth_token(uid, type, scope, expiration=7200): serializer = Serializer(current_app.config["SECRET_KEY"], expires_in=expiration) token = serializer.dumps({"uid":uid, "type":type.value, "scope":scope}) return token
当前端传递用户名,密码到服务端时,服务端校验用户存在而且密码正确时候,就会调用generate_auth_token函数,生成token
值得注意的一点是,这里生成的token是二进制的,因此咱们在返回给前端时,须要将二进制解码token.decode("ascii")
后面用户在访问须要登陆才能访问的的接口时,就不须要再登陆,只须要将token传递过来便可。
1)验证用户名是否存在,此方法做为静态方法放在User模型下
@staticmethod def checkUser(email, password): user = User.query.filter_by(account=email).first_or_404() res = user.checkPassword(password) if not res: raise AuthFailed() scope = "adminScope" if user.auth=="2" else "scope" return {"uid":user.id, "scope":scope}
2)校验密码是否匹配
def checkPassword(self, raw): if not self._password: return False # check_password_hash将raw加密后和_password比较 return check_password_hash(self._password, raw)
3)校验经过后,调用generate_auth_token方法生成token
@api.router("/", methods=["POST"]) def get_token(): data = request.json form = EmailLoginForm(data=data).validate_for_api() type = form.type.data promise = { ClientType.REGISTER_EMAIL:User.checkUser } identify = promise[ClientType(type)](form.account.data, form.password.data) expiration = current_app.config["EXPIRATION"] token = generate_auth_token(identify["uid"], type,identify["scope"], expiration) r = { "token":token.decode("ascii") } return jsonify(r)
四、token认证
如用户想要获取用户信息,这个是要登陆后才能访问的接口,咱们可使用一个装饰器 @auth.login_required 保护,即表示只有正常登陆的用户才能够访问
这个装饰器用到了flask_httpauth库下面的HTTPBasicAuth
auth = HTTPBasicAuth
HTTP Basic Authentication 协议没有具体要求必须使用用户名密码进行验证,HTTP头可使用两个字段去传输认证信息,对于token,咱们只须要将token做为用户名传递过去便可,密码字段能够不填
@auth.verify_password将做为@auth.login_required的中校验密码的回调函数被调用。
咱们前面生成token的时候,用到了咱们自定义了SECRET_KEY加密,一样解密也须要使用咱们的秘钥SECRET_KEY,加密调用的是serializer.dumps(),解密对应的须要使用serializer.loads()
调用serializer.loads(token)时,若是捕捉到下面两个错误:
BadSignature:签名错误,签名可能被篡改
SignatureExpired:签名已过时
表示验证token失败,直接抛出自定义异常,若是没有捕捉到错误,表示,验证经过。能够从中取得前面加密的用户信息,并将信息保存在g变量中,留作他用。
这里的g变量和request同样,都是代理模式的实现,并且是线程隔离的,因此也不用担忧多个请求线程致使数据错乱。
@auth.verify_password def check_authorization(token, pwd): user_info = check_auth_token(token) if not user_info: return False else: g.user = user_info return True def check_auth_token(token): serialzer = Serializer(current_app.config["SECRET_KEY"]) try: s = serialzer.loads(token) except BadSignature: raise AuthFailed(msg="token is invalid", error_code=1004) except SignatureExpired: raise AuthFailed(msg="token is expired", error_code=1004) uid = s["uid"] type = s["type"] scope = s["scope"] return user(uid, type, scope)
【补充】
某些要求比较严谨的验证,还能够将设备mac地址等信息加入都token中
获取设备ip mac等信息的方法:
import socket
host_name = socket.gethostname()
ip = socket.gethostbyname(host_name)
import uuid
mac = uuid.UUID(int=uuid.getnode()).hex[-12:].upper()