Contents:

Django权限系统


1612 浏览 5 years, 11 months

3 扩展用户授权

版权声明: 转载请注明出处 http://www.codingsoho.com/

扩展用户授权

用户授权的实现包括以下几个方面

  • 在settings.py添加backend入口
  • 实现backend完成authenticate工作

指定authentication backend

Django维护了一个“authentication backends”列表,通过它在授权。Django会遍历所有的backends直到授权接收。 authentication backends在settings里的AUTHENTICATION_BACKENDS指定,可以是任意路径,默认的backends是 ['django.contrib.auth.backends.ModelBackend']   轮询算法见authenticate

AUTHENTICATION_BACKENDS的顺序是有影响的,如果多个backends的用户名和密码都能验证通过,django在第一次匹配后就会停止。所以要特别小心参数的匹配。
如果backend抛出PermissionDenied异常,授权检查会立即停止,不会再检查后面的backend。

注意:如果授权成功,django会把这个授权方式保存到session里,session周期里的下一次接入还是用这种方式。如果要强迫用不同方法授权,一个简单的方法是调用Session.objects.all().delete().

Settings.py

AUTHENTICATION_BACKENDS = (        
    'authwrapper.backends.auth.MyBackend', 
    'django.contrib.auth.backends.ModelBackend', 
    )
AUTH_USER_MODEL = 'authwrapper.MyUser'

实现authentication backend

必须实现的方法

get_user(user_id)
authenticate(request, **credentials)

遗留问题

不知道为什么?username登录时的request.user是有值的,但是wechat登录永远是anonymous,这个从一开始的render就开始了 _cached_user为AnonymousUser: AnonymousUser
问题查清楚了,下面这个wechat auth backend函数没写好,之前返回None,这个函数在eclipse上打断点也进不去不知道为什么
好像也不是这个问题,突然就好了

authwrapper\backends\auth.py

class MyBackend(object):
    """Allows user to sign-in using email, username or phone_number."""
    def authenticate(self, username=None, password=None, **kwargs):  
        try:
            """login with user info directly"""
            if kwargs['user'] :
                if isinstance(kwargs['user'], UserModel):
                    return kwargs.get('user',None)
                else: 
                    return None
        except:
            pass
#
        user = None
        if username is None and kwargs.get(UserModel.USERNAME_FIELD,None) is None:
            return None
#            
        try:
            """if allow mix login options
            username/phone/mail """
            if True == settings.ACCOUNT_ALLOW_MIX_TYPE_LOGIN:
                if '@' in username:
                    user = UserModel._default_manager.get(email=username)
                elif '+' in username[0]: # to be precise
                    user = UserModel._default_manager.get(phone=username)
                else:
                    user = UserModel._default_manager.get(username=username)
            else:    
                user = UserModel._default_manager.get_by_natural_key(username)
#            
            if user.check_password(password):
                return user
        except UserModel.DoesNotExist:
            UserModel().set_password(password)
            return None
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user
            else:
                return None
#                
    def user_can_authenticate(self, user):
        """Reject users with is_active=False. Custom user models that don't have that attribute are allowed."""
        is_active = getattr(user, 'is_active', None)
        return is_active or is_active is None
#        
    def get_user(self, user_id):        
        try:
            user = UserModel._default_manager.get(pk=user_id)
        except UserModel.DoesNotExist:
            return None
        return user if self.user_can_authenticate(user) else None        
  1. authenticate 这里面包含了几种场景
  2. 传递user信息,那么如果用户存在的话,可以直接授权,这个往往用于第三方授权场景。第三方本身授权通过之后,然后储存的信息获取到MyUser信息,然后执行授权,不执行授权login会失败
  3. 混合输入登陆,支持用户名,电话,邮箱登陆,这个时候要尝试多种匹配
  4. 单一登陆,根据setting设置的登陆方式进行校验授权

  5. get_user 获取用户信息

  6. user_can_authenticate 这个用于验证注册的场景,比如邮箱或者电话号码注册账号都会立即创建账户,但不会立即激活,只有激活is_active = True之后,才能授权

  7. UserModel._default_manager.get和UserModel._default_manager.get_by_natural_key

user = UserModel.objects.filter(username=username).first()  
user = UserModel.objects.get(username=username) 

上面这两种方式调用都会出错,分析原因可能是因为UserModel有多个manager, 导致行为上有差异,待验证

UserModel._default_manager MyUserManager: personalcenter.MyUser.objects

UserModel._base_manager Manager: personalcenter.MyUser._base_manager

以后关于UserModel的get,尽量用 UserModel._default_manager

针对不同的注册方式,get_by_natural_key可能根据 USERNAME_FIELD 准确的找到默认的field

    def get_by_natural_key(self, username):
        return self.get(**{self.model.USERNAME_FIELD: username})

**{}解决了get后面model名字不确定的问题,也是之前困扰voith的一个问题

执行get时会做validation,即使值一致但是validation不过也会返回失败
比如电话号码+86135000000000,之前validation时没有+86,添加的值也不会带+86.
Validation规则变化之后,即使搜索135000000000(老的添加值在数据库存在)也会返回失败

用户扩展 (支持微信)

这儿会用到一个微信库 python-weixin

  1. 定义微信用户数据模型

authwrapper\models.py

class WechatUserProfile(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        blank = True,
        null = True
    )
    openid = models.CharField(max_length=120, blank=True, null=True) #wechat only
    unionid = models.CharField(max_length=120, blank=True, null=True) #wechat only
    privilege = models.CharField(max_length=120, blank=True, null=True) #wechat only    
    headimgurl = models.CharField(max_length=500, blank=True, null=True)
    nickname = models.CharField(max_length=120, blank=True, null=True)
    sex = models.CharField(max_length=45, blank=True, null=True)    
    city = models.CharField(max_length=45, blank=True, null=True)
    country = models.CharField(max_length=45, blank=True, null=True)    
    language = models.CharField(max_length=45, blank=True, null=True)
#
    def __unicode__(self):
        if self.nickname:
            return self.nickname
        elif self.user:
            return self.user.username
        else:
            return self.openid
#
    def get_absolute_url(self):
        return reverse("personalcenter", kwargs={"id": self.id })  
#
    def get_image_url(self):
        return self.headimgurl   

这些域field是从微信的结构拷贝来的,添加了OneToOneField user

  1. 添加backend (settings.py)
AUTHENTICATION_BACKENDS = (    
    'authwrapper.backends.auth.WechatBackend',
    )
  1. 实现backend 同样要实现基本的authenticate和get_user函数

authwrapper\backends\auth.py

class WechatBackend(object):
#
    def authenticate(self, request, user):
        obj = None
        cur_user = auth.get_user(request)
        profile, created = WechatUserProfile.objects.get_or_create(openid = user['openid'])
#
        if created is False:
            obj = profile.user            
            if profile.user is None: # wechat profile not linked to UserModel yet
                 if cur_user.is_active and not cur_user.is_anonymous() and cur_user is not None:
                    profile.user =  cur_user # link wechat profile to UserModel
                    profile.save()
        else:
            profile.unionid = user['unionid']
            #profile.privilege = user['privilege'] #privilege is list
            profile.city = user['city']
            profile.country = user['country']
            profile.language = user['language']
            if 1 == user['sex']:
                profile.sex = 'male'
            else:
                profile.sex = 'female'
            profile.nickname = user['nickname']
            profile.headimgurl = user['headimgurl']
            if cur_user.is_active and not cur_user.is_anonymous() and cur_user is not None:
                profile.user = cur_user
            profile.save()
            obj = request.user
#
        request.session['wechat_id'] = profile.id
        # request._cached_user = obj
#
        return obj

    def get_user(self, user_id):
        try:
            return UserModel.objects.get(pk=user_id)
        except UserModel.DoesNotExist:
            return None
  1. authenticate 调用时传递的参数为request和user,这个user是WeixinMpAPI User

根据openid获取wechat用户model, 如果该wechat用户已存在,进一步检查该用户是否已关联UserModel,如果未关联,则关联数据;如果wechat用户不存在,则创建新用户

  1. get_user 该处返回的还是UserModel对象

  2. get_wechat_user django在维护user的信息,但是wechat user的信息,需要我们自己维护   目前的实现方式是通过添加session "wechat_id"在完成的,授权完成之后

request.session['wechat_id'] = profile.id

定义 get_wechat_user 用于获取wechat用户

    def get_wechat_user(self, request):
        wechat_id = request.session.get("wechat_id", None)
        if wechat_id:
            try:
                wechat = WechatUserProfile.objects.get(pk=wechat_id)
                return wechat
            except:
                pass
        return None

在middle里将wechat信息添加进request,这样在处理navbar登陆选项时能够知道wechat信息

zakkabag\middleware.py

class openidmiddleware():

    def process_request(self, request):
        request.register_type = settings.ACCOUNT_REGISTER_TYPE

        if request.user.is_anonymous:
               from django.utils.module_loading import import_string
               backend = import_string('authwrapper.backends.auth.WechatBackend')()
               request.wechat = backend.get_wechat_user(request)

学习get_user里面load module的方法 - load_backend,里面用了一个新的方法去import model (import_string),可以以后再函数里对module数组操作时使用,这儿仅是一个练习

调用get_wechat_user的必须是一个函数,所以要对它实例化之后才能调用

logout的时候要删除这个session

del request.session['wechat_id']

登陆 login

from django.contrib.auth import login as auth_login

登陆最终都会调用基本的login函数

退出   

from django.contrib.auth import logout as auth_logout

def logout(request):
    try:
        del request.session['wechat_id']
    except:
        pass
    auth_logout(request)
    return redirect(reverse("home", kwargs={}))

退出时调用基本的logout函数,并删除wechat_id session

完整的过程

authwrapper\login.py

from django.contrib.auth import login as auth_login
from django.contrib.auth import views as auth_views
def login(request):
    REDIRECT_URI = request.POST.get('next', request.GET.get('next', reverse("home", kwargs={}))) #next indicated in templaetes
    if request.method == 'GET':
        code = request.GET.get('code')
        if code:
            redirect_to = "[http://%s%s](http://%s%s)" % (request.META['HTTP_HOST'], reverse("home", kwargs={})) # redirection URL after authenticate
            api = WeixinMpAPI(appid=APP_ID, 
                        app_secret=APP_SECRET,
                        redirect_uri=redirect_to)
            auth_info = api.exchange_code_for_access_token(code=code)
            api = WeixinMpAPI(access_token=auth_info['access_token'])
            api_user = api.user(openid=auth_info['openid'])                
            user = authenticate(request = request, user = api_user)
            if user and not user.is_anonymous():
                auth_login(request, user)
                return redirect(redirect_to)
#
        return redirect(reverse("auth_login", kwargs={})) # if user is not login, redirect to login template
    else:  #normal login is POST
        REDIRECT_FIELD_NAME = 'next'
        return auth_views.login(request, redirect_field_name=REDIRECT_FIELD_NAME, extra_context=None)    
#
    return auth_views.login(request, redirect_field_name=REDIRECT_URI, extra_context=None)    

1.WeixinMpAPI对微信进行授权
登陆的过程会用到python-weixin的API,这个API主要是针对Mobile Phone使用的,其实的还没有去尝试使用过 其中会用到几个参数APP_ID,APP_SECRET,这些都可以在公众号里面获取
redirect_uri表示授权成功之后跳转的地址   Wechat的login是通过get方式进行的

2.系统授权
对于微信登陆方式,授权传递的参数即为wechat profile,在authenticate函数里会把它和UserModel进行绑定处

  • 如果当前用户已登陆,则直接绑定该用户到微信账户,将来可能会增加确认窗口 (WechatBackend.authenticate)
  • 否则重定向到新的网页进行登陆或注册处理,登陆或注册成功后会继续绑定 提取上面的部分代码,如果用户存在,并且非匿名用户,说明wexin登陆之前,用户已正常登陆,并且在authenticate里面已完成绑定。否则,重定向到登陆界面
if user and not user.is_anonymous():
    auth_login(request, user)
    return redirect(redirect_to)
return redirect(reverse("auth_login", kwargs={}))

登陆的模板如下,用户登陆成功时进行绑定

templates\registration\login.html

{% block content %}
<div class = "row">
    <div class = "col-sm-3 col-sm-offset-3">
        {% if request.user.is_anonymous and request.wechat %}
            You need to login to continue
        {% endif %}        
        <form method="post" action=""> {% csrf_token %}            
            {{ form|crispy }}            
            <input class = "btn btn-block btn-primary" type="submit" value="{% trans 'Log in' %}" />            
            {% if request.user.is_anonymous and request.wechat %}
                <input type="hidden" name="next" value="{% url 'link_to_wechat' %}" /> 
            <!-- workaround here, maybe we need to change it in login function later-->
            {% else %}
                <input type="hidden" name="next" value="{{ next }}" /> 
            {% endif %}
        </form>        
    </div>
</div>
{% endblock %}

next跳转链接会根据用户状态进行设置,如果发现系统用户未登陆,但微信用户登陆了,则在提交成功后重定向到link_to_wechat进行关联

注册情况下的处理,最终激活成功时进行绑定

templates.registration.activation_complete.html

{% block content %}
<p class = "lead">
    {% trans "Your account is now activated." %}
    {% if not user.is_authenticated %}
        {% trans "You can log in." %}
    {% else %}
        <a href='{% url "link_to_wechat" %}'>Link to wechat</a> 
    {% endif %}
</p>
{% endblock %}

personalcenter\login.py

@login_required
def account_link_to_wechat(request):
    user = auth.get_user(request)
    wechat = WechatBackend().get_wechat_user(request)
    if wechat:
        wechat.user = user   
        wechat.save()    
        return redirect(reverse("home", kwargs={}))

    return redirect(reverse("home", kwargs={}))

对于正常登陆方式,可以针对不同的backend进行授权 比如: - ModelBackend

        username = request.POST['username']
        password = request.POST['password']
        user = authenticate(request=request, username=username, password=password)

3.登陆

最终的登陆都是调用django.contrib.auth.login - django.contrib.auth.login() 纯登陆操作,之前需完成相关的授权工作

有些情况可以把调用封装起来,比如login form的处理 - django.contrib.auth.view.login(),这个是正常的视图的登陆的处理,模板为'registration/login.html'

参考文档

  1. https://docs.djangoproject.com/en/1.5/ref/settings/#auth-profile-module
  2. https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model
  3. Django中扩展User模型
  4. 非profile方式扩展Django User Model
  5. django user 权限
  6. django 注册、登录及第三方接口程序(2):扩展User表
  7. 扩展django的User的部分方法
  8. django admin框架使用系列之三:扩展user model
  9. Django扩展user表并使用email登陆
  10. Django 扩展 User model 的字段有什么好的方法?
  11. django使用email进行身份验证
  12. 自定义的用户认证如何登录使用Django自带的后台管理?