
文章目录
前言
在当今的数字化时代,用户认证是任何在线服务安全性的基石。本文将简明扼要地介绍登录注册流程中的核心概念:HTTP无状态性、Session、Token与JWT,并详细阐述两种实用登录方式—— 手机号登录验证(借助容联云/云通讯服务) 与钉钉第三方登录。我们将探讨这些概念的基本原理,并深入解析两种登录方式的实现流程,旨在帮助开发者提升用户认证的安全性与便捷性。
一、几个关键概念
首先,我们来分析登录注册流程中涉及的几个关键概念及其工作机制,特别是关于HTTP无状态性、Session机制、Token认证,以及它们如何影响 单点登录(SSO) 的实现。
1.HTTP无状态性
HTTP协议本身是无状态的,这意味着服务器不会保留来自先前请求的客户端信息。每次请求都被视为完全独立的,服务器不会记住之前发生了什么。正是由于HTTP的无状态性,服务器无法直接记住用户是否已经登录或注册。这意味着每次用户请求都需要重新验证用户的身份,这在没有额外机制的情况下是不可行的。这种设计虽然简化了服务器的实现,但也要求开发者实现一些机制来跟踪用户会话和状态。
2.Session机制
工作原理:
- 1. 请求发起:客户端(如浏览器)向服务器发起请求。
- 2. 验证与生成Session:服务器验证请求(如登录验证),验证通过后,在服务器上创建一个Session对象,并为其分配一个唯一的标识符(如JSESSIONID)。这个Session对象可以存储用户的信息,如用户名、权限等。
- 3. 返回Session ID:服务器将JSESSIONID作为响应的一部分(通常通过Set-Cookie头部)发送给客户端。
- 4. 客户端存储:客户端(浏览器)将JSESSIONID存储在Cookie中。
- 5. 后续请求:在后续的请求中,客户端会自动将JSESSIONID包含在请求头(如Cookie)中发送给服务器。
- 6. 验证Session:服务器通过JSESSIONID在服务器上查找对应的Session对象,如果找到,则继续处理请求;如果未找到,则可能表示用户未登录或Session已过期。
不适合单点登录的原因:
- 每个应用或服务都可能有自己的Session管理机制,这使得跨多个应用或服务共享登录状态变得复杂。
3.Token认证
工作原理:
- 1.请求发起:客户端向服务器发起请求(如登录请求)。
- 2.验证与生成Token:服务器验证请求(如用户名和密码),验证通过后,生成一个唯一的Token(通常是一个加密的字符串),并将其存储在数据库中(或缓存中)。
- 3.返回Token:服务器将Token作为响应的一部分发送给客户端。
- 4.客户端存储:客户端将Token存储在Cookie、LocalStorage或SessionStorage中(取决于具体实现和安全需求)。
- 5.后续请求:在后续的请求中,客户端将Token包含在请求头(如Authorization: Bearer )中发送给服务器。
- 6.验证Token:服务器通过查询数据库(或缓存)来验证Token的有效性,如果Token有效,则继续处理请求。
适合单点登录的原因:
- Token可以在多个应用或服务之间共享,只要它们能够访问存储Token的数据库或缓存。
- 通过适当的认证服务器(如OAuth2.0中的授权服务器),可以实现跨域的单点登录。
4.JWT
JWT(JSON Web Token) 是一种无状态的Token认证机制,它允许服务器在Token中直接包含用户的身份信息和其他必要的验证信息。客户端在每次请求时携带这个Token,服务器通过验证Token来识别用户的身份和权限。
JWT的组成部分:
JWT由三部分组成:头部(Header)、载荷(Payload)和签名(Signature),每一部分都通过Base64编码后进行传输。
- 头部(Header):包含了令牌的元数据,如令牌的类型(JWT)和所使用的签名算法(如HMAC SHA256或RSA)。
- 载荷(Payload):包含了实际要传输的用户信息和其他元数据。这些信息可以是用户的姓名、角色、权限、到期时间等。载荷是JWT的主体部分,用于传递用户信息。
- 签名(Signature):用于验证JWT的完整性和真实性。签名通过对头部和载荷进行哈希运算,并使用私钥(在生成JWT时)或公钥(在验证JWT时)进行加密生成。接收者可以使用公钥对签名进行解密,从而验证JWT的真实性和来源。
- 加密: base64(头部).base64(载荷).HS256(base64(头部).base64(载和),‘盐’)
- 解密: HS256(base64(头部).base64(载荷),‘盐’) 生成一个新的签名和传递过来的签名进行对比
根据上述内容,我们可以构建一个简单的jwt工具类用于jwt的加密解密:
from fuguang_back.settings import SECRET_KEY
import jwt
import time
classMyJwt():def__init__(self):
self.secret = SECRET_KEY
# 加密defjwt_encode(self, payload):# 载荷、盐、加密方式return jwt.encode(payload, self.secret, algorithm='HS256')# 解密defjwt_decode(self, token):# token,盐、解密方式return jwt.decode(token, self.secret, algorithms=['HS256'])
mjwt = MyJwt()# user = {"id":1,"name":"zhangsan","exp":int(time.time()) + 3600}# token = mjwt.jwt_encode(user)# print(token)# token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwibmFtZSI6InpoYW5nc2FuIiwiZXhwIjoxNzE5OTI0OTQ5fQ.rTPpHAx-92Pz1CmoRdO-yfGlAhAfuMInju2bFi1iixs"# data = mjwt.jwt_decode(token)# print(data)# {'id': 1, 'name': 'zhangsan', 'exp': 1719924949}
二、通过手机号验证码登录
手机号登录流程:
- 1.在登录界面输入手机号,点击发送验证码
- 2.写一个发送验证码的接口,获取手机号(正则有效性验证),限制一分钟内只能发一次,查询redis中是否存在,如果存在返回已经发过,不存在调用发送,发送成功后存入redis
- 3.用户输入验证码点击登录
- 4.写一个登录接口 - 获取用户输入的手机号和验证码- 通过手机号查询redis获取验证码,如果存在,则对比验证码- 通过手机号查询用户表,如果存在获取用户信息生成 jwt token,如果不存在,写入用户表,用户信息生成 jwt token返回给客户端
- 5.客户端把token存在 localStorage中,以后每次请求在头部携带token。vue设置 interceptors拦截器,对每次请求前统一在头部加token
- 6.服户端验证采用中间件方式实现。自定义中间件,继承MiddiwareMinxin类重写process_request.方法中 - 定义白名单,在登录前需要操作的接口放到白名单中 - 如果不在白名单,获取token,验证- 验证是否被修改,是否过期,是否已经退出 (点击退出,把token存入redis,加一个过期时间),任何一个问题,return 401没有权限操作,通过继续下一步操作
- 安全问题及优化 - 1.token加过期时间 retoken。 - 登录成功后返回 token(3小时) retoken(4小时),当快到期的时候客户端携带retoken来换取新的token更新- 2.oa系统公司内部使用加ip地址过滤- 3.https证书加密
1.前端短信登录界面

src\components\Login.vue
<!-- 1-短信登录 --><divclass="inp"v-show="user.login_type==1"><inputv-model="user.account"type="text"placeholder="手机号码"class="user"><inputv-model="user.code"type="text"class="code"placeholder="短信验证码"><el-buttonid="get_code"type="primary"@click="sendsms">获取验证码</el-button><divclass="rember"><label><inputtype="checkbox"class="no"v-model="user.rememberMe"/><span>记住我</span></label><p>忘记密码</p></div><buttonclass="login_btn"@click="loginmobile">登录</button><pclass="go_login">没有账号 <router-linkto="/register">立即注册</router-link></p></div>
// 点击事件:发送短信验证码constsendsms=()=>{//在前端正则验证手机号是否正确let reg=/^1[3-9]\d{9}$/if(!reg.test(user.account)){alert("[前端]手机号校验失败!")returnfalse;}
http.get(`/sendsms/?phone=${this.account}`).then((result)=>{if(result.data.code =="200"){alert("发送成功!")}else{alert(result.data.message)}}).catch((err)=>{alert(err)});}// 点击事件:登录按钮constloginmobile=()=>{
http.post("/login/",{"mobile":user.account,"code":user.code}).then(res=>{if(res.data.code=="200"){//登录成功
localStorage.setItem('userid',res.data.userid);//客户端存储userid
localStorage.setItem('token',res.data.token);//客户端存储tokenreturn router.push("/");//跳转主页}else{return router.push("/login");//登录失败-->调整登录页面}}).catch((err)=>{
console.log(err);});}
2.发送短信接口与短信登录接口
*** 这里使用容联云服务,来通过短信发送验证码***
云通讯后台管理:https://console.yuntongxun.com/member/numbermanager
容联云短信开发手册:https://doc.yuntongxun.com/pe/5f029ae7a80948a1006e776e
参考容联云短信开发手册,构建发送短信的工具类,代码如下:
#tools/common.pyfrom ronglian_sms_sdk import SmsSDK
import json
accId ='容联云通讯分配的主账号ID'
accToken ='容联云通讯分配的主账号TOKEN'
appId ='容联云通讯分配的应用ID'# 发送短信defsend_message(mobile,sms_code):
sdk = SmsSDK(accId, accToken, appId)
tid ='1'
mobile = mobile
datas =(sms_code,)
resp = sdk.sendMessage(tid, mobile, datas)
data = json.loads(resp)#json-->对象print(data)if data['statusCode']=='000000':returnTruereturnFalse
接口:发送短信(验证码)接口、登录接口
# user/views.py# 发送验证码/获取验证码接口classSendMobileCodeView(APIView):defget(self, request):# send_sms前端: /sendsms/?phone=${this.account}/
phone = request.GET.get('phone')print(phone)#if not re.match(r"1[3-9]\d{9}$",phone):# return Response({"message":"手机号验证失败!","code":"410"})
flag = r.get_str("phone")if flag:return Response({"message":"一分钟只能发送一次验证码,请稍稍后发送!","code":"200"})
sms_code = random.randint(1000,9999)
r.delete_str("sms_code")
r.setex_str("sms_code",60*5,sms_code)# times:验证码5分钟有效期
send_message(phone, sms_code)# 发送短信(验证码)
r.setex_str("phone",60,phone)#同一个手机号60s内只能发送一次验证码return Response({"message":"发送成功","code":"200"})# 手机短信登录接口classLoginView(APIView):defpost(self, request):
myphone = request.data.get("mobile")
mycode = request.data.get("code")
sms_code = r.get_str("sms_code")#从redis中取出系统生成的验证码if mycode == sms_code:#与前端发送来的验证码对比
user = UsersModel.objects.filter(phone=myphone).first()# 获取用户信息 生成 jwt token
token = mjwt.jwt_encode({"userid":user.id,"exp":int(time.time())+3600})return Response({"code":200,"token":token,"userid":user.id})else:return Response({"code":4001,"message":"验证码错误!"})
3.Vue 设置interceptors拦截器
客户端把token存在 localStorage中,以后每次请求在头部携带token。
vue interceptors拦截器,对每次请求前统一在头部添加token
src\http\index.js
import axios from"axios"import settings from"../settings";import router from"../router"const http = axios.create({// timeout: 2500, // 请求超时,有大文件上传需要关闭这个配置baseURL: settings.host,// 设置api服务端的默认地址[如果基于服务端实现的跨域,这里可以填写api服务端的地址,如果基于nodejs客户端测试服务器实现的跨域,则这里不能填写api服务端地址]withCredentials:false,// 是否允许客户端ajax请求时携带cookie})// 请求拦截器 +token
http.interceptors.request.use((config)=>{
console.log("http请求之前");//获取登录后localStorage存储的tokenlet token = localStorage.getItem('token')if(token){
config.headers.Authorization = token;}return config;},(error)=>{
console.log("http请求错误");return Promise.reject(error);});// 响应拦截器
http.interceptors.response.use((response)=>{return response;},(error)=>{if(error.code ==="ERR_NETWORK"){
ElMessage.error("网络异常,无法请求服务端信息!");}if(error.response.status ===401){
ElMessage.error("未登录或登录超时!限制本次请求操作!请求登录后继续!");return router.push("/login");}return Promise.reject(error);});exportdefault http;
4. 服务端验证采用自定义中间件方式实现
自定义中间件,继承MiddiwareMinxin类重写process_request.方法中
- 定义白名单,在登录前需要操作的接口放到白名单中 - a.如果不在白名单,获取token,验证- b.验证是否被修改,是否过期,是否已经退出 (点击退出,把token存入redis,加一个过期时间),任何一个问题,return 401没有权限操作,通过继续下一步操作
from django.http import JsonResponse
from django.utils.deprecation import MiddlewareMixin
from tools.myjwt import mjwt
from tools.myredis import r
import time
classPermitionMiddleware(MiddlewareMixin):defprocess_request(self, request):# 1.定义白名单,在登录前需要操作的接口放到白名单中
wlist =['/register/','/login/','/sendsms/']# 获取当前的url
path = request.path
# 2.如果不在白名单,获取token,验证if path notin wlist:try:
token = request.headers.get('Authorization')
data = mjwt.jwt_decode(token)except:return JsonResponse({"code":401,"mes":"token不存在或者被修改"})# data没问题 ↓
exp =int(data['exp'])
now =int(time.time())# 判断是否过期if now > exp:return JsonResponse({"code":401,"mes":"token已经过期不能操作"})#是否退出,退出时存token
value = r.get_str(token)if value:return JsonResponse({"code":401,"mes":"用户已经退出,不能操作"})# ↑↑↑3.验证是否被修改,是否过期,是否已经退出 (点击退出,把token存入redis, 加一个过期时间),任何一个问题,return 401没有权限操作,通过继续下一步操作
不要忘记在主项目下的settings.py文件下配置写好的自定义中间件
MIDDLEWARE =['corsheaders.middleware.CorsMiddleware','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',# 'user.middleware.PermitionMiddleware', #自定义中间件]
5. 操作流程及效果图如下:
1.输入手机号,点击发送验证码
2.手机接收验证码3.输入验证码后,点击登录
三、通过第三方平台进行登录
三方登录,这里以钉钉为例
1.准备工作
钉钉开放平台:https://open.dingtalk.com/
- 进入钉钉开放平台,注册登录钉钉账号
- 注册应用,配置回调地址
具体操作流程如下:
1.登录开发者后台
2.创建应用
3.安全设置
4.凭证与基础信息
5.权限管理
2.前端钉钉登录界面
<!-- 0-密码登录 --><divclass="inp"v-if="user.login_type==0"><inputv-model="user.account"type="text"placeholder="用户名 / 手机号码 / 邮箱"class="user"><inputv-model="user.password"type="password"class="pwd"placeholder="密码"><divclass="rember"><label><inputtype="checkbox"class="no"v-model="user.rememberMe"/><span>记住我</span></label><p>忘记密码</p></div><buttonclass="login_btn"@click="show_captcha">登录</button><pclass="go_login">没有账号 <router-linkto="/register">立即注册</router-link></p><p><a:href="ddurl">钉钉登录</a></p></div>
onMounted(()=>{
http.get("dingtalkLogin/").then((result)=>{
ddurl.value = result.data.url;
console.log(result.data);}).catch((err)=>{alert(err)});})
3.后端逻辑处理
三方登录表:
- 字段:id、userid(外键)、方式(wb、qq、dd)、uid、token、retoken
处理流程:
- 1.点击钉钉登录,调用钉钉三方登录接口DingTalkLogin,跳转到钉钉授权页面
- 2.授权通过,登录成功,根据回调地址进行成功接口的回调 - 获取code、调用授权接口、获取uid- 根据token获取手机号- 查询三方登录表 - 不存在,在用户表中进行注册,同时更新三方登录表- 根据uid获取用户信息,生成token,跳转到前端页面
# 钉钉三方登录接口classDingTalkLogin(APIView):defget(self, request):from urllib.parse import quote
params =[f"redirect_uri={quote('http://127.0.0.1:8000/user/dingtalkCallback/')}","response_type=code","client_id=dingrcnkswwakld0y5jx","scope=openid","prompt=consent"]
url ="https://login.dingtalk.com/oauth2/auth?"+("&".join(params))return Response({"url": url})# 钉钉回调接口(钉钉登录接口 点击登录后调用)classDingTalkCallback(APIView):defget(self, request):
authCode = request.query_params.get('authCode')# 根据authCode获取用户accessToken
data ={"clientId":"钉钉开放平台-Client ID","clientSecret":"钉钉开放平台-Client Secret","code": authCode,"grantType":"authorization_code"}
resp = requests.post('https://api.dingtalk.com/v1.0/oauth2/userAccessToken', json=data).json()
accessToken = resp.get('accessToken')# 根据accessToken获取用户信息
headers ={"x-acs-dingtalk-access-token": accessToken}
resp = requests.get('https://api.dingtalk.com/v1.0/contact/users/me', headers=headers).json()
name = resp.get('nick')
uid = resp.get('openId')
phone = resp.get('mobile')print(name)print(uid)print(phone)# ---# 登录,查询三方登录表,是否存在
Sfuser = SfLoginModel.objects.filter(uid__exact=uid).first()print("三方登录表中--->")print(Sfuser)if Sfuser isNone:# 注册用户表和三方登录表(先判断用户表是否已有用户信息)
user = UsersModel.objects.filter(phone__exact=phone).first()print(user)if user isNone:
userinfo ={"phone":phone,"username":name,"password":"123"}
userSer = UsersSerializer(data=userinfo)if userSer.is_valid():
userSer.save()else:return Response({"code":10001,"message":userSer.errors})# 存在用户,写入三方登录表
sfinfo ={"type":1,"uid":uid,"token":accessToken,"user":user.id}
sfSer = SfLoginSerializer(data=sfinfo)if sfSer.is_valid():
sfSer.save()else:return Response({"code":10001,"message":sfSer.errors})else:#三方登录表中存有信息,uid->获取用户信息,生成token# 根据uid获取用户信息,生成token,跳转到前端页面
user = Sfuser.user
Sfuser.token = accessToken
Sfuser.save()# 生成jwt token 并返回前端
payload ={"userid":user.id,"username":user.username,"exp":int(time.time())+3600}
token = mjwt.jwt_encode(payload)
payload["exp"]=int(time.time())+3600
retoken = mjwt.jwt_encode(payload)
query =[f"userid={payload['userid']}",f"username={payload['username']}",f"token={token}",f"retoken={retoken}"]return HttpResponseRedirect('http://localhost:3000/?'+'&'.join(query))
4.操作流程及效果图如下:
1.登录界面点击钉钉登录:
2.跳转钉钉授权登录页面
3.点击登录后,通过钉钉配置的回调接口,跳转自己的平台首页

版权归原作者 么凹猫' 所有, 如有侵权,请联系我们删除。











