使用Python+Flask+Bootstrap实现一个简单的用户权限管理(RBAC)(Part4)

  • Post author:
  • Post category:python




代码实现详解

本章中我将会介绍templates和view下的文件和函数。

templates目录下都是html模板文件,内容如下

├── index.html

├── layout.html

└── login

├── change_password.html

├── login.html

├── role_add.html

├── role_edit.html

├── role_management.html

├── user_add.html

└── user_management.html

views目录下只有两个文件,其中__init__.py是空文件,login.py是主要文件

├──

init

.py

├── login.py

因为这些文件都是互相调用和指向,所以我会从功能的实现来解读



用户登录

我们通过判断session中是否存在 username来检查用户是否登录,所以在login.py中有如下的修饰函数

def login_required(view):
    """View decorator that redirects anonymous users to the login page."""

    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if session.get("username") == None:
            return redirect(url_for('login.login'))

        return view(**kwargs)

    return wrapped_view

这个函数如果检测到session中不存在username,则会转向login.py文件中的login函数

@bp.route('/', methods=['GET', 'POST'])
def login():
    if request.method == 'POST' :
        username = request.form['username']
        password = hashlib.sha224(request.form['password'].encode()).hexdigest()

        user_info = USERS.query.filter_by(username=username,password=password).all()

        if len(user_info)==0:
            flash ("Wrong Username or Password")
            return render_template('login/login.html')  
        else:
            userrole = user_info[0].userrole


        session['username']=username
        session['userrole']=userrole

        return redirect(url_for('login.success'))
    return render_template('login/login.html')

因为这个转页不是post方法,所以login函数则会直接弹出templates目录下login.html文件让用户输入用户名和密码。

<form class="form-signin" method="POST" action="{{url_for('login.login') }}">
    {% if get_flashed_messages() %}
    <h1 class="h4 mb-3 font-weight-normal">{{ get_flashed_messages()[0] }}</h1>
    {% endif %}
    <h1 class="h4 mb-3 font-weight-normal">Please sign in</h1>
    <label for="username" class="sr-only">Username</label>
    <input type="text" name="username" id="username" class="form-control" placeholder="Username" required autofocus>
    <label for="password" class="sr-only">Password</label>
    <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
    <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>

这个文件中的将会把username和password传给上面login函数,因为这一次是post的方法,所以login函数会去验证user表中是否有相同的用户名和密码。

  1. 如果用户名密码正确,就把username和userrole加到session中,并转入login.success函数
  2. 如果错误,则会转回登录界面,并提示。

    其中密码我们使用了sha224加密,这也是为什么我们设置密码为Password123!但是插入数据库的时候是一串很长字符的原因。

而login.success函数,则主要做如下事情:

  1. 从menus获取所有的菜单信息,包括menuid,menuname和parentmenu
  2. 从rolemapping表中获得用户角色对应的所有menuid
  3. 将这些信息存入session,然后渲染index.html页面
@bp.route('/success')
@login_required
def success():
    roleid=session['userrole']
    all_menus = MENUS.query.order_by(MENUS.menuorder).all()
    menus=[]
    for menu in all_menus:
        menu_detail=[menu.menuid,menu.menuname,menu.parentmenu]
        menus.append(menu_detail)
    session['menus']=menus
    mappings = ROLEMAPPINGS.query.filter_by(roleid=roleid).all()
    mapping_menus=[]
    for mapping in mappings:
        mapping_menus.append(mapping.menuid)
    session['mapping_menus']=mapping_menus
    return render_template('index.html')

index.html就是登陆成功以后的主页面,包括下拉菜单和主要内容,因为这个教程主要是讲怎么实现基于角色的菜单控制,所以主要内容基本为空。

{% extends 'layout.html' %}
{% block content %}
<div class="col-xl-9">
    <div class="mb-3">
        <h3 class="display-8 card p-3">Welcome {{session['username']}}</h3>
    </div>
</div>
{%endblock%}

而第一行的layout.html,才是我们下拉菜单需要的主要文件。下拉菜单的实现是使用了Bootstrap的navbar。

其实现模式可以参考https://getbootstrap.com/docs/4.4/components/dropdowns/

一个最简单的下拉菜单如下

<div class="dropdown">
  <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
    Dropdown button
  </button>
  <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
    <a class="dropdown-item" href="#">Action</a>
    <a class="dropdown-item" href="#">Another action</a>
    <a class="dropdown-item" href="#">Something else here</a>
  </div>
</div>

我的代码中只是在这个菜单中增加了部分内容,如果想要详细了解,可以参考我的代码和官方链接做对比。

我这里主要是讲述如何实现的动态菜单内容

  1. 用户名下的下拉菜单,针对用户角色是admin的,我们将会显示所有菜单,而其他用户则只显示修改密码以及退出
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
{%if session['userole']==1%}
<a class="dropdown-item" href="{{url_for('login.usermanagement')}}">User Management</a>
<a class="dropdown-item" href="{{url_for('login.rolemanagement')}}">Role Management</a>
{%endif%}
<a class="dropdown-item" href="{{url_for('login.passwordmanagement')}}">Password Management</a>
<a class="dropdown-item" href="{{url_for('login.logout')}}">Logout</a>
</div>
  1. 功能菜单,由于我们已经在session中保存了所有菜单的相关信息,以及对应角色的所有菜单id,所以我们第一步将会查询该用户所有的parentmenu,然后查询该用户对应角色在parentmenu下面的menu。

    我们认为在所有菜单中,如果该菜单没有parentmenu,则其本身就是parentmenu。
{%for parentmenu in session['menus']%}
{%if not parentmenu[2]%}
{%if parentmenu[0] in session['mapping_menus']%}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownParent" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{parentmenu[1]}}
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
{%for submenu in session['menus']%}
{%if submenu[2]|int==parentmenu[0]%}
{%if submenu[0] in session['mapping_menus']%}
<a class="dropdown-item" href="{{submenu[3]}}">{{submenu[1]}}</a>
{%endif%}
{%endif%}
{%endfor%}
</div>
</li>
{%endif%}
{%endif%}
{%endfor%}



用户退出

当用户点击logout的时候,我们将session中的用户置为false,并重新定位到login页面, logout对应的html代码

<a class="dropdown-item" href="{{url_for('login.logout')}}">Logout</a>

对应的python代码

def logout():
    session['username']=False
    return redirect(url_for('login.login'))



修改密码

修改密码的时候主要是需要验证两次输入的密码是一样的,所以在html部分包括了jquery, html form如下,有两个输入框password和confirmpassword

<form class="card p-3 mb-5" action="{{ url_for('login.passwordmanagement')}}" id="passwordmanagement" method="post">
<div class="mb-3 col-xl-6 form-group">
<input type="password" class="form-control" id="password" name="password" placeholder="newpassword" value="" required>
</div>
<div class="mb-3 col-xl-6 form-group">
<input type="password" class="form-control" id="confirmpassword" name="confirmpassword" placeholder="confirmpassword" value="" required>
</div>
<div class="mb-3 form-group" align="right">
<button class="btn btn-primary" id="btnSavePasswd" type="button">Save</button>
</div>
</form>

Jquery部分主要是确认在提交按钮输入后,确认两个密码一致,如果不一致,则提示密码不一致

<script>
$('#btnSavePasswd').on('click',function(){
    var form_id =  $(this).closest('form').attr("id");
    var values={};
    $.each($('#'+form_id).serializeArray(), function(i, field) {
        values[field.name] = field.value;
    });
    var password= values["password"];
    var confirmpassword=values["confirmpassword"];
    if (password != confirmpassword) {
        alert("New Password Not Match");
        return false;
    } else {
        $(this).closest('form').submit();
        return true;

    }

});
</script>



添加角色

我们会首先在html页面展示一个输入角色名字的输入框,然后展示所有可供这个角色使用的菜单。

Html的form

<form class="card p-3 mb-5" action="{{ url_for('login.role_add')}}" method="post" id="roleAdd" name="roleAdd">
<div class="mb-3">
<div>
<label for="rolename" class="form-inline">Role Name: <input type="text" class="form-control" id="rolename" name="rolename" required></label>
</div>
</div>
{% for menu in menus %}
<div class="mb-3">
{%if not menu.parentmenu%}
<input type="checkbox" name="rolemenu" value="{{menu.menuid}}">{{menu.menuname}}<br>
{%set parent_menu_id=menu.menuid%}
{% for menu in menus %}
{%if menu.parentmenu|int==parent_menu_id%}
&emsp;<input type="checkbox" name="rolemenu" value="{{menu.menuid}}">{{menu.menuname}}<br>
{%endif%}
{%endfor%}
{%endif%}
</div>
{%endfor%}
<div align="right">
<button class="btn btn-primary btn-save" type="submit" id="btnSaveRole">Save</button>
</div>
</form>

而form中的menus的来源于

def add_new_role():
    if request.method == 'GET':
        menus = MENUS.query.order_by(MENUS.menuorder).all()
        return render_template('/login/role_add.html',menus=menus)

当用户输入了角色名字选择了menu以后,提交给python脚本处理,脚本会将角色和角色菜单映射添加到对应的表中去。

def role_add():
    if request.method == 'POST':
        rolename = request.form['rolename']
        rolemenuidlist = request.form.getlist('rolemenu')
        rolemenuidlist = [int(i) for i in rolemenuidlist]
        newrole=ROLES(rolename=rolename)
        dbusers.session.add(newrole)
        dbusers.session.commit()
        newroleid=ROLES.query.filter_by(rolename=rolename).first().role_id
        for rolemenuid in rolemenuidlist:
            new_role_mapping = ROLEMAPPINGS(roleid=newroleid, menuid=rolemenuid)
            dbusers.session.add(new_role_mapping)
            dbusers.session.commit()
        return redirect(url_for('login.rolemanagement'))



修改角色

有时候可能觉得角色不需要了,或者觉得给这个角色的菜单需要改动,就调用修改角色的功能,我们需要先列出所有菜单,然后标记该角色对应的所有菜单,从而方便进行修改。

所以渲染html前,我们需要查询所有菜单,角色信息,以及角色和菜单映射

def edit_role():
    if request.method == 'GET':
        menus = MENUS.query.order_by(MENUS.menuorder).all()
        roleid=request.args['roleid']
        roleinfo=ROLES.query.filter_by(roleid=roleid).first()
        mappings=ROLEMAPPINGS.query.filter_by(roleid=roleid).all()
        return render_template('/login/role_edit.html',menus=menus,roleinfo=roleinfo,mappings=mappings)

接下来用这些数据渲染html

<form class="card p-3 mb-5" action="{{ url_for('login.role_edit')}}" method="post" id="roleEdit"
              name="roleAdd">
            <div class="mb-3">
                <div>
                    <label class="form-inline">Role Name: {{roleinfo.rolename}} </label>
                    <input type="hidden" name="roleid" value="{{roleinfo.roleid}}">
                </div>
            </div>
            {% for menu in menus %}
            <div class="mb-3">
                {%if not menu.parentmenu%}
                    {%set mybool = [False]%}
                    {%for mapping in mappings%}
                        {%if menu.menuid==mapping.menuid %}
                        {%set _ = mybool.append(not mybool.pop())%}
                        {%endif%}
                    {%endfor%}

                    {%if mybool[0] %}
                    <input type="checkbox" name="rolemenu" value="{{menu.menuid}}" checked>{{menu.menuname}}<br>
                    {%else%}
                    <input type="checkbox" name="rolemenu" value="{{menu.menuid}}">{{menu.menuname}}<br>
                    {%endif%}
                    {%set parent_menu_id=menu.menuid%}
                    {% for menu in menus %}
                        {%if menu.parentmenu|int==parent_menu_id%}
                            {%set mybool = [False]%}
                            {%for mapping in mappings%}
                                {%if menu.menuid==mapping.menuid %}
                                {%set _ = mybool.append(not mybool.pop())%}
                                {%endif%}
                            {%endfor%}
                            {%if mybool[0] %}
                            &emsp;<input type="checkbox" name="rolemenu" value="{{menu.menuid}}" checked>{{menu.menuname}}<br>
                            {%else%}
                            &emsp;<input type="checkbox" name="rolemenu" value="{{menu.menuid}}">{{menu.menuname}}<br>
                            {%endif%}
                        {%endif%}
                    {%endfor%}
                {%endif%}
            </div>
            {%endfor%}
            <div align="right">
                <button class="btn btn-primary btn-save" type="submit" id="btnSaveRole">Save</button>
            </div>
        </form>

这段数据被提交后,python将会清空角色菜单映射表中和该角色相关的所有数据,然后重新插入新的菜单数据

def role_edit():
    if request.method == 'POST':
        roleid = request.form['roleid']
        rolemenuidlist = request.form.getlist('rolemenu')
        rolemenuidlist = [int(i) for i in rolemenuidlist]
        #Remove the existing menus
        existingmenus = ROLEMAPPINGS.query.filter_by(roleid=roleid).all()
        for existingmenu in existingmenus:
            dbusers.session.delete(existingmenu)
            dbusers.session.commit()
        #Add new menus
        for rolemenuid in rolemenuidlist:
            new_role_mapping = ROLEMAPPINGS(roleid=roleid, menuid=rolemenuid)
            dbusers.session.add(new_role_mapping)
            dbusers.session.commit()
        return redirect(url_for('login.rolemanagement'))



添加用户

添加用户也比较简单,主要是让输入用户名和密码,并选择角色。

<form class="card p-3 mb-5" action="{{ url_for('login.add_new_user')}}" id="add_new_user"
              method="post">
            <div class="row form-group">
                <div class="mb-3 col-xl-3 form-group">
                <input type="text" class="form-control" id="username" name="username" placeholder="username"
                       value=""
                       required>
                </div>
                <div class="mb-3 col-xl-3 form-group">
                <input type="password" class="form-control" id="password" name="password" placeholder="password"
                       value=""
                       required>
                </div>
                <div class="mb-3 col-xl-3 form-group">
                <select id="userrole" name="userrole">
                                    {%for userrole in userroles%}
                                    <option value="{{userrole.roleid}}">{{userrole.rolename}}
                                    </option>
                                    {%endfor%}
                                </select>
                </div>
                <div class="mb-3 form-group" align="right">
                <button class="btn btn-primary" id="btnSaveIssue" type="submit">Save</button>
            </div>
            </div>
        </form>

其中userroles的数据是在login脚本中提供,可参加其他类似的功能。

Python收到添加用户的请求后,会把对应的数据加入数据库,当然,此前会检查这个用户是不是存在,同时也会将密码加密。

def add_new_user():
    if request.method == 'GET':
        userroles = ROLES.query.all()
        return render_template('/login/user_add.html',userroles=userroles)
    elif request.method == 'POST':
        username = request.form['username'].lower()
        existing_user=USERS.query.filter_by(username=username).first()
        if existing_user:
            flash ("User already exist")
            userroles = ROLES.query.all()
            return render_template('/login/user_add.html', userroles=userroles)
        else:
            password = hashlib.sha224(request.form['password'].encode()).hexdigest()
            userrole = request.form['userrole']

            new_user=USERS(username=username,password=password,userrole=userrole)
            dbusers.session.add(new_user)
            dbusers.session.commit()

            return redirect(url_for('login.usermanagement'))



修改用户角色

列出所有的用户和所有的用户角色,并让管理员进行修改, html被渲染过的模板

{% for user in users %}
                <form action="{{ url_for('login.usermanagement')}}" method="post" id="form{{user.userid}}">
                    <table class="table">
                        <tr>
                            <input type="hidden" name="userid" value="{{user.userid}}"/>
                            <td width="10%">{{user.username}}</td>
                            <td width="10%">
                                <select disabled="disabled" id="userrole" name="userrole">
                                    {%for userrole in userroles%}
                                    <option value="{{userrole.roleid}}" {%if user.userrole==userrole.roleid%} selected {%endif%}>{{userrole.rolename}}
                                    </option>
                                    {%endfor%}

                                </select>
                            </td>
                            <td width="20%">
                                <button class="btn btn-primary btn-edit" type="button" id="btnEditUser">Edit</button>
                                <a class="btn btn-primary" id="btnDelUser"
                           href="{{ url_for('login.delete_user',userid=user.userid) }}"
                           style="color: white"
                           onclick="{if(confirm('Are you sure?')){this.document.formname.submit();return true;}return false;}">Delete</a>
                                <button class="btn btn-primary d-none btn-save" type="button" id="btnSaveUser">Save
                                </button>

                            </td>
                            <td>&nbsp;</td>
                        </tr>
                    </table>
                </form>
                {% endfor %}

脚本收到数据后,则直接更新user表

def usermanagement():
    if request.method=="POST":
        userid=request.form['userid']
        userrole=request.form['userrole']
        userinfo=USERS.query.filter_by(userid=userid)
        userinfo[0].userrole=userrole
        dbusers.session.commit()
    users=USERS.query.all()
    userroles=ROLES.query.all()
    return render_template('login/user_management.html',users=users,userroles=userroles)

至此,本功能全部介绍完毕,欢迎批评和指正。



版权声明:本文为ppcorn2原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。