猛犸系统-登录功能开发
登录功能开发
后台构建
python3.7+、Django3.0+
安装
在Pycharm中,创建Django项目Mammothbe,命令行中执行以下操作
$ pip install django
$ pip install mysqlclient
$ pip install djangorestframework
$ django-admin startproject mammoth .
$ python manage.py startapp user
在user包目录下,新建urls.py、serializers.py
在项目根目录下,创建utils包目录
全局配置
setting.py
前后端分离,不使用模板
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
DEBUG = True # 生产环境一定设置为False
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'user',
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'mm_aiks',
'USER': 'root',
'PASSWORD': '123456',
'HOST': '127.0.0.1',
'PORT': '3306',
}
}
LANGUAGE_CODE = 'zh-Hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = True
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"loggers": {
"django.db.backends": {
"handlers": ["console"],
"level": "DEBUG",
},
},
}
迁移Django表
python manage.py migrate
创建超级用户
python manage.py createsuperuser
admin/adminadmin,也看到了用到系统表 INSERT INTO auth_user ,加入了超级用户admin
初始化git
$ git init
创建.gitignore文件,忽略掉一些非必要文件
$ git commit -m "Project Init"
构建git忽略文件
使用Pycharm,选择File/ New/ .ignore File/ .gitignore file(Git)来创建。/结尾表示目录,/开头表示从项目根目录开始
.gitignore
/t*.py
.idea/
migrations/
migrations/、.idea/,带斜杠结尾的只能匹配目录
/t*.py,.gitignore所在目录下的t开头的py文件
Django认证
认证:标识请求具有合法得身份。
认证和权限是2个不同概念,认证标识身份,权限和身份相关,表示该身份能干什么。比如,认证判断你是不是我们单位人,权限表示你在单位能接触到什么等级的信息。
HTTP协议的无状态,所以,必须使用一些机制来解决无状态的问题,例如Cookie-Session机制。
参考 https://docs.djangoproject.com/en/3.2/topics/auth/default/
django.contrib.auth中提供了许多方法:
1、认证
authenticate(**credentials)
使用AUTHENTICATION_BACKENDS 配置定义得认证类得authenticate方法,默认为django.contrib.auth.backends.ModelBackend
ModelBackend提供了用户认证能力,使用用户Model类验证用户名以及密码是否正确,检查is_active字段是否为1即激活得用户。
user = authenticate(username='someone',password='password')
本质上就是用用户名查了下数据库,如果用户存在,还需密码比对一致,且用户激活状态,才算成功,返回当前这个用户对象。
比对失败,返回None。
认证过程:
此函数会遍历配置中得认证类列表,调用backend类得authenticate方法,如果返回user时User得实例,则认证成功,否则返回None
如果是None,则测试下一个认证类
使用用户名加上密码认证,先使用用户来查库,如果有则进一步检查密码,还要看is_active,才能返回User得实例,如果某一个认证类认证成功,则返回User实例,全部认证失败,抛异常或者返回None,这个函数只能说明用户名和密码是否能够对应数据库用户表中唯一一个用户。
2、登录
login(request,user,backend=None)
- 该函数接受一个HttpRequest对象,以及一个认证了得User对象。不认证密码,只把user注入request
- 注入request.session[SESSION_KEY]
- 响应报文返回set-cookie带着SessionID,避免下一次请求后重新认证
user = authentica(username='xxx',password='xxxx')
if user:
login(request,user)
认证成功后,拿到request.user,user就是认证后的User实例,同时,request.session就会创建session信息,返回响应报文时,中间件就会set-cookie 将sessionid返回,因为http协议得无状态,下回浏览器端发来请求时cookie中就会携带sessionid,如果没有过期且查到了,就不需要使用用户名和密码登录了
3、登出
logout(request)
该函数接受一个HttpRequest对象,无返回值
当调用该函数时,当前请求得Session信息会全部清除,包括清除数据库django_session记录
request.user = AnonymousUser()
该用户即使没有登录,使用该函数也不会报错
4、身份确认
如果确认请求得身份?每一回都需要登录吗?这就要看cookie中得SessionID了
- 有SessionID就在服务器进行比对
- 比对成功则request.user就是用户对象
- 比对失败,就去登录页
- 无SessionID,直接去登录页
这个身份确认得过程时中间件完成的
中间件
参看 https://docs.djangoproject.com/en/1.11/topics/http/middleware/#writing-your-own-middleware
先来看看settings.py中定义的中间件
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions', # 基于数据库的session
'django.contrib.messages',
'django.contrib.staticfiles',
'user',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
#SESSION_COOKIE_NAME = 'sessionid' # 从cookie中提取session使用的名字
是一个列表,说明处理有顺序
基于Session得认证
SessionMiddleware
- 使用Model类django.contrib.sessions.models.Session操作表django_session
- 从请求报文中提取sessionid,构建request.session属性
def process_request(self, request):
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
request.session = self.SessionStore(session_key)
在请求来的时候有request对象,从request对象中提取sessionid,生成request.session属性
在响应时,配置login,set-cookie 传入sessionid
engine = import_module(settings.SESSION_ENGINE) 加载引擎
self.SessionStore = engine.SessionStore 持久化session
AuthenticationMiddleware
- 依赖于request.session,也就是说SessionMiddleware必须在前
- auth模块得get_user中查库
- 成功则返回user对象,is_authenticated为True
- 失败返回匿名用户对象
- 把返回得对象赋给request.user
注意:authenticate不管request.user是否为匿名用户,它负责对提供得凭证(可以是用户名密码)进行验证,验证成功返回user对象,否则返回None。
只在请求时工作,request.user = SimpleLazyObject(lambda: get_user(request)) ,就有了request.user属性,成功了就是User实例,失败了就是AnonymousUser()匿名用户,get_user从request里面提取sessionid,通过sessionid查数据库,获得session_data中得user_id,检查sessionid,查询数据库中得sessionid是否过期,是否存在,
第一次访问时,没有sessionid,请求依次通过中间件,通过request.user 就可以知道是否认证成功,如果是User的实例那就成功认证了。认证失败了,继续访问,直到视图函数
DRF也提供了基于Django Session得SessionAuthentication
- 认证成功,request.user存放user实例,request.auth是None
- 认证失败,返回403
- 不认证,request.user就是匿名用户,request.auth是None
- 浏览器发起请求时没有sessionid,不必验证csrftoken,一旦浏览器端提供了sessionid,就必须验证csrf token了,否则报错
https://www.django-rest-framework.org/api-guide/authentication/#sessionauthentication
上面的意思是,浏览器发起的请求,进入到DRF中,在调用视图函数之前,SessionAuthentication等就已经完成了对sessionid的确认。如果查到此ID,就经确认了用户的身份,也就是认证了,request.user就是该用户实例。如果没有sessionid或者对比失败,无法确认用户身份,request.user就是匿名用户对象。
settings.py中配置
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
#'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
]
}
基于token得认证
由于Session得一些固有问题,目前也有替代方案,令牌Token方案就是其中一种。
用户登录成功,发回一个Token到浏览器端,浏览器端每一次请求都发给服务器端这个令牌,有则表示登录过了,无则表示无身份,需要重新登录。
这个令牌值被签名,以防篡改,保护过期时间。服务器端可以校验,可以判断是否过期。
DRF提供了一个TokenAuthentication,它需要使用数据库存储Token,现在广泛用于互联网中得JWT(JSON WEB Token)比较好,它不需要数据库。DRF官网也提供了DRF得第三方插件
Token发送给服务器需要
1、配置有效期
2、加密,非对称加密,但是对比较繁忙得服务器,加密解密消耗cpu,可以用hash值
token组成和认证原理
payload {user_id:123, expire_time: 2022-03-28 00:00:00} + 签名
这样服务器端就不用保存user_id和过期时间了,也就是说不用数据库或内存存储大量得数据了
因为客户端可以篡改,因为服务器端没有保存,所以就使用签名技术。
如果payload得值有任何一个改变,就会导致签名算法得到得结果有巨大变化,那么payload 得值到达服务端就会再次使用服务端签名 算法 + 强密钥再算一次,和客户端发过来得签名进行比对,签名验证成功那么payload可信。
JWTAuthentication 会判断token是否有效(将payload解出来),如果无效则直接返回401,做一次加密得到签名,做认证
JWT官网:https://jwt.io/
官网:https://github.com/jazzband/djangorestframework-simplejwt
文档:https://django-rest-framework-simplejwt.readthedocs.io/en/latest/getting_started.html
要求:Python 3.7+、Django 2.2+、DRF 3.10+
$ pip install djangorestframework-simplejwt
settings.py中
INSTALLED_APPS = [
...
'rest_framework_simplejwt', # 注册应用,国际化,可选
...
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES':[
'rest_framework_simplejwt.authentication.JWTAuthentication',
]
}
主路由配置
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
# 获取
path('token/refresh/', TokenRefreshView.as_view(),
name='token_refresh'), # 刷新
]
TokenObtainPairView的父类TokenViewBase继承自GenericAPIView,且只实现了post方法,所以只能使用POST请求。
登录功能
登录功能实现
测试 POST http://127.0.0.1:8000/token/
错误 400 Bad Request
{
"username": [
"这个字段是必填项。"
],
"password": [
"这个字段是必填项。"
]
}
也就是说必须提供用户名和密码,认证成功后才能返回JWT Token
使用POST方法提交Json数据(用户名、密码)到http://127.0.0.1:8000/token/,内部会使用用户名密码查询数据库,认证成功返回token(有过期时间、user\_id),认证失败返回401。
JWT分为3部分,头、数据、签名,中间那部分使用Base64解码可以得到
{"token_type":"access","exp":1628792597,"jti":"df76bb1f43344eb3959dc2fa0c93e161","user_id":1}
jti就是jwt的id,唯一标识。user_id就是认证成功后返回的用户id值。exp过期的时间点的时间戳,默认5分钟后。
refresh是access短时间token过期后,对access延期的。我们这一次把access时间延长,过期就重新登录。
settings.py配置过期时间
参考:https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(hours=1),
#'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
}
登录效果测试
之后,浏览器每次都带上令牌发起HTTP请求。如果是POST请求,还需要带上CSRF Token
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from user.views import test
urlpatterns = [
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
# 获取
path('token/refresh/', TokenRefreshView.as_view(),
name='token_refresh'), # 刷新
path('test', test),
]
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.request import Request
@api_view(['POST', 'GET'])
def test(request:Request):
print('~' * 30)
print(request.COOKIES, request._request.headers)
print(request.data) # 被DRF处理为字典
print(request.user) # 可能是匿名用户
print(request.auth)
print('=' * 30)
if request.auth:
return Response({'test':10000})
else:
return Response({'test':20000})
GET测试
1、无Token
请求到达了视图函数,request.user 为AnonymousUser,request.auth为None
2、有Token
注意使用请求头字段Authorization
也就是访问的时候在请求报文头带上token就行。验证成功走正常流程,一旦失败走失败处理流程。
3、Token过期
注意:这个异常是在到达视图函数之前就抛出得
过期后,返回错误码,前端代码看到后就需要跳转到登录页
异常处理
如果登录或认证失败,返回4xx也可以,但是浏览器端使用得是Vue开发得,为了浏览器端处理方便,返回状态码统一采用200,那么如果返回错误信息呢?
约定返回得Json,如果正常code为0或者undefined,如果有异常,返回非0,并且提供message提供消息。
可以每一个视图中handler中单独设置异常怎么处理,也可以全局统一处理。
DRF得异常得基类是 rest_framework.exceptions.APIException
在utils包下创建exceptions.py
from rest_framework.response import Response
from rest_framework.views import exception_handler
from rest_framework import exceptions
class MagBaseException(exceptions.APIException):
"""基类定义基本的异常"""
code = 10000 # code为0表示正常,非0表示错误
message = "非法请求" # 错误描述
@classmethod
def get_message(cls):
return {'code': cls.code, 'message': cls.message}
class InvalidUsernameOrPassword(MagBaseException):
code = 1
message = "用户名或密码错误,请重新登录"
class InvalidToken(MagBaseException):
code = 10001
message = "token过期,请重新登录"
class NotAuthenticated(MagBaseException):
code = 10002
message = "未登录,请重新登录"
# 内部异常暴露徐杰,替换为自定义
exc_map = {
'AuthenticationFailed': InvalidUsernameOrPassword,
'InvalidToken' : InvalidToken,
'NotAuthenticated' : NotAuthenticated
}
def global_exception_handler(exc, context):
"""
全局异常处理
不管什么异常这里统一处理。根据不同类型显示不同得信息,为了前端JS解析方便,这里响应状态码统一为200,异常对应处理后返回对应得错误码和错误描述,异常找不到对应返回缺省
"""
# 调用DRF缺省异常处理,返回Response对应或None
response = exception_handler(exc, context)
print(type(exc),exc.__dict__)
if response is not None: # 说明就是APIException,否则是none
if isinstance(exc, MagBaseException):
# 如果是MagBaseException,就是自定义类,就直接返回消息
errmsg = exc.get_message()
else:
# 如果是APIException,就利用exc得类型名称映射到自定义类型
print('——————————————————————————————————————————————————————————————————————————————',exc.__class__.__name__)
errmg = exc_map.get(exc.__class__.__name__,MagBaseException).get_message()
return Response(errmg, status=200) # 状态码恒为200
return response
全局异常配置如下
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'utils.exceptions.global_exception_handler',
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication'
]
}
注意:global_exception_handler捕获的是异常,需要raise。
前后端联调
后台主路由增加登录
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from user.views import test
tobview = TokenObtainPairView.as_view() # 这个视图函数生成一次就可以了,可以调用n次
urlpatterns = [
path('login/', tobview, name='login'),
path('token/', tobview, name='token_obtain_pair'), # 获取
path('token/refresh/', TokenRefreshView.as_view(),
name='token_refresh'), # 刷新
path('test', test),
]
前端main.js
axios.defaults.baseURL = 'http://127.0.0.1:8000/'
Vue.prototype.$http = axios
前端LoginView.vue中得login函数
const res = await this.$http.post('login/', this.loginForm)
启动前端项目,输入用户名密码后点击登录,立即发现跨域请求问题(端口不一样)。解决方法就是发起对后台得请求,用代理
前端main.js
// axios全局设置,baseURL指向后台服务
axios.defaults.baseURL = '/api/v1' // 代理到'http://127.0.0.1:8000/'
// 为Vue类增加全局属性$http,这样所有组件实例都可以使用该属性了
Vue.prototype.$http = axios
前端代理
前端项目根目录下,配置vue.config.js(和package.json同级得)
参考:https://cli.vuejs.org/zh/config/#devserver-proxy
// const { defineConfig } = require('@vue/cli-service')
module.exports = ({
transpileDependencies: true,
devServer: {
proxy: {
'/api/v1': {
target: 'http://127.0.0.1:8000/',
}
}
}
})
登录发现竟然返回404,为什么?浏览器中可以看到访问的是
http://localhost:8080/api/v1/login 。也就是说,确实代理了,但是URL不对,多了/api/v1。拿掉它,就要用rewrite了。
去webpack官网 https://www.webpackjs.com/configuration/dev-server/#devserver-proxy
// const { defineConfig } = require('@vue/cli-service')
module.exports = ({
transpileDependencies: true,
devServer: {
proxy: {
'/api/v1': {
target: 'http://127.0.0.1:8000/',
pathRewrite: { '^/api/v1': '' }
}
}
}
})
终于,联调成功,用户名、密码正确后,状态码200,token返回,里面包含着过期时间和user_id。
Home组件
登录成功后,可以看见Home组件
src/views/HomeView.vue
<template>
<div>Home组件</div>
</template>
<script>
export default {}
</script>
<style>
</style>
route/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/LoginView.vue'
import Home from '../views/HomeView.vue'
Vue.use(VueRouter)
const routes = [
{ path: '/', redirect: '/login' },
{ path: '/login', component: Login },
{ path: '/home', component: Home }
]
const router = new VueRouter({
routes
})
export default router
登陆成功跳转,使用 this.$router ,还要用到编程式导航 https://router.vuejs.org/zh/guide/essentials/navigation.html
LoginView.vue如下
<script>
methods: {
loginEvent () {
console.log(this.$refs.loginFormRef)
this.$refs.loginFormRef.validate(async (valid) => {
if (valid) {
const res = await this.$http.post('login/', this.loginModel)
console.log(res)
const {
data,
status
} = res
console.log(data)
console.log(status)
if (data.code) {
this.$message.error(data.message)
console.log('登录失败')
} else {
console.log('登录成功')
window.localStorage.setItem('token', data.access)
this.$router.push('/home')
}
}
})
}
}
}
</script>
消息提示
src/plugins/element.js如下
import Vue from 'vue'
import { Form, FormItem, Input, Button, Message } from 'element-ui'
Vue.use(Button)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Input)
// 全局导入
Vue.prototype.$message = Message
全局导入后,在每一个Vue组件上都可以通过 this.$message 来使用了
LoginView.vue变化如下:
<script>
export default {
methods: {
login() {
this.$refs.loginFormRef.validate(async (valid) => {
if (valid) {
const res = await this.$http.post('login/', this.loginForm) //
post返回一个Promise
// console.log(res) // status状态码,data返回的数据
const { data: response } = res // data解构出来
// 返回的对象
// console.log(response.code, response.message)
if (response.code) {
// 如果返回的对象有code属性,说明登录不成功
return this.$message.error(response.message)
}
this.$message('登录成功')
this.$router.push('/home')
}
})
}
}
}
</script>
Token持久化
如果不做持久化,直接在浏览器输入http://127.0.0.1:8080/#/home,不用登录也可以访问,登录形同虚设。
应该将登陆的、未登录得区分开,未登陆了跳转到登录页。浏览器如何区分?用token,可以将token存储起来,方便使用。
浏览器会持久化一些键值对得数据,早期仅仅可以使用cookie,现在还可以使用LocalStorage、SessionStorage等。
LocalStorage存储得数据可以长期保留,SessionStorage得数据是会话级得,会话结束会被清除。
通过全局windows对象可以使用localStorage、sessionStorage属性设置其中得键值对。键值对总以字符串得形式存储。
// sessionStorage方法一样
window.localStorage.setItem('k1', 'v1')
var val = localStorage.getItem('k1')
localStorage.removeItem('k1')
localStorage.clear() // 移除所有
####### 导航守卫
说明:
除了/login 外,所有url前端路由都要通过这个导航守卫得检查,检查有没有token,如果有就放行,没有就返回登录页面。
如果有token,但是并不检查token得有效性,因为客户端无法检查,解决方案:
- 只要检查token,就立即把token发给服务端,服务端返回验证结果
- 有token不检查,显示组件时可以,但是没有数据,以后所有得数据都是从后端来得,如果需要数据就必须和后端通信,后端收到请求后就必须要token,token不对就不给前端数据
简单得方法,就是浏览器端登录后,拿到一个不可篡改得凭证,只要凭证在,就认为登录过了,这也是token。
浏览器每一次向服务端发起请求得时候,服务器端验证这个token,成功了服务器端返回请求得资源,失败返回失败代码。
管理后台几乎所有组件都需要身份验证,能不能找一个公共得地方,进行判断?无token直接路由到/login,有token,则跳转到目标路由,至于组件是否显示数据,需要把token发送到服务器端,由服务器端判断token是否有效。
全局导航守卫参考:https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
const router = new VueRouter({
routes
})
// 全局前置守卫,在每一次路由前。
// to要进入的目标,from当前导航要离开的路由
// next回调函数,next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是confirmed (确认的)。
router.beforeEach((to, from, next) => {
// ...
})
src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/LoginView.vue'
import Home from '../views/HomeView.vue'
Vue.use(VueRouter)
const routes = [
{ path: '/', redirect: '/login' },
{ path: '/login', component: Login },
{ path: '/home', component: Home }
]
const router = new VueRouter({
routes
})
// 挂载全局导航守卫
router.beforeEach((to, from, next) => {
// from从哪里来,to去哪里,next函数跳转
// 只要不是登录页,都要查看token
console.log(to)
console.log(from)
if (to.path === '/login') {
next()
} else {
// 读取token
const token = window.localStorage.getItem('token')
console.log(token)
if (!token) {
next('/login')
} else {
next()
}
}
})
export default router
存储token
登录成功后,提取access token存储起来
LoginView.vue 得script标签如下
methods: {
loginEvent () {
console.log(this.$refs.loginFormRef)
this.$refs.loginFormRef.validate(async (valid) => {
if (valid) {
const res = await this.$http.post('login/', this.loginModel)
console.log(res)
const {
data,
status
} = res
console.log(data)
console.log(status)
if (data.code) {
this.$message.error(data.message)
console.log('登录失败')
} else {
console.log('登录成功')
window.localStorage.setItem('token', data.access)
this.$router.push('/home')
}
}
})
}
}
前端提交合并代码
$ git add .
$ git commit -m "Login finished"
$ git branch
* login 还在login分支
master
合并到主分支
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ git branch
login
* master
$ git merge login
Updating e3182dc..0d55786
Fast-forward
缺省使用fast-forward合并,如果不需要使用--no-ff
$ git push
目前在master分支,所以,只把master分支推送到了远程仓库
推送login分支
$ git checkout login
Switched to branch 'login'
$ git push
fatal: The current branch login has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin login
$ git push -u origin login
Total 0 (delta 0), reused 0 (delta 0)
remote: Powered by GITEE.COM [GNK-5.0]
remote: Create a pull request for 'login' on Gitee by visiting:
remote:
https://gitee.com/cloudytimes/vueadmin/pull/new/cloudytimes:login...cloudyti
mes:master
To gitee.com:cloudytimes/vueadmin.git
* [new branch] login -> login
Branch 'login' set up to track remote branch 'login' from 'origin'.
后端提交合并代码
pycharm中
$ git checkout -b login
git checkout master 然后在菜单中操作login合并到master,然后push远程仓库。