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
- authenticate 这里面包含了几种场景
- 传递user信息,那么如果用户存在的话,可以直接授权,这个往往用于第三方授权场景。第三方本身授权通过之后,然后储存的信息获取到MyUser信息,然后执行授权,不执行授权login会失败
- 混合输入登陆,支持用户名,电话,邮箱登陆,这个时候要尝试多种匹配
单一登陆,根据setting设置的登陆方式进行校验授权
get_user 获取用户信息
user_can_authenticate 这个用于验证注册的场景,比如邮箱或者电话号码注册账号都会立即创建账户,但不会立即激活,只有激活is_active = True之后,才能授权
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
- 定义微信用户数据模型
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
- 添加backend (settings.py)
AUTHENTICATION_BACKENDS = (
'authwrapper.backends.auth.WechatBackend',
)
- 实现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
- authenticate 调用时传递的参数为request和user,这个user是WeixinMpAPI User
根据openid获取wechat用户model, 如果该wechat用户已存在,进一步检查该用户是否已关联UserModel,如果未关联,则关联数据;如果wechat用户不存在,则创建新用户
get_user 该处返回的还是UserModel对象
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'
参考文档
- https://docs.djangoproject.com/en/1.5/ref/settings/#auth-profile-module
- https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model
- Django中扩展User模型
- 非profile方式扩展Django User Model
- django user 权限
- django 注册、登录及第三方接口程序(2):扩展User表
- 扩展django的User的部分方法
- django admin框架使用系列之三:扩展user model
- Django扩展user表并使用email登陆
- Django 扩展 User model 的字段有什么好的方法?
- django使用email进行身份验证
- 自定义的用户认证如何登录使用Django自带的后台管理?