項目目錄搭建示意圖
web是js前端代碼項目 api是后台接口項目
后端項目的搭建可以用任何web框架 flask或者django都可以 和平常用flask和django開發不同的是 每個url對應的view處理函數不要返回模板內容
即不再調用render等其它渲染函數 全部使用 return json_response({'data': users, 'total': total})返回數據到前端即可
前端調用后端接口的方式如下:
this.$http.get('/api/account/roles/').then(res => {
this.tableData = res.result
}, res => this.$layer_message(res.result)).finally(() => this.tableLoading = false)
通過這種方式實現前后端數據的交互和展示
前端實現權限管理控制
1.在登錄系統的時候把用戶的權限集合獲取到並且存儲到localstorage中
2.每個權限用一個唯一的字符串來標識即可
3.把用戶權限判斷函數掛載到全局Vue的原型上面

// 權限判斷 Vue.prototype.has_permission = function (str_code) { if (localStorage.getItem('is_supper') === 'true') { return true } let permissions = localStorage.getItem('permissions'); if (!str_code || !permissions) return false; permissions = permissions.split(','); for (let or_item of str_code.split('|')) { if (isSubArray(permissions, or_item.split('&'))) { return true } } return false }; 在main.js中導入 import GlobalTools from './plugins/globalTools' Vue.use(GlobalTools, router);

<el-table-column label="操作" width="240px" v-if="has_permission('assets_host_edit|assets_host_del|assets_host_valid')"> <template slot-scope="scope"> <el-button v-if="has_permission('assets_host_edit')" size="small" @click="handleEdit(scope.row)">編輯</el-button> <el-button v-if="has_permission('assets_host_valid')" size="small" type="primary" @click="valid(scope.row)" :loading="btnValidLoading[scope.row.id]">驗證 </el-button> <el-button v-if="has_permission('assets_host_del')" size="small" type="danger" @click="deleteCommit(scope.row)" :loading="btnDelLoading[scope.row.id]">刪除 </el-button> </template> </el-table-column>

router是全局的router對象 // 路由導航鈎子 router.beforeEach((to, from, next) => { if (['/', '/login', '/deny','/account/person','/account/personset','/home','/welcome'].includes(to.path)) { next() } else if (to.meta.hasOwnProperty('permission') && Vue.prototype.has_permission(to.meta.permission)) { next() } else { next({path: '/deny'}) } })
Vue.use(GlobalTools, router); //把router對象傳遞到GlobalTools模塊中
后端實現權限管理

class User(db.Model, ModelMixin): __tablename__ = 'account_users' @property def permissions(self): if self.is_supper: return set() return Role.get_permissions(self.role_id) class Role(db.Model, ModelMixin): __tablename__ = 'account_roles' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), unique=True, nullable=False) desc = db.Column(db.String(255)) @staticmethod def get_permissions(role_id): sql = text('select p.name from account_role_permission_rel r, account_permissions p where r.role_id=%d and r.permission_id=p.id' % role_id) result = db.engine.execute(sql) return {x[0] for x in result}
用戶登錄成功時候返回數據處理

if user.is_active: if user.verify_password(form.password): login_limit.pop(form.username, None) token = uuid.uuid4().hex user.access_token = token user.token_expired = time.time() + 8 * 60 * 60 user.save() return json_response({ 'token': token, 'is_supper': user.is_supper, 'nickname': user.nickname, 'permissions': list(user.permissions) })
后端接口調用鑒權設計和實現
1.通過中間件把登錄用戶信息存儲到全局對象中

# coding=utf-8 from flask import request, make_response, g from libs.tools import json_response from apps.account.models import User from public import app import time import flask_excel as excel def init_app(app): excel.init_excel(app) app.before_request(cross_domain_access_before) app.before_request(auth_middleware) app.after_request(cross_domain_access_after) app.register_error_handler(Exception, exception_handler) app.register_error_handler(404, page_not_found) def auth_middleware(): if request.path == '/account/users/login/' or request.path.startswith('/apis/configs/') \ or request.path.startswith('/apis/files/'): return None token = request.headers.get('X-TOKEN') if token and len(token) == 32: g.user = User.query.filter_by(access_token=token).first() if g.user and g.user.is_active and g.user.token_expired >= time.time(): g.user.token_expired = time.time() + 8 * 60 * 60 g.user.save() return None return json_response(message='Auth fail, please login'), 401
2.創建一個裝飾器模塊用來裝飾所有需要鑒權的view處理函數

from flask import g from public import app from functools import wraps from libs.tools import json_response # Flask中的g對象是個很好的東西,主要用於在一個請求的過程中共享數據。 # 可以隨意給g對象添加屬性來保存數據,非常的方便 # # def require_permission(str_code): def decorate(func): @wraps(func) def wrapper(*args, **kwargs): if not g.user.is_supper: or_list = [x.strip() for x in str_code.split('|')] for or_item in or_list: and_set = {x.strip() for x in or_item.split('&')} if and_set.issubset(g.user.permissions): break else: return json_response(message='Permission denied'), 403 return func(*args, **kwargs) return wrapper return decorate
3.在view函數中調用裝飾器

@blueprint.route('/', methods=['GET']) @require_permission('assets_host_view | publish_app_publish_host_select | ' 'job_task_add | job_task_edit | assets_host_exec') def get(): form, error = JsonParser(Argument('page', type=int, default=1, required=False), Argument('pagesize', type=int, default=10, required=False), Argument('host_query', type=dict, required=False), ).parse(request.args) if error is None: host_data = Host.query if form.page == -1: return json_response({'data': [x.to_json() for x in host_data.all()], 'total': -1}) if form.host_query.get('name_field'): host_data = host_data.filter(Host.name.like('%{}%'.format(form.host_query['name_field']))) if form.host_query.get('zone_field'): host_data = host_data.filter_by(zone=form.host_query['zone_field']) result = host_data.limit(form.pagesize).offset((form.page - 1) * form.pagesize).all() return json_response({'data': [x.to_json() for x in result], 'total': host_data.count()}) return json_response(message=error)
Token的實現和具體用途
token是一個由服務端生成並返回給客戶端的隨機編碼字符串 並且客戶端每次向服務端發起請求的時候在請求頭中必須包含token 具體用途如下:
1.用來判斷用戶是否登錄並且查看登錄是否過期 類似於session
2.用來對客戶端請求服務端的第一道檢查 如果沒有token 所有的接口統一返回未登錄
3.token驗證不能對登錄后的用戶實現具體的權限驗證
token的存儲設計 后台存儲到用戶信息表中

class User(db.Model, ModelMixin): __tablename__ = 'account_users' id = db.Column(db.Integer, primary_key=True) role_id = db.Column(db.Integer, db.ForeignKey('account_roles.id')) username = db.Column(db.String(50), unique=True, nullable=False) nickname = db.Column(db.String(50)) password_hash = db.Column(db.String(100), nullable=False) email = db.Column(db.String(120)) mobile = db.Column(db.String(30)) is_supper = db.Column(db.Boolean, default=False) is_active = db.Column(db.Boolean, default=True) access_token = db.Column(db.String(32)) token_expired = db.Column(db.Integer) role = db.relationship('Role')
token的后端生成 在用戶登錄成功的時候

@blueprint.route('/login/', methods=['POST']) def login(): form, error = JsonParser('username', 'password').parse() if error is None: user = User.query.filter_by(username=form.username).first() if user: if user.is_active: if user.verify_password(form.password): login_limit.pop(form.username, None) token = uuid.uuid4().hex user.access_token = token user.token_expired = time.time() + 8 * 60 * 60 user.save() return json_response({ 'token': token, 'is_supper': user.is_supper, 'nickname': user.nickname, 'permissions': list(user.permissions) }) else: login_limit[form.username] += 1 if login_limit[form.username] >= 3: user.update(is_active=False) return json_response(message='用戶名或密碼錯誤,連續3次錯誤將會被禁用') else: return json_response(message='用戶已被禁用,請聯系管理員') elif login_limit[form.username] >= 3: return json_response(message='用戶已被禁用,請聯系管理員') else: login_limit[form.username] += 1 return json_response(message='用戶名或密碼錯誤,連續3次錯誤將會被禁用') else: return json_response(message='請輸入用戶名和密碼')
token截驗證每個發送請求的客戶端是否合法 通過中間件實現

def auth_middleware(): if request.path == '/account/users/login/' or request.path.startswith('/apis/configs/') \ or request.path.startswith('/apis/files/'): return None token = request.headers.get('X-TOKEN') if token and len(token) == 32: g.user = User.query.filter_by(access_token=token).first() if g.user and g.user.is_active and g.user.token_expired >= time.time(): g.user.token_expired = time.time() + 8 * 60 * 60 g.user.save() return None return json_response(message='Auth fail, please login'), 401
token的前端存儲 存儲到cookie或者localstorage中

handleSubmit() { this.error = ''; this.$refs['form'].validate(pass => { if (!pass) { return false } this.loading = true; this.$http.post('/api/account/users/login/', this.form).then(res => { localStorage.setItem('token', res.result['token']); localStorage.setItem('is_supper', res.result['is_supper']); localStorage.setItem('permissions', res.result['permissions']); localStorage.setItem('nickname', res.result['nickname']); this.$router.push('/welcome'); }, response => { this.error = response.result }).finally(() => this.loading = false) }) } }
token被集成到請求頭中進行發送

// 請求攔截器 axios.interceptors.request.use(request => { if (request.url.startsWith('/api/')) { request.headers['X-TOKEN'] = localStorage.getItem('token'); // request.url = config.apiServer + request.url.replace('/api', '') // request.url = config.apiServer + request.url } request.timeout = envs.request_timeout; return request; }); // 返回攔截器 axios.interceptors.response.use(response => { return handleResponse(response, router) }, error => { if (error.response) { return handleResponse(error.response, router) } return Promise.reject({result: '請求異常: ' + error.message}) });
token無效示例
用戶無權限示例
正常用戶訪問示例