修改织梦网站管理系统PHP程序,实现禁止同一会员帐号多地登录,一般来讲就要从判断IP入手。我修改的这个原理是:会员登录后增加创建名为Only的cookie,当会员刷新页或浏览新页面时判断从数据表@_member中当前会员的loginip值md5后与Only比较异同,把此条件加在验证用户是否已经登录函数IsLogin()中,成立返回真,不成立返回假,即可实现。修改如下:
修改文件/include/memberlogin.class.php
构造函数,大约171行左右的$this->OnlyCookie = GetCookie("Only");代码下一行增加代码如下:
$this->OnlyCookie = GetCookie("Only");
验证用户是否已经登录函数IsLogin(),大约第290行 改为
function IsLogin()
{
$loginipCookie = substr(md5($this->fields['loginip']),0,16);
if($this->M_ID > 0 && $this->OnlyCookie == $loginipCookie) return TRUE;
else return FALSE;
}
重置用户信息函数ResetUser()内的最后,大约第389行左右代码DropCookie('DedeLoginTime');的下一行增加代码如下(除管理员外的cookie方法):
DropCookie('Only');
意为重置会员cookie信息
大约第530行左右保存用户cookie的函数PutLoginInfo()内的if($this->M_KeepTime > 0)内增加代码如下(除管理员外的cookie方法);
PutCookie('Only',substr(md5(GetIp()),0,16),$this->M_KeepTime);
意为增加cookie条目
下面的else内最后加上
PutCookie('Only',$this->M_LoginTime);
修改文件/include/userlogin.class.php(这个没有测试管理员的帐号效果,后来我没我修改这条,也就是说这条改不改不影响除ID为1的其他会员的禁止多地登录效果)
keepUser()函数内大约第315行增加代码如下:
PutCookie('Only', substr(md5('MrDede'.GetIp()),0,16), 3600 * 24, '/');
保持用户的会话状态,这里给管理员(id=1)增加了名为Only的cookie信息
————————–
2020-02-26 修改
2021-02-01 再次修改
因为之前修改时的说明有些笼统,说明中隐含的借用了上面的说明,逻辑也有一些问题,造成使用此方法修改的朋友不能实现单点登录的功能,在这里表示抱歉。
这次(2021-02-01)有朋友来问我为什么不能实现单点登录功能,看自己的说明,实测了一下,确实。所以,又进行了回忆、修改、添加详细说明。现在实测可用。
当时可能是因为任务匆忙,没有写详细说明,只是在心里想着,没有写出来,造成现在自己看自己的说明都看不懂。这也是对自己的一个教训,以后一定要写详细的说明。
以下修改方法与以上方法无关
————————–
有朋友看到文章来找我帮着修改单点登录的功能,上面是过去的思路,随着时间的变化,对此功能的实现也有了新的想法。
因为现在的路由多是动态路由,所以靠IP来识别,就显示不严谨、不可靠了。另外,还要看用户的需求,一种需求是在同一IP下可以多设备登录,另一种需求就是即使在同一IP下也要实现单个设备登录。
新的思路(单设备登录):
1、数据表dede_member新建两个字段,token,token_time
ALTER TABLE `dede_member`
ADD COLUMN `token` varchar(120) NOT NULL DEFAULT '' COMMENT 'Token字符串' AFTER `checkmail`,
ADD COLUMN `token_time` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Token创建时间' AFTER `token`;
0
1
2
ALTERTABLE`dede_member`
ADDCOLUMN`token`varchar(120)NOTNULLDEFAULT''COMMENT'Token字符串'AFTER`checkmail`,
ADDCOLUMN`token_time`int(10)UNSIGNEDNOTNULLDEFAULT0COMMENT'Token创建时间'AFTER`token`;
上面SQL语句中的checkmail是表dede_member的最后一个字段,当然新字段也可以接在别的字段后,这个随自己的意。
2、在/include/memberlogin.class.php的MemberLogin类内部的最后增加方法:
/**
* 单点登录更新token - 织梦先生 添加
* 注:在确保按原有逻辑登录成功后,使用此方法
* @param boolean $set 是否直接设置 token,登录时$set=true,非登录时不会设置此参数
* @return boolean
*/
function resetToken($set = false) {
global $dsql;
$token_time = time();
if(!empty($set)) { // 登录时,直接设置 Cookie
// 强制生成一个随机数做为新 token,并和当前时间组合加密
$token = rand(1000, 9999);
$newEncode = sha1($token . $token_time);
// 更新会员表 token_time 字段
$dsql->ExecuteNoneQuery("UPDATE `#@__member` SET token='$token',token_time='$token_time' WHERE mid='{$this->M_ID}' ");
// 更新前端 Cookie 中的 DedeToken 值
if($this->M_KeepTime > 0) {
PutCookie('DedeToken',$newEncode,$this->M_KeepTime);
} else {
PutCookie('DedeToken',$newEncode);
}
$this->isToken = true;
$this->fields['token_time'] = $token_time;
return $this->isToken;
}
if(!empty($this->fields) && empty($this->fields['token'])) {
// 这个逻辑只会进入一次,也就当会员表字段token为空的时间,
// 理论上讲,只要会员登录一次,这个字段就永远不会为空,除非人为的修改
// 所以才说,这个逻辑只会进入一次。
$token = 'Genesis'; // 创世
$encode = sha1($token . $token_time); // 与时间组合加密
// 更新会员的 token 字段和与token组合的时间字段 token_time
$dsql->ExecuteNoneQuery("UPDATE `#@__member` SET token='$token',token_time='$token_time' WHERE mid='{$this->M_ID}' ");
// 织梦系统是否设置了Cookie过期时间,如果设置了的话,在生成cookie的时候要带上
if($this->M_KeepTime > 0) {
PutCookie('DedeToken',$encode,$this->M_KeepTime);
} else {
PutCookie('DedeToken',$encode);
}
$this->fields['token_time'] = $token_time;
$this->isToken = false;
} else { // 每次访问都会进入这个逻辑
// 获取前端保存的 token
$tokenCookie = GetCookie('DedeToken');
if(empty($tokenCookie)) {
$this->isToken = false;
} else {
// 组合 token 和 token_time,并加密
$encode = sha1($this->fields['token'] . $this->fields['token_time']);
// 与前端传递的加密token对比
$this->isToken = $tokenCookie === $encode;
if($this->isToken) { // 验证通过
// 保存 cookie 的 DedeToken 值到token,并更新token_time字段为当前时间戳
$dsql->ExecuteNoneQuery("UPDATE `#@__member` SET token='$tokenCookie',token_time='$token_time' WHERE mid='{$this->M_ID}' ");
// 生成新的准备下发到cookie DedeToken的加密值
$newEncode = sha1($tokenCookie . $token_time);
if($this->M_KeepTime > 0) {
PutCookie('DedeToken',$newEncode,$this->M_KeepTime);
} else {
PutCookie('DedeToken',$newEncode);
}
}
}
}
return $this->isToken;
}
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
* 单点登录更新token - 织梦先生 添加
* 注:在确保按原有逻辑登录成功后,使用此方法
* @param boolean $set 是否直接设置 token,登录时$set=true,非登录时不会设置此参数
* @return boolean
*/
functionresetToken($set=false){
global$dsql;
$token_time=time();
if(!empty($set)){// 登录时,直接设置 Cookie
// 强制生成一个随机数做为新 token,并和当前时间组合加密
$token=rand(1000,9999);
$newEncode=sha1($token.$token_time);
// 更新会员表 token_time 字段
$dsql->ExecuteNoneQuery("UPDATE `#@__member` SET token='$token',token_time='$token_time' WHERE mid='{$this->M_ID}' ");
// 更新前端 Cookie 中的 DedeToken 值
if($this->M_KeepTime>0){
PutCookie('DedeToken',$newEncode,$this->M_KeepTime);
}else{
PutCookie('DedeToken',$newEncode);
}
$this->isToken=true;
$this->fields['token_time']=$token_time;
return$this->isToken;
}
if(!empty($this->fields)&&empty($this->fields['token'])){
// 这个逻辑只会进入一次,也就当会员表字段token为空的时间,
// 理论上讲,只要会员登录一次,这个字段就永远不会为空,除非人为的修改
// 所以才说,这个逻辑只会进入一次。
$token='Genesis';// 创世
$encode=sha1($token.$token_time);// 与时间组合加密
// 更新会员的 token 字段和与token组合的时间字段 token_time
$dsql->ExecuteNoneQuery("UPDATE `#@__member` SET token='$token',token_time='$token_time' WHERE mid='{$this->M_ID}' ");
// 织梦系统是否设置了Cookie过期时间,如果设置了的话,在生成cookie的时候要带上
if($this->M_KeepTime>0){
PutCookie('DedeToken',$encode,$this->M_KeepTime);
}else{
PutCookie('DedeToken',$encode);
}
$this->fields['token_time']=$token_time;
$this->isToken=false;
}else{// 每次访问都会进入这个逻辑
// 获取前端保存的 token
$tokenCookie=GetCookie('DedeToken');
if(empty($tokenCookie)){
$this->isToken=false;
}else{
// 组合 token 和 token_time,并加密
$encode=sha1($this->fields['token'].$this->fields['token_time']);
// 与前端传递的加密token对比
$this->isToken=$tokenCookie===$encode;
if($this->isToken){// 验证通过
// 保存 cookie 的 DedeToken 值到token,并更新token_time字段为当前时间戳
$dsql->ExecuteNoneQuery("UPDATE `#@__member` SET token='$tokenCookie',token_time='$token_time' WHERE mid='{$this->M_ID}' ");
// 生成新的准备下发到cookie DedeToken的加密值
$newEncode=sha1($tokenCookie.$token_time);
if($this->M_KeepTime>0){
PutCookie('DedeToken',$newEncode,$this->M_KeepTime);
}else{
PutCookie('DedeToken',$newEncode);
}
}
}
}
return$this->isToken;
}
3、类中增加成员变量 var $isToken = false;
4、在构造方法内引用resetToken()方法,一定要放在构造方法内的最后引用。
为了尽可能是符合织梦系统的逻辑,所以选择把此验证方法加在如下位置。在开发过程中,无论怎么修改,都要尽可能的保持原信息的逻辑关系。
//php5构造函数
function __construct($kptime = -1, $cache=FALSE)
{
global $dsql;
......
if(empty($this->M_ID))
{
$this->ResetUser();
}else{
$this->M_ID = intval($this->M_ID);
......
if(is_array($this->fields)){
......
if( !$formcache )
{
SetCache($this->memberCache, $this->M_ID, $this->fields, 1800);
}
$this->resetToken(); // 加在这里
}else{
$this->ResetUser();
}
}
}
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//php5构造函数
function__construct($kptime=-1,$cache=FALSE)
{
global$dsql;
......
if(empty($this->M_ID))
{
$this->ResetUser();
}else{
$this->M_ID=intval($this->M_ID);
......
if(is_array($this->fields)){
......
if(!$formcache)
{
SetCache($this->memberCache,$this->M_ID,$this->fields,1800);
}
$this->resetToken();// 加在这里
}else{
$this->ResetUser();
}
}
}
5、在PutLoginInfo()方法中引用resetToken()方法,如:
function PutLoginInfo($uid, $logintime=0)
{
global $cfg_login_adds, $dsql;
$this->resetToken();
......
0
1
2
3
4
functionPutLoginInfo($uid,$logintime=0)
{
global$cfg_login_adds,$dsql;
$this->resetToken();
......
6、修改ResetUser()方法
/**
* 重置用户信息
*
* @return void
*/
function ResetUser()
{
$this->fields = '';
$this->M_ID = 0;
$this->M_LoginID = '';
$this->M_Rank = 0;
$this->M_Face = "";
$this->M_Money = 0;
$this->M_UserName = "";
$this->M_LoginTime = 0;
$this->M_MbType = '';
$this->M_Scores = 0;
$this->M_Spacesta = -2;
$this->M_UpTime = 0;
$this->M_ExpTime = 0;
$this->M_JoinTime = 0;
$this->M_HasDay = 0;
DropCookie('DedeUserID');
DropCookie('DedeLoginTime');
DropCookie('DedeToken'); // 添加这个
}
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 重置用户信息
*
* @return void
*/
functionResetUser()
{
$this->fields='';
$this->M_ID=0;
$this->M_LoginID='';
$this->M_Rank=0;
$this->M_Face="";
$this->M_Money=0;
$this->M_UserName="";
$this->M_LoginTime=0;
$this->M_MbType='';
$this->M_Scores=0;
$this->M_Spacesta=-2;
$this->M_UpTime=0;
$this->M_ExpTime=0;
$this->M_JoinTime=0;
$this->M_HasDay=0;
DropCookie('DedeUserID');
DropCookie('DedeLoginTime');
DropCookie('DedeToken');// 添加这个
}
7、/member/index_do.php中,在$cfg_ml->DelCache($cfg_ml->M_ID);后添加引用$cfg_ml->resetToken(true);如:
else
{
// 清除会员缓存
$cfg_ml->DelCache($cfg_ml->M_ID);
$cfg_ml->resetToken(true);
......
0
1
2
3
4
5
else
{
// 清除会员缓存
$cfg_ml->DelCache($cfg_ml->M_ID);
$cfg_ml->resetToken(true);
......
8、修改登录验证的方法IsLogin(),修改如下:
/**
* 验证用户是否已经登录
*
* @return bool
*/
function IsLogin()
{
if($this->M_ID > 0 && $this->isToken) return TRUE;
else return FALSE;
}
0
1
2
3
4
5
6
7
8
9
/**
* 验证用户是否已经登录
*
* @return bool
*/
functionIsLogin()
{
if($this->M_ID>0&&$this->isToken)returnTRUE;
elsereturnFALSE;
}
9、前端控制:后端创建接口,验证验证登录状态;前端轮询访问接口,编写自己的逻辑。后端控制:根据实际业务编写逻辑。
简单一点说这个逻辑,就是用户每次访问,都验证签名,验证通过后创建一个新签名,把签名生成条件保存入库,同时把签名写入Cookie,如此循环。。。