用go设计开发一个自己的轻量级登录库/框架吧(业务篇)
用go设计开发一个自己的轻量级登录库/框架吧(业务篇)
本篇会讲讲框架的登录业务的实现。实现三种登录模式:
- 同一用户只能登录一次
- 同一用户多次登录多token
- 同一用户多次登录共享一个token
源码:weloe/token-go: a light login library (github.com)
存储结构
首先从我们要考虑是底层该怎么存储登录信息来去达成这三种登录模式
- 同一用户只能登录一次
- 同一用户多次登录多token
- 同一用户多次登录共享一个token
我们不能使用无状态token模式,要有状态,在后端存储会话信息才能达成想要实现的一些逻辑,因此,存储会话信息是必要的。
对于每个请求,我们会存储一个token-loginId的k-v结构。
对于整个会话,我们会存储一个loginId-session的k-v结构。
基于这个存储结构我们就可以方便的实现这三种模式。
Session结构体
session包括了多个tokenValue,这就是我们用来实现同一用户多次登录多token,或者同一用户多次登录共享一个token的关键点
type TokenSign struct {
Value string
Device string
}
type Session struct {
Id string
TokenSignList *list.List
}
总之,我们实现的业务将基于这两种k-v结构
功能实现
源码:https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer.go#L167
我们再来梳理一些功能和配置的对应关系
同一用户只能登录一次:IsConcurrent == false
同一用户多次登录多token: IsConcurrent == true && IsShare == false这时候配置MaxLoginCount才生效
同一用户多次登录共享一个token: IsConcurrent == true && IsShare == true
接着我们再讲讲登录的具体流程:
我们大致将它分为几个阶段:
生成token
生成session
存储token-id , id-session
返回信息
调用watcher和logger
检测登录人数
生成token
生成token的时候,我们要判断他是否是可多次登录,也就是isConcurrent是否为false。
如果可多次登录并且共享token即IsConcurrent == true && IsShare == true,就判断能否复用之前的token
这里我们还允许用户自定义token。
loginModel *model.Login是为了支持自定义这几个参数
type model.Login struct {
Device string
IsLastingCookie bool
Timeout int64
Token string
IsWriteHeader bool
}
// createLoginToken create by config.TokenConfig and model.Login
func (e *Enforcer) createLoginToken(id string, loginModel *model.Login) (string, error) {
tokenConfig := e.config
var tokenValue string
var err error
// if isConcurrent is false,
if !tokenConfig.IsConcurrent {
err = e.Replaced(id, loginModel.Device)
if err != nil {
return "", err
}
}
// if loginModel set token, return directly
if loginModel.Token != "" {
return loginModel.Token, nil
}
// if share token
if tokenConfig.IsConcurrent && tokenConfig.IsShare {
// reuse the previous token.
if v := e.GetSession(id); v != nil {
tokenValue = v.GetLastTokenByDevice(loginModel.Device)
if tokenValue != "" {
return tokenValue, nil
}
}
}
// create new token
tokenValue, err = e.generateFunc.Exec(tokenConfig.TokenStyle)
if err != nil {
return "", err
}
return tokenValue, nil
}
生成session
https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer.go#L183
先判断是否已经存在session,如果不存在需要先创建,避免空指针
// add tokenSign
if session = e.GetSession(id); session == nil {
session = model.NewSession(e.spliceSessionKey(id), "account-session", id)
}
session.AddTokenSign(&model.TokenSign{
Value: tokenValue,
Device: loginModel.Device,
})
存储
https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer.go#L192
在存储的时候,需要拼接key防止与其他的key重复
// reset session
err = e.SetSession(id, session, loginModel.Timeout)
if err != nil {
return "", err
}
// set token-id
err = e.adapter.SetStr(e.spliceTokenKey(tokenValue), id, loginModel.Timeout)
if err != nil {
return "", err
}
返回token
这个操作对应我们配置的TokenConfig的IsReadCookie、IsWriteHeader和CookieConfig
// responseToken set token to cookie or header
func (e *Enforcer) responseToken(tokenValue string, loginModel *model.Login, ctx ctx.Context) error {
if ctx == nil {
return nil
}
tokenConfig := e.config
// set token to cookie
if tokenConfig.IsReadCookie {
cookieTimeout := tokenConfig.Timeout
if loginModel.IsLastingCookie {
cookieTimeout = -1
}
// add cookie use tokenConfig.CookieConfig
ctx.Response().AddCookie(tokenConfig.TokenName,
tokenValue,
tokenConfig.CookieConfig.Path,
tokenConfig.CookieConfig.Domain,
cookieTimeout)
}
// set token to header
if loginModel.IsWriteHeader {
ctx.Response().SetHeader(tokenConfig.TokenName, tokenValue)
}
return nil
}
调用watcher和logger
https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer.go#L210
在事件发生后回调,提供扩展点
// called watcher
m := &model.Login{
Device: loginModel.Device,
IsLastingCookie: loginModel.IsLastingCookie,
Timeout: loginModel.Timeout,
JwtData: loginModel.JwtData,
Token: tokenValue,
IsWriteHeader: loginModel.IsWriteHeader,
}
// called logger
e.logger.Login(e.loginType, id, tokenValue, m)
if e.watcher != nil {
e.watcher.Login(e.loginType, id, tokenValue, m)
}
检测登录人数
https://github.com/weloe/token-go/blob/f58ba4d93f0f012972bf6a35b9127229b5c328fe/enforcer.go#L227
要注意的是检测登录人数需要配置IsConcurrent == true && IsShare == false,也就是:同一用户多次登录多token模式,提供一个特殊值-1,如果为-1就认为不对登录数量进行限制,不然就开始强制退出,就是删除一部分的token
// if login success check it
if tokenConfig.IsConcurrent && !tokenConfig.IsShare {
// check if the number of sessions for this account exceeds the maximum limit.
if tokenConfig.MaxLoginCount != -1 {
if session = e.GetSession(id); session != nil {
// logout account until loginCount == maxLoginCount if loginCount > maxLoginCount
for element, i := session.TokenSignList.Front(), 0; element != nil && i < session.TokenSignList.Len()-int(tokenConfig.MaxLoginCount); element, i = element.Next(), i+1 {
tokenSign := element.Value.(*model.TokenSign)
// delete tokenSign
session.RemoveTokenSign(tokenSign.Value)
// delete token-id
err = e.adapter.Delete(e.spliceTokenKey(tokenSign.Value))
if err != nil {
return "", err
}
}
// check TokenSignList length, if length == 0, delete this session
if session != nil && session.TokenSignList.Len() == 0 {
err = e.deleteSession(id)
if err != nil {
return "", err
}
}
}
}
测试
同一用户只能登录一次
IsConcurrent = false
IsShare = false
func TestEnforcerNotConcurrentNotShareLogin(t *testing.T) {
err, enforcer, ctx := NewTestNotConcurrentEnforcer(t)
if err != nil {
t.Errorf("InitWithConfig() failed: %v", err)
}
loginModel := model.DefaultLoginModel()
for i := 0; i < 4; i++ {
_, err = enforcer.LoginByModel("id", loginModel, ctx)
if err != nil {
t.Errorf("Login() failed: %v", err)
}
}
session := enforcer.GetSession("id")
if session.TokenSignList.Len() != 1 {
t.Errorf("Login() failed: unexpected session.TokenSignList length = %v", session.TokenSignList.Len())
}
}
同一用户多次登录共享一个token
IsConcurrent = true
IsShare = false
func TestEnforcer_ConcurrentNotShareMultiLogin(t *testing.T) {
err, enforcer, ctx := NewTestConcurrentEnforcer(t)
if err != nil {
t.Errorf("InitWithConfig() failed: %v", err)
}
loginModel := model.DefaultLoginModel()
for i := 0; i < 14; i++ {
_, err = enforcer.LoginByModel("id", loginModel, ctx)
if err != nil {
t.Errorf("Login() failed: %v", err)
}
}
session := enforcer.GetSession("id")
if session.TokenSignList.Len() != 12 {
t.Errorf("Login() failed: unexpected session.TokenSignList length = %v", session.TokenSignList.Len())
}
}
同一用户多次登录多token
IsConcurrent = true
IsShare = true
func TestEnforcer_ConcurrentShare(t *testing.T) {
err, enforcer, ctx := NewTestEnforcer(t)
if err != nil {
t.Errorf("InitWithConfig() failed: %v", err)
}
loginModel := model.DefaultLoginModel()
for i := 0; i < 5; i++ {
_, err = enforcer.LoginByModel("id", loginModel, ctx)
if err != nil {
t.Errorf("Login() failed: %v", err)
}
}
session := enforcer.GetSession("id")
if session.TokenSignList.Len() != 1 {
t.Errorf("Login() failed: unexpected session.TokenSignList length = %v", session.TokenSignList.Len())
}
}
至此,我们就实现了三种登录模式,
用go设计开发一个自己的轻量级登录库/框架吧(业务篇)的更多相关文章
- 用Vue开发一个实时性时间转换功能,看这篇文章就够了
前言 最近有一个说法,如果你看见某个网站的某个功能,你就大概能猜出背后的业务逻辑是怎么样的,以及你能动手开发一个一毛一样的功能,那么你的前端技能算是进阶中高级水平了.比如咱们今天要聊的这个话题:如何用 ...
- C# 如何设计一个好用的日志库?【架构篇】
〇.前言 相信你在实际工作期间经常遇到或听到这样的说法: "我现在加一下日志,等会儿你再操作下." "只有在程序出问题以后才会知道打一个好的日志有多么重要.&qu ...
- HTML5实战教程———开发一个简单漂亮的登录页面
最近看过几个基于HTML5开发的移动应用,比如臭名昭著的12036移动客户端就是主要使用HTML5来实现的,虽然还是有点反应迟钝,但已经比较流畅了,相信随着智能手机的配置越来越高性能越来越好,会越来越 ...
- 准备开发一个基于canvas的图表库,记录一些东西(一)
开源的图表库已经有很多了,这里从头写个自己的,主要还是 提高自己js的水平,增加复杂代码组织的经验 首先写一个画图的库,供以后画图表使用.经过2天的开发,算是能拿出点东西了,虽然功能还很弱,但是有了一 ...
- 【Mac系统 + Python + Django】之开发一个发布会系统【Django视图(二)】
此学习资料是通过虫师的python接口自动化出的书学习而来的,在此说明一下,想学习更多的自动化的同学可以找虫师的博客园,非广告,因为我python+selenium自动化也是跟虫师学的,学习效果很好的 ...
- Smart Framework:轻量级 Java Web 框架
Smart Framework:轻量级 Java Web 框架 收藏 黄勇 工作闲暇之余,我开发了一款轻量级 Java Web 框架 —— Smart Framework. 开发该框架是为了: 加 ...
- Qt移动应用开发(二):使用动画框架
Qt移动应用开发(二):使用动画框架 上一篇博客介绍了怎样使用Qt的QML来对屏幕分辨率大小进行适应,其实,不同分辨率的适应是一个很棘手的问题,除了分辨率不同外,宽高比(aspect ratio)也不 ...
- 给力的轻量级JavaScript动画框架 - jsMorph
jsMorph 是一个独立的轻量级 JavaScript 动画框架,可以用它来操纵多个 HTML 元素的样式,实现动画效果.此框架会自动检测起始位置.转换单位.调整渲染的速度,以此来获得更流畅的渲染体 ...
- 巧用第三方高速开发Android App 热门第三方SDK及框架
巧用第三方高速开发Android App 热门第三方SDK及框架 历经大半年的时间,最终是把这门课程给录制出来了,也就在今天,正式在慕课网上上线了 项目地址:巧用第三方高速开发Android App ...
- 巧用第三方快速开发Android App 热门第三方SDK及框架
巧用第三方快速开发Android App 热门第三方SDK及框架 历经大半年的时间,终于是把这门课程给录制出来了,也就在今天,正式在慕课网上上线了 项目地址:巧用第三方快速开发Android App ...
随机推荐
- 给jui(dwz)的菜单树换一套漂亮的图标
JUI是一个免费开源的框架,在使用初期,会遇到一些麻烦,因为文档实在太少了,完全不知道从哪里入门,但是,一旦你深入学习后,你会发现,你的选择是不错的,它会提高你开发的效率,同时,你会深深爱上它. 目前 ...
- RPA现阶段的问题
RPA(Robotic Process Automation)全称机器人流程自动化,作为"自动化为先"时代的翘楚和先驱,被广泛地用来代替人类自动执行任务,越来越多的领域.企业和人开 ...
- RPA的市场需求
最基本的RPA软件机器人定义:机器人通过记录员工在电脑桌面上的操作行为,将业务处理规则和操作行为记录下来,并模拟人的方式在电脑上自动执行一系列特定的工作流程.采用RPA软件机器人解决方案,快速实施,快 ...
- Jquery 和 Vue 入门学习
0x01 前言 零零散散学完了html.css.javascript的基础知识,但感觉写不了什么炫酷的前端界面,始终对前端开发有种生疏感.而时间的流逝也总会让我忘却零碎学习到的知识!为了改变这种尴 ...
- Nacos的微服务与本地测试的问题
前提条件: 这里是微服务上的yml的配置: uri: base-svc-authcenter: 192.168.1.121:28002 base-svc-file: 192.168.1.121:280 ...
- 设计一款可扩展和基于windows系统的一键处理表格小工具思路
原创总结/朱季谦 设计一款可扩展和基于windows系统的一键处理表格小工具思路 日常开发当中,业务人员经常会遇到一些重复性整理表格的事情,这时候,就可以通过一些方式进行自动化程序处理,提高工作(摸鱼 ...
- 如何解决 Iterative 半监督训练 在 ASR 训练中难以落地的问题丨RTC Dev Meetup
前言 「语音处理」是实时互动领域中非常重要的一个场景,在声网发起的「RTC Dev Meetup丨语音处理在实时互动领域的技术实践和应用」活动中,来自微软亚洲研究院.声网.数美科技的技术专家,围绕该话 ...
- CSS 基础属性篇组成及作用
#### 学习目标- css属性和属性值的定义- css文本属性- css列表属性- css背景属性- css边框属性- css浮动属性##### 一.css属性和属性值的定义>属性:属性是指定 ...
- ABAP READ内表新老语法对比
1.读取内表行新语法 740新语法中,对标READ,提出了新的语法,如下: 1.1.根据字段值查找 "-----------------------------@斌将军----------- ...
- 使用msf生成shellcode并用Go免杀?
msf生成的裸马现在已经不行了,加壳也只能加冷门壳了,VMP,Shielden,upx不是失效就是效果很差,所以当下,得用shellcode来免杀了 msfvenom -a x86 --platfor ...