Bläddra i källkod

开始实现登陆流程

James Iter 9 år sedan
förälder
incheckning
7fb4b107cb
8 ändrade filer med 237 tillägg och 49 borttagningar
  1. 70 0
      api/user.py
  2. 4 0
      api_route_table.py
  3. 65 2
      main.py
  4. 20 0
      misc/init.sql
  5. 5 1
      models/__init__.py
  6. 55 0
      models/user.py
  7. 6 38
      templates/login.html
  8. 12 8
      views/misc.py

+ 70 - 0
api/user.py

@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+from flask import Blueprint, request, g, make_response
+from flask import request
+from flask import session
+import json
+import jimit as ji
+
+from api.base import Base
+from models import User
+from models import Utils
+from models import Rules
+
+
+__author__ = 'James Iter'
+__date__ = '2017/9/14'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2017 by James Iter.'
+
+
+blueprint = Blueprint(
+    'api_user',
+    __name__,
+    url_prefix='/api/user'
+)
+
+blueprints = Blueprint(
+    'api_users',
+    __name__,
+    url_prefix='/api/users'
+)
+
+
+user_base = Base(the_class=User, the_blueprint=blueprint, the_blueprints=blueprints)
+
+
+@Utils.dumps2response
+def r_sign_in():
+
+    user = User()
+
+    args_rules = [
+        Rules.LOGIN_NAME.value,
+        Rules.PASSWORD.value
+    ]
+    user.login_name = request.json.get('login_name')
+    user.password = request.json.get('password')
+
+    try:
+        ji.Check.previewing(args_rules, user.__dict__)
+        plain_password = user.password
+        user.get_by('login_name')
+
+        if not ji.Security.ji_pbkdf2_check(password=plain_password, password_hash=user.password):
+            ret = dict()
+            ret['state'] = ji.Common.exchange_state(40101)
+            ret['state']['sub']['zh-cn'] = ''.join([ret['state']['sub']['zh-cn'], u': 鉴权失败'])
+            raise ji.PreviewingError(json.dumps(ret, ensure_ascii=False))
+
+        token = Utils.generate_token(user.id)
+        session['token'] = token
+        rep = make_response()
+        rep.data = json.dumps({'state': ji.Common.exchange_state(20000)}, ensure_ascii=False)
+        return rep
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+

+ 4 - 0
api_route_table.py

@@ -4,6 +4,7 @@
 
 from models.utils import add_rule_api
 from api import config
+from api import user
 from api import guest
 from api import disk
 from api import boot_job
@@ -27,6 +28,9 @@ add_rule_api(config.blueprint, '', api_func='config.r_create', methods=['POST'])
 add_rule_api(config.blueprint, '', api_func='config.r_update', methods=['PATCH'])
 add_rule_api(config.blueprint, '', api_func='config.r_get', methods=['GET'])
 
+# 用户管理
+add_rule_api(user.blueprint, '/_sign_in', api_func='user.r_sign_in', methods=['POST'])
+
 # 系统启动作业配置操作
 add_rule_api(boot_job.blueprint, '', api_func='boot_job.r_create', methods=['POST'])
 add_rule_api(boot_job.blueprint, '/<_id>', api_func='boot_job.r_update', methods=['PATCH'])

+ 65 - 2
main.py

@@ -6,12 +6,14 @@ import traceback
 import signal
 
 import time
+from datetime import timedelta
 import jimit as ji
 import json
 
 import os
 import threading
-from flask import g, request, redirect, url_for, Response
+from flask import g, request, redirect, url_for, Response, session
+from flask.ext.session import Session
 
 from models import Utils
 from models.event_processor import EventProcessor
@@ -20,6 +22,7 @@ import api_route_table
 import views_route_table
 from models import Database as db
 from models import Config
+from models import User
 from api.boot_job import blueprint as boot_job_blueprint
 from api.boot_job import blueprints as boot_job_blueprints
 from api.operate_rule import blueprint as operate_rule_blueprint
@@ -68,6 +71,11 @@ __contact__ = 'james.iter.cn@gmail.com'
 __copyright__ = '(c) 2017 by James Iter.'
 
 
+# 替换为Flask-Session
+app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(seconds=app.config['PERMANENT_SESSION_LIFETIME'])
+Session(app)
+
+
 def instantiation_ws_vnc(listen_port, target_host, target_port):
     # 用于 Web noVNC 代理
     ws = WebSocketProxy(listen_host="0.0.0.0", listen_port=listen_port, target_host=target_host,
@@ -94,7 +102,8 @@ def is_not_need_to_auth(endpoint):
     not_auth_table = [
         'api_config.r_get',
         'api_config.r_create',
-        'v_config.create'
+        'v_config.create',
+        'api_user.r_sign_in'
     ]
 
     if endpoint in not_auth_table:
@@ -113,15 +122,68 @@ def r_before_request():
             g.config.id = 1
             g.config.get()
 
+            token = session.get('token', '')
+            g.token = Utils.verify_token(token)
+
+            user = User()
+            user.id = g.token['uid']
+
+            try:
+                user.get()
+            except ji.PreviewingError, e:
+                # 如果该用户获取失败,则清除该用户对应的session。因为该用户可能已经被删除。
+                for key in session.keys():
+                    session.pop(key=key)
+                return json.loads(e.message)
+
     except ji.JITError, e:
         ret = json.loads(e.message)
 
         if ret['state']['code'] == '404':
             return redirect(location=url_for('v_config.create'), Response=Response)
 
+        if ret['state']['sub']['code'] in ['41208']:
+            return redirect(location=url_for('v_misc.login'), Response=Response)
+
         return ret
 
 
+@app.after_request
+@Utils.dumps2response
+def r_after_request(response):
+    try:
+        # https://developer.mozilla.org/en/HTTP_access_control
+        # (中文版) https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Credentials
+        # http://www.w3.org/TR/cors/
+        # 由于浏览器同源策略,凡是发送请求url的协议、域名、端口三者之间任意一与当前页面地址不同即为跨域。
+
+        if request.referrer is None:
+            # 跑测试脚本时,用该规则。
+            response.headers['Access-Control-Allow-Origin'] = '*'
+        else:
+            # 生产环境中,如果前后端分离。那么请指定具体的前端域名地址,不要用如下在开发环境中的便捷方式。
+            # -- Access-Control-Allow-Credentials为true,携带cookie时,不允许Access-Control-Allow-Origin为通配符,是浏览器对用户的一种安全保护。
+            # -- 至少能避免登录山寨网站,骗取用户相关信息。
+            response.headers['Access-Control-Allow-Origin'] = '/'.join(request.referrer.split('/')[:3])
+
+        response.headers['Access-Control-Allow-Credentials'] = 'true'
+        response.headers['Access-Control-Allow-Methods'] = 'HEAD, GET, POST, DELETE, OPTIONS, PATCH, PUT'
+        response.headers['Access-Control-Allow-Headers'] = 'X-Request-With, Content-Type'
+        response.headers['Access-Control-Expose-Headers'] = 'Set-Cookie'
+
+        # 少于session生命周期一半时,自动对其续期
+        if not is_not_need_to_auth(request.endpoint) and hasattr(g, 'token') and \
+                        g.token['exp'] < (ji.Common.ts() + (app.config['token_ttl'] / 2)):
+            token = Utils.generate_token(g.token['uid'])
+            # 清除原有session,由新session代替
+            for key in session.keys():
+                session.pop(key=key)
+            session['token'] = token
+        return response
+    except ji.JITError, e:
+        return json.loads(e.message)
+
+
 # noinspection PyBroadException
 try:
     db.init_conn_mysql()
@@ -168,6 +230,7 @@ try:
 except:
     logger.error(traceback.format_exc())
 
+
 if __name__ == '__main__':
     # noinspection PyBroadException
     try:

+ 20 - 0
misc/init.sql

@@ -3,6 +3,26 @@ CREATE DATABASE IF NOT EXISTS jimv CHARACTER SET utf8;
 USE jimv;
 
 
+CREATE TABLE IF NOT EXISTS user(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    login_name VARCHAR(30) NOT NULL UNIQUE,
+    password VARCHAR(100) NOT NULL,
+    create_time BIGINT UNSIGNED NOT NULL,
+    mobile_phone VARCHAR(13) NOT NULL DEFAULT '',
+    email VARCHAR(30) NOT NULL DEFAULT '',
+    mobile_phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
+    email_verified BOOLEAN NOT NULL DEFAULT FALSE,
+    enabled BOOLEAN NOT NULL DEFAULT TRUE,
+    role_id BIGINT NOT NULL DEFAULT 0,
+    PRIMARY KEY (id))
+    ENGINE=InnoDB
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE user ADD INDEX (login_name);
+ALTER TABLE user ADD INDEX (mobile_phone);
+ALTER TABLE user ADD INDEX (email);
+
+
 CREATE TABLE IF NOT EXISTS guest(
     id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
     uuid CHAR(36) NOT NULL,

+ 5 - 1
models/__init__.py

@@ -27,6 +27,10 @@ from orm import (
     ORM
 )
 
+from user import (
+    User
+)
+
 from config import (
     Config
 )
@@ -84,7 +88,7 @@ __copyright__ = '(c) 2017 by James Iter.'
 
 __all__ = [
     'Rules', 'Utils', 'Init', 'Database', 'FilterFieldType', 'Filter', 'EmitKind', 'GuestState', 'DiskState',
-    'LogLevel', 'ORM', 'Config', 'Guest', 'Disk', 'BootJob', 'OperateRule', 'OSTemplate', 'GuestXML', 'Log',
+    'LogLevel', 'ORM', 'User', 'Config', 'Guest', 'Disk', 'BootJob', 'OperateRule', 'OSTemplate', 'GuestXML', 'Log',
     'EventProcessor', 'ResponseState', 'CPUMemory', 'Traffic', 'DiskIO', 'HostCPUMemory', 'HostTraffic',
     'HostDiskUsageIO'
 ]

+ 55 - 0
models/user.py

@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import jimit as ji
+
+from filter import FilterFieldType
+from orm import ORM
+
+
+__author__ = 'James Iter'
+__date__ = '2017/9/14'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2017 by James Iter.'
+
+
+class User(ORM):
+
+    _table_name = 'user'
+    _primary_key = 'id'
+
+    def __init__(self, **kwargs):
+        super(User, self).__init__()
+        self.id = 0
+        self.login_name = kwargs.get('login_name', None)
+        self.password = kwargs.get('password', None)
+        self.create_time = ji.Common.tus()
+        self.mobile_phone = ''
+        self.email = ''
+        self.mobile_phone_verified = False
+        self.email_verified = False
+        self.enabled = True
+        self.role_id = 0
+
+    @staticmethod
+    def get_filter_keywords():
+        return {
+            'id': FilterFieldType.INT.value,
+            'login_name': FilterFieldType.STR.value,
+            'create_time': FilterFieldType.INT.value,
+            'mobile_phone': FilterFieldType.STR.value,
+            'email': FilterFieldType.STR.value,
+            'mobile_phone_verified': FilterFieldType.BOOL.value,
+            'email_verified': FilterFieldType.BOOL.value,
+            'enabled': FilterFieldType.BOOL.value,
+            'role_id': FilterFieldType.INT.value
+        }
+
+    @staticmethod
+    def get_allow_update_keywords():
+        return ['mobile_phone_verified', 'email_verified', 'enabled', 'role_id']
+
+    @staticmethod
+    def get_allow_content_search_keywords():
+        return ['login_name', 'mobile_phone', 'email']
+

+ 6 - 38
templates/login.html

@@ -9,9 +9,10 @@
         }
 
         .btn,
+        .content-box,
         .form-group>div>div,
         .form-control {
-            border-radius: 0;
+            border-radius: 0 !important;
         }
 
         .form-horizontal > .form-group {
@@ -37,61 +38,28 @@
             <h3 class="text-center pad25B font-primary" style="font-family: 'Baskerville'; opacity: 0.83; font-size: -webkit-xxx-large;">JimV<span style="font-family: 'serif';"> Admin</span></h3>
             <div id="login-form" class="content-box bg-default">
                 <div class="content-box-wrapper pad20A">
-                    <img class="mrg25B center-margin radius-all-100 display-block" src="../../assets/image-resources/gravatar.jpg" alt="">
                     <div class="form-group">
                         <div class="input-group">
                             <span class="input-group-addon addon-inside bg-gray">
-                                <i class="glyph-icon icon-envelope-o"></i>
+                                <i class="glyph-icon icon-user"></i>
                             </span>
-                            <input type="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
+                            <input type="email" class="form-control" id="login_name" placeholder="用户名" value="admin" disabled>
                         </div>
                     </div>
                     <div class="form-group">
                         <div class="input-group">
                             <span class="input-group-addon addon-inside bg-gray">
-                                <i class="glyph-icon icon-unlock-alt"></i>
+                                <i class="glyph-icon icon-lock"></i>
                             </span>
-                            <input type="password" class="form-control" id="exampleInputPassword1" placeholder="Password">
+                            <input type="password" class="form-control" id="password" placeholder="密码">
                         </div>
                     </div>
                     <div class="form-group">
                         <button type="submit" class="btn btn-block btn-primary">Login</button>
                     </div>
-                    <div class="row">
-                        <div class="checkbox-primary col-md-6" style="height: 20px;">
-                            <label>
-                                <input type="checkbox" id="loginCheckbox1" class="custom-checkbox">
-                                Remember me
-                            </label>
-                        </div>
-                        <div class="text-right col-md-6">
-                            <a href="#" class="switch-button" switch-target="#login-forgot" switch-parent="#login-form" title="Recover password">Forgot your password?</a>
-                        </div>
-                    </div>
                 </div>
             </div>
-
-            <div id="login-forgot" class="content-box bg-default hide">
-                <div class="content-box-wrapper pad20A">
-
-                    <div class="form-group">
-                        <label for="exampleInputEmail2">Email address:</label>
-                        <div class="input-group">
-                            <span class="input-group-addon addon-inside bg-gray">
-                                <i class="glyph-icon icon-envelope-o"></i>
-                            </span>
-                            <input type="email" class="form-control" id="exampleInputEmail2" placeholder="Enter email">
-                        </div>
-                    </div>
-                </div>
-                <div class="button-pane text-center">
-                    <button type="submit" class="btn btn-md btn-primary">Recover Password</button>
-                    <a href="#" class="btn btn-md btn-link switch-button" switch-target="#login-form" switch-parent="#login-forgot" title="Cancel">Cancel</a>
-                </div>
-            </div>
-
         </form>
-
     </div>
 </div>
 {% endblock body %}

+ 12 - 8
views/misc.py

@@ -3,7 +3,7 @@
 
 
 import json
-from flask import Blueprint, render_template, url_for, request
+from flask import Blueprint, render_template, request
 import requests
 
 
@@ -25,16 +25,20 @@ def login():
 
     if request.method == 'POST':
         payload = {
-            'jimv_edition': int(request.form.get('jimv_edition', 0))
+            'login_name': request.form.get('login_name'),
+            'password': request.form.get('password')
         }
 
-        url = host_url + '/api/config'
+        url = host_url + '/api/user/_sign_in'
         headers = {'content-type': 'application/json'}
-        config_ret = requests.post(url, data=json.dumps(payload), headers=headers)
-        config_ret = json.loads(config_ret.content)
-        return render_template('success.html', go_back_url='/', timeout=5000, title='提交成功',
-                               message_title='初始化 JimV 请求已被接受',
-                               message='JimV 已被初始化。页面将在5秒钟后自动跳转到实例列表页面!')
+        ret = requests.post(url, data=json.dumps(payload), headers=headers)
+        ret = json.loads(ret.content)
+
+        if ret['state']['code'] == '200':
+            return
+
+        else:
+            pass
 
     else:
         return render_template('login.html')