def api_verify_code(request): """检查验证码是否正确 POST /api/account/verify-code { // 验证码用途,可以是:register, login, reset-password "usage": "register", // 与 send-code 类似,有两种方式指定发送对象 // * 提供 phone 或 email 字段 // * 提供 identity 字段 "phone": "13912345678", "email": "*****@*****.**", "identity": "13912345678", // 用户输入的验证码 "code": "123456", } 如果成功,返回 200,否则返回 400。 """ usage = request.json.get('usage') if usage not in otp_tools.USAGES: return api.bad_request(message='Invalid "usage"') code = request.json.get('code') identity = request.json.get('identity') phone = request.json.get('phone') email = request.json.get('email') if phone: if not is_valid_phone(phone): return errors.InvalidPhone() elif email: if not is_valid_email(email): return errors.InvalidEmail() elif identity: if is_valid_phone(identity): phone = identity elif is_valid_email(identity): email = identity else: return errors.InvalidIdentity() else: return api.bad_request(message='Missing "phone", "email" or "identity"') recipient = phone or email engine = 'sms' if phone else 'email' valid = otp_tools.verify_code(engine, request, recipient, usage, code, False) if valid: return api.ok(message='验证码正确') else: return errors.InvalidVerifyCode()
def authenticate(self, request, identity=None, email=None, code=None, **kwargs): if not email and is_valid_email(identity): email = identity if not email or not code: return None try: user = UserModel.objects.get(email_validated=True, email=email) except UserModel.DoesNotExist: return None except FieldError: return None usage = kwargs.get('usage', USAGES.login) valid = verify_code('email', request, email, usage, code, True) if valid and self.user_can_authenticate(user): return user
def api_send_code(request): """发送验证码,此函数仅支持发送注册、短信登录、重置密码三种验证码 POST /api/account/send-code { // 验证码用途,可以是:register, login, reset-password "usage": "register", // 有两种方式指定发送对象, // * 提供 phone 字段,则会发送短信, 提供 email 字段,则会发送邮件 // * 提供 identity 字段,则会自动判断 identity 是手机号还是邮箱,从而执行相应的操作 // phone、email、identity 三个参数只能提供一个 "phone": "13912345678", "email": "*****@*****.**", "identity": "13912345678", } * 如果缺少参数(一般只在开发阶段),会返回 400,message 描述错误信息。 * 如果传递的 phone、email 或 identity 非法,会返回 InvalidPhone, InvalidEmail 或 InvalidIdentity * 如果在静默期内重复请求,会返回 RateExceeded * 其他所有情况都会返回 200(比如被后端认定为恶意请求而没有发送,比如登录请求中提供的 username 不存在而未发送等等),这些错误无需告知用户。 登录、重置密码验证码,后端会查找用户,如果用户不存在,则不会发送验证码。 如果用户存在,但是该用户的 phone_validated 为 False(即手机号未验证), 也不会发送验证码。邮箱也是如此。即发送验证码的前置条件是相应的短信或邮箱已经验证过。 对于注册验证码,我们不检查账号是否已经存在,也不会因为账号已存在而不发送短信。原因: * 如果我们告诉用户账号已存在,那么有心人可以遍历手机号来判断我们有哪些注册账号 * 如果我们不告诉用户账号已存在、也不发送验证码,那么用户会很奇怪(用户收不到验证码, 会觉得很奇怪)。让用户正常收验证码,在注册时会提示账号已存在。 """ usage = request.json.get('usage') if usage not in otp_tools.USAGES: return api.bad_request(message='Invalid "usage"') identity = request.json.get('identity') phone = request.json.get('phone') email = request.json.get('email') if phone: if not is_valid_phone(phone): return errors.InvalidPhone() elif email: if not is_valid_email(email): return errors.InvalidEmail() elif identity: if is_valid_phone(identity): phone = identity elif is_valid_email(identity): email = identity else: return errors.InvalidIdentity() else: return api.bad_request(message='Missing "phone", "email" or "identity"') # 对于登录、重置密码的验证码,我们需要校验用户存在 if usage in ['login', 'reset-password']: if phone: user = User.objects.filter(phone=phone, phone_validated=True).first() else: user = User.objects.filter(email=email, email_validated=True).first() if not user or not user.is_active: # 用户不存在或用户已锁定,不发送验证码,但仍然返回“验证码已发送” return api.ok(message='验证码已发送') recipient = phone or email engine = 'sms' if phone else 'email' vcode, result = otp_tools.generate_code(engine, request, recipient, usage=usage) if result == otp_tools.GENERATE_RESULTS.silent: return errors.RateExceeded() return api.ok(message='验证码已发送')
def validate_recipient(engine, recipient): if engine == 'sms' and is_valid_phone(recipient): return True if engine == 'email' and is_valid_email(recipient): return True return False
def api_register(request): """ 注册 API POST /api/account/register { // 与登录 API 类似,可以明确指定 phone+code 或 email+code 或 identity+code "identity": "13912345678", "code": "123456", "phone": "13912345678", "code": "123456", "email": "*****@*****.**", "code": "123456", "password": "******", } """ code = request.json.get('code') if not code: return errors.InvalidVerifyCode() password = request.json.get('password') if not password: return api.bad_request('Missing "password"') identity = request.json.get('identity') phone = request.json.get('phone') email = request.json.get('email') if phone: if not is_valid_phone(phone): return errors.InvalidPhone() elif email: if not is_valid_email(email): return errors.InvalidEmail() elif identity: if is_valid_phone(identity): phone = identity elif is_valid_email(identity): email = identity else: return errors.InvalidIdentity() else: return api.bad_request( message='Missing "phone", "email" or "identity"') if phone: find_user_args = dict(phone=phone, phone_validated=True) create_user_args = dict(phone=phone, phone_validated=True, password=password) vcode_engine = 'sms' recipient = phone else: create_user_args = dict(email=email, email_validated=True, password=password) vcode_engine = 'email' recipient = email valid = otp_tools.verify_code(vcode_engine, request, recipient, 'register', code, True) if not valid: return errors.InvalidVerifyCode() # 由于数据库中不宜为 phone、email 添加 unique contraint # (主要是 Django 里的实现,如果要添加 unique contraint,那么 phone、email # 比如允许为 null,但 django 中 email 并未声明允许为 null,一些地方的代码 # 也有这样的假定,因此我们尽量不打破这种惯例) # 我们手动判断邮箱或手机号是否存在,如果存在则报错。这会存在 race condition, # 有可能两个并发的请求导致创建了邮箱或手机号相同的账号,不过这种概率应该很低。 # 除非两个请求同时运行上述 verify_code 并且都返回了 True (极小概率时间), # 而且这里又同时读取数据库、创建新用户,才会导致问题。因此暂时不考虑这个问题。 if phone: if User.objects.filter(phone=phone, phone_validated=True).exists(): return errors.PhoneExists() user = User.objects.create_user(phone=phone, phone_validated=True, password=password) else: if User.objects.filter(email=email, email_validated=True).exists(): return errors.EmailExists() user = User.objects.create_user(email=email, email_validated=True, password=password) return api.ok(data=serialize_user(user))