Django + Google Authenticator

因担心网站账号被破,限制访问 IP 又带来诸多不便,想到集成 Google Authenticator 校验。关于 Google Authenticator 的原理介绍可以参考:http://www.csdn.net/article/2014-09-23/2821808-Google-Authenticator

新建一个认证 app,做到集成 Google Authenticator 需要做以下工作:

1. 新建 app,我们这里赞命名为 auth2

2. 添加自定义用户 Model

class User(AbstractBaseUser, PermissionsMixin):  # 注意继承的父类
    username = models.CharField(u'User Name', max_length=30, unique=True)  # 必须
    mobile = models.CharField(u'Mobile', max_length=11, default='')
    email = models.EmailField(u'Email address', default='')  # 必须
    ga_key = models.CharField(u'GA Key', max_length=16, default='')  # Google Authenticator 用到
    is_staff = models.BooleanField(u'staff status', default=False)
    is_active = models.BooleanField(u'active', default=True)

    date_joined = models.DateTimeField(u'date joined', default=timezone.now)  # Add if use manager of Django

    USERNAME_FIELD = 'username'  # 指定用户名的字段名,在认证时通过此设置反查
    REQUIRED_FIELDS = ['email']  # 因为下面使用 Django 自带的 UserManager,创建 super user 时需要

    objects = UserManager()  # Use manager of Django

    def get_full_name(self):
        '''如果会使用 Django 自带 Admin,则必须'''
        return self.username

    def get_short_name(self):
        '''如果会使用 Django 自带 Admin,则必须'''
        return self.username

    def authenticate_ga(self, otp):
        '''使用 Google Authenticator 校验'''
        if self.ga_key:
            try:
                t = pyotp.TOTP(self.ga_key)
                return t.verify(int(otp))
            except:
                return False

3. 定制 Form

class GAAuthenticationForm(AuthenticationForm):  # 注意继承父类
    """
    A custom authentication form used in the auth2 app.
    """
    ga = forms.CharField(label='GA', widget=forms.PasswordInput)  # 口令输入框

    error_messages = {
        'invalid_login': _("Please enter the correct %(username)s and password "
                           "for a staff account. Note that both fields may be "
                           "case-sensitive."),
    }
    required_css_class = 'required'

    def confirm_login_allowed(self, user):  # 重写父类方法,此方法在校验用户名密码之后调用
        ga = self.cleaned_data.get('ga')

        if not user.is_active or not user.is_staff or not user.authenticate_ga(ga):  # 加上 Google Authenticator 校验
            raise forms.ValidationError(
                self.error_messages['invalid_login'],
                code='invalid_login',
                params={'username': self.username_field.verbose_name}
            )>/code>

4. 登录 view,核心部分

if request.method == "POST":
    form = authentication_form(request, data=request.POST)
    if form.is_valid():  # 此处最终会调用用户密码认证和GA认证
        auth_login(request, form.get_user())

        return HttpResponseRedirect(redirect_to)

5. 页面表单部分

<form method="post" id="login-form">{% csrf_token %}
        <div>
            {{ form.username.errors }}
            {{ form.username.label_tag }} {{ form.username }}
        </div>
        <div>
            {{ form.password.errors }}
            {{ form.password.label_tag }} {{ form.password }}
        </div>
        <div>
            {{ form.ga.errors }}
            {{ form.ga.label_tag }} {{ form.ga}}
            <input type="hidden" name="next" value="{{ next }}"/>
        </div>
        <div>
            <label>&nbsp;</label><input type="submit" value="Log in"/>
        </div>
    </form>

当然,表单不一定要用 form 来生成。HTML 中包含 username、password、ga 输入框即可。

6. settings.py 加上

AUTH_USER_MODEL = 'auth2.User'  # 指定认证用的 Model
LOGIN_URL = '/auth2/login'  # 指定登录 URL

以上就是核心代码,一些注意点:

1. 认证的 backend 仍 使用 Django 的 ModelBackend,利用这部分只做用户名和密码认证,而 Google Authenticator 口令认证放在 GAAuthenticationForm 中实现

2. Model 中的 ga_key 必须与终端安装的 Google Authenticator 中的 密钥一致。Python 可通过

pyotp.random_base32()

生成,需安装 pyopt。补充一句,Google Authenticator 的 email 名称,可以理解为终端中该动态口令的名称,不影响认证,具体看 Google Authenticator 的原理

3. 如果不是新项目,而是改造老的认证方式。如果想把老的用户(auth_user 中的用户)导入,需自行实现。如果要删除 auth_user、auth_user_groups、auth_user_user_permissions 这几张表,需先删除 django_admin_log 中对应 auth_user 的外键约束

可能有些遗漏,有的话以后补充。可以看到要写的代码不多,尽量复用 Django 自带功能。

——

扼住命运的咽喉