Radius+Vue+FastAPI搭建全套游客网络
随着公司来访员工,防止被社工,网络安全性加强建设的推进,需要部署一套专门的网络供来客访问,并通过OA申请流程自动化处理。开发了目前这一套系统.
演示
后端服务验证身份 Radius
docker-compose.yaml 添加相关service
1 | freeradius: |
client.conf 配置文件
1 | client localhost { |
sql 配置文件
1 | sql { |
更改完配置文件后,需要在mysql中导入db 表结构 /etc/raddb/mods-config/sql/main/mysql/schema.sql
AC 配置
取了重要部分 ,添加了Guest-test网络为游客访问的网络,设置了web验证模板portal页面
1 | version AC_RGOS 11.9(2)B2P15, Release(09182118) |
后端验证身份
发送账号密码与AC通信,验证身份 该系统最重点的地方
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352import random
import hashlib
import struct
import binascii
import IPy
import logging
import socket
logging.basicConfig(level=logging.INFO)
class PortalMessage:
#定义portal报文类型
REQ_CHALLENGE=1
ACK_CHALLENGE=2
REQ_AUTH=3
ACK_AUTH=4
REQ_LOGOUT=5
ACK_LOGOUT=6
AFF_ACK_AUTH=7
NTF_LOGOUT=8
REQ_INFO=9
ACK_INFO=10
#定义属性类型
ATTR_UserNAME=1
ATTR_PassWord=2
ATTR_Challenge=3
ATTR_ChapPassWord=4
ATTR_TextInfo=5
# ATTR_UplinkFlux=6
# ATTR_DownFlux=7
ATTR_Port=8
def __init__(self, secret, data=None):
self.attrList=[]
self.secret=secret
self.Version=2
if data :
self.decodePkt(data)
else:
self.Version=2
self.type=0
self.papChap =0
self.rsvd =0
self.SerialNo=0
self.ReqIdentifier=0
self.userIp=0
self.userPort =0
self.errCode=0
self.attrNum=0
self.attrList=[]
self.Authenticator=self.initAuthenticator()
def getSerialNo(self):
"""生成随机序列号"""
return random.randint(1000,65000)
def initAuthenticator(self):
"""初始化Authenticator为16个0"""
return bytearray(16)
def _getErrorInfo(self):
"""
协议定义如下:
第1和2字节为标志字节分别是十进制88和99
第3字节是报文类型:
1:认证请求
2:注销请求
3:操作成功
4:操作失败
第4字节为TVL数量
从第5字节开始到最后都是TLV字段
TVL的类型定义如下:
1:用户名
2:明文密码
3:用户IP
以上三个都是字符串
4:错误代码(参照getErrorInfo函数,内容有两个字节,第一个字节是pktType,第二个字节是ErrCode
"""
errInfo=u"未知错误:%d->%d" % (self.type,self.errCode)
error_mapping = {
f"{self.ACK_CHALLENGE}_0": "请求Challenge成功",
f"{self.ACK_CHALLENGE}_1": "请求Challenge被拒绝",
f"{self.ACK_CHALLENGE}_2": "链接已建立",
f"{self.ACK_CHALLENGE}_3": "有一个用户在认证过程中,请稍后重试",
f"{self.ACK_CHALLENGE}_4": "请求Challenge失败,发生错误",
f"{self.ACK_AUTH}_0": "认证成功",
f"{self.ACK_AUTH}_1": "认证请求被拒绝",
f"{self.ACK_AUTH}_2": "链接已建立",
f"{self.ACK_AUTH}_3": "有一个用户在认证过程中,请稍后重试",
f"{self.ACK_AUTH}_4": "认证请求失败,发生错误",
f"{self.REQ_LOGOUT}_0": "用户下线成功",
f"{self.REQ_LOGOUT}_1": "用户下线被拒绝",
f"{self.ACK_LOGOUT}_0": "用户下线成功",
f"{self.ACK_LOGOUT}_1": "用户下线被拒绝",
f"{self.ACK_LOGOUT}_2": "用户下线失败",
f"{self.ACK_INFO}_0": "处理成功",
f"{self.ACK_INFO}_1": "功能不支持",
f"{self.ACK_INFO}_2": "消息处理失败",
"0_0": "处理成功",
"100_0": "处理成功",
"100_1": "认证请求不完整",
"100_2": "消息类型没有定义",
}
return error_mapping.get(f"{self.type}_{self.errCode}", errInfo)
#根据ErrCode及报文类型返回具体的错误信息
def getErrInfo(self):
return self._getErrorInfo()
def createChapPassword(self, password, challenge, reqID):
""" 通过challenge计算chap_password"""
myreqID = reqID & 0xFF
mydata = bytearray([myreqID])
mydata += password.encode("utf-8") + challenge
chap_pass = hashlib.md5(mydata).digest()
logging.debug(f"chap_pass: {chap_pass}")
return chap_pass
def createPacket(self):
"""创建字节流,用于在网络上发送"""
buf = bytearray([
self.Version,
self.type,
self.papChap,
self.rsvd,
])
# 序列号,注意要用大端模式
buf.extend(struct.pack('!H', self.SerialNo))
# reqID,注意要用大端模式
buf.extend(struct.pack('!H', self.ReqIdentifier))
# 用户IP,注意要用大端模式
buf.extend(struct.pack('!L', self.userIp))
# 用户端口,注意要用大端模式
buf.extend(struct.pack('!H', self.userPort))
buf.extend([self.errCode,self.attrNum,])
buf += self.Authenticator
for AttrType, AttrLen, AttrStr in self.attrList:
buf.extend([AttrType,AttrLen + 2,])
if isinstance(AttrStr, str):
AttrStr = AttrStr.encode("utf-8")
buf += AttrStr
# print("createPacket===>","version:",self.Version, "type:", self.type, "pap/chap: ",self.papChap,
# "ip:", self.userIp, "code:", self.errCode, "attrnum:", self.attrNum, "attr:", self.attrList)
return buf
def createAuthenticator(self):
"""计算Authenticator"""
buf = self.createPacket()
buf += self.secret.encode("utf-8")
self.Authenticator = hashlib.md5(buf).digest()
logging.debug(f"authenticator: {self.Authenticator}")
return self.Authenticator
def createNewMsg(self,msgType,userIP,sn,reqID):
"""创建一个新的消息报文"""
self.type=msgType
self.userIp=userIP
self.SerialNo=sn
self.ReqIdentifier=reqID
def createChallenge(self,userIP):
"""创建挑战报文 """
sn = self.getSerialNo()
self.createNewMsg(self.REQ_CHALLENGE,userIP,sn,0)
self.createAuthenticator()
def createAuth(self,userIP,challengeID,sn,reqID,userName,userPass):
"""创建请求认证报文 """
#初始化AUTH请求报文
self.createNewMsg(self.REQ_AUTH,userIP,sn,reqID)
#先计算chap密码
chap_pass=self.createChapPassword(userPass,challengeID,reqID)
#增加chap密码属性
self.addAttr(self.ATTR_ChapPassWord, chap_pass)
#增加用户名属性
self.addAttr(self.ATTR_UserNAME, userName)
#计算Authenticator
self.createAuthenticator()
def createAFF(self,userIP,sn,reqID):
"""创建BAS认证成功后的回复报文AFF_ACK_AUTH"""
self.createNewMsg(self.AFF_ACK_AUTH,userIP,sn,reqID)
self.createAuthenticator()
def createLogout(self,userIP):
"""创建请求下线报文,注意文档上要求提供上线时的reqID,但实际上好像用0即可"""
# sn=self.getSerialNo()
self.createNewMsg(self.REQ_LOGOUT,userIP,0,0)
self.createAuthenticator()
def addAttr(self,mytype,data):
"""报文增加属性"""
self.attrList.append((mytype,len(data),data))
self.attrNum=self.attrNum+1
def getAttr(self, mytype):
"""获取属性"""
for attrType, attrLen, attrData in self.attrList:
if attrType==mytype:
return attrData
return None
def decodePkt(self, data):
"""解码报文"""
(
self.Version,
self.type,
self.papChap,
self.rsvd,
self.SerialNo,
self.ReqIdentifier,
self.userIp,
self.userPort,
self.errCode,
self.attrNum,
) = struct.unpack('!BBBBHHLHBB', data[:16])
self.Authenticator = data[16:32]
self.decodeAttr(data[32:])
# print("decodePkt===>", "version:",self.Version, "type:", self.type, "pap/chap: ",self.papChap,
# "ip:", self.userIp, "code:", self.errCode, "attrnum:", self.attrNum,"data:", data[32:])
def decodeTLV(self, attNum, data):
"""解码TLV数据"""
data = bytearray(data)
attrList = []
while len(data) >= 3 and attNum > 0:
attType, attLen = data[0], data[1] - 2
if len(data) < attLen + 2:
break
attData = data[2:2 + attLen]
attrList.append((attType, attLen, attData))
data = data[2 + attLen:]
attNum -= 1
return attrList
def decodeAttr(self, data):
"""解码属性"""
self.attrList=self.decodeTLV(self.attrNum, data)
def guest_login_ac(username, password, userip, acserver="172.16.3.251", secret="portal"):
try:
userIP = IPy.IP(userip).int()
authserver = (acserver, 2000)
socket.setdefaulttimeout(30)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
logging.info("第一步 请求获取challenge")
msg=PortalMessage(secret)
msg.createChallenge(userIP)
request_packet_v1 = msg.createPacket()
sock.sendto(bytes(request_packet_v1), authserver)
logging.info("第二步 解析响应报文Challenge")
response_packet_v2, addr = sock.recvfrom(1024)
# logging.info(f"response_packet_v2: {response_packet_v2}", )
msg_v2 = PortalMessage(secret,response_packet_v2)
logging.info(f"返回结果: {msg_v2.getErrInfo()}")
if msg_v2.errCode not in [0,2]:
return {"code": msg_v2.errCode, "msg": msg_v2.getErrInfo()}
if msg_v2.errCode == 2:
return {"code": 0, "msg": msg_v2.getErrInfo()}
challengeID = msg_v2.getAttr(PortalMessage.ATTR_Challenge)
reqID = msg_v2.ReqIdentifier
sn = msg_v2.SerialNo
logging.info("第三步 发送认证请求报文")
msg_auth=PortalMessage(secret)
msg_auth.createAuth(userIP, challengeID, sn, reqID, username, password)
request_packet_v3 = msg_auth.createPacket()
sock.sendto(bytes(request_packet_v3), authserver)
logging.info("第四步 验证报文")
response_packet_v4, addr = sock.recvfrom(1024)
# logging.info(f"response_packet_v4: {response_packet_v4}", )
msg_v4 = PortalMessage(secret,response_packet_v4)
reqID = msg_v4.ReqIdentifier
sn = msg_v4.SerialNo
logging.info(f"返回结果: {msg_v4.getErrInfo()}")
if msg_v4.errCode != 0:
return {"code": msg_v4.errCode, "msg": msg_v4.getErrInfo()}
logging.info("第五步发送响应认证成功报文AFF_ACK_AUTH")
msg_aff=PortalMessage(secret)
msg_aff.createAFF(userIP, sn, reqID)
request_packet_v5 = msg_aff.createPacket()
sock.sendto(bytes(request_packet_v5), authserver)
except socket.timeout:
logging.info("网络状态不稳定!请稍后重试!")
return {"code": 400502, "msg": "网络状态不稳定!请稍后重试!"}
except TypeError as ex:
logging.info(f"报文解析出错: {ex}")
return {"code": 400500, "msg": "报文接续报错!"}
except Exception as ex:
return {"code": 400501, "msg": "内部错误!" }
return {"code": 0, "msg": "登录成功!", "sn": sn }
def guest_logout_ac(userip, acserver="172.16.3.251", secret="portal"):
userIP = IPy.IP(userip).int()
authserver = (acserver, 2000)
socket.setdefaulttimeout(5)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
logging.info(u'发送下线报文')
msg_logout_req=PortalMessage(secret)
msg_logout_req.createLogout(userIP)
response_packet_v6 = msg_logout_req.createPacket()
sock.sendto(bytes(response_packet_v6), authserver)
response_packet_v7, _ = sock.recvfrom(1024)
# logging.info(f"response_packet_v2: {response_packet_v7}")
msg_v7 = PortalMessage(response_packet_v7)
return {"code": msg_v7.errCode, "msg": {msg_v7.getErrInfo()}}
except socket.timeout:
logging.info("网络状态不稳定!请稍后重试!")
return {"code": 400500, "msg": "下线请求超时!请稍后重试!"}
except TypeError as ex:
logging.info(f"报文解析出错: {ex}")
return {"code": 400500, "msg": "下线请求数据错误!请稍后重试!"}
if __name__ == '__main__':
guest_login_ac(username="guest001", password="kjK8eMwKZCWL4CbM3cYXEHKW", userip='172.16.27.156')
guest_logout_ac(userip='172.16.27.156', acserver="172.16.3.251")后端url接口
routers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20from fastapi import APIRouter, Request, Body
from app.schema.wifi import Wifi
router = APIRouter()
def guest_wifi(wifi: Wifi):
response = query_guest_wifi_account(wifi)
return response
def guest_wifi_offline(wifi: Wifi):
response = query_guest_wifi_offline(wifi)
return response
def send_smscode(mobile: str = Body(...),):
response = guest_smscode(mobile)
return responseutils.py
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144import logging
import random
import base64
import hmac
import hashlib
import requests
import time
import json
from datetime import datetime
from app import crud
from app.model.wifi import WifiLog
from app.lib.directory.portal import guest_login_ac, guest_logout_ac
from app.lib.directory.utils import dir_dingtalk, dingtalk_tools, get_login_token
from app.schema.wifi import WifiUpdate, WifiCreate, WifiBase
from app.schema.smscode import SmsCodeCreate
from app.db.session import db_session,radius_db_session
from app.core.config import settings
from app.utils.desc import pycrypt
from app.utils.send_smscode import send_sms
from app.errors import respon
logger = logging.getLogger(__name__)
def guest_smscode(mobile: str):
"""获取短信验证码"""
mobile_valid = guestwifi_valid_usernames()
if not mobile_valid or mobile not in mobile_valid:
return respon.resp_error(respon.USER_MOBILE_NOT_VALID)
code_obj = crud.smscode.get_smscode_five_minute_ago(db_session=db_session, username="guest", mobile=mobile)
if code_obj:
return respon.resp_error(respon.CAPTCHA_CODE_BUSY_ERROR)
smscode = random.randint(100000, 999999)
result = send_sms(mobile.lstrip("+86-"), '{"code": %s }' % smscode)
result_code = json.loads(result)["Code"]
if result_code == 'OK':
crud.smscode.create(
db_session=db_session,
obj_in=SmsCodeCreate(
username="guest",
mobile=mobile,
smscode=smscode
)
)
return respon.resp_success(msg="发送验证码成功!请注意查收!")
return respon.resp_error(respon.SYSTEM_UNKNOW_ERROR)
def get_dingtalk_userinfo_qrcode(code):
"""钉钉扫码的token获取身份"""
app_key, corp_id, app_secret, _ = dir_dingtalk.config.values()
timestamp = int(round(time.time() * 1000))
signature = hmac.new(app_secret.encode('utf-8'), str(timestamp).encode('utf-8'), hashlib.sha256).digest()
signature = base64.b64encode(signature).decode('utf-8')
params = {'accessKey': app_key, 'timestamp': timestamp , 'signature': signature}
userinfo = requests.post("https://oapi.dingtalk.com/sns/getuserinfo_bycode",
params=params, data=json.dumps({"tmp_auth_code": code})
).json()
return userinfo
def get_dingtalk_token_by_qrcode(code):
"""扫码时获取token"""
qrcode_user = get_dingtalk_userinfo_qrcode(code)
nickname = qrcode_user['user_info']['nick']
userinfo = dingtalk_tools.client.user.get_userid_by_unionid(qrcode_user['user_info']["unionid"])
data = get_login_token(nickname, userinfo['userid'])
return data
def guestwifi_valid_usernames():
"""raius导入的数据库里是否存在该用户"""
objects = crud.radcheck.get_all(radius_db_session)
return [i.username for i in objects if "guest" not in i.username]
def query_guest_wifi_account(wifi: WifiBase):
"""AC登录"""
ac_response = {"code": 404, "errmsg": "失败"}
valid_usernames = guestwifi_valid_usernames()
if all([wifi.mobile, wifi.smscode]) and not wifi.dingcode:
"""短信登录"""
if wifi.mobile not in valid_usernames:
return respon.resp_error(respon.USER_MOBILE_NOT_VALID)
smscode_obj = crud.smscode.get_smscode_five_minute_ago(db_session=db_session, username="guest", mobile=wifi.mobile)
if not smscode_obj:
return respon.resp_error(respon.CAPTCHA_CODE_NOT_VALID)
if smscode_obj.smscode != int(wifi.smscode):
return respon.resp_error(respon.CAPTCHA_CODE_ERROR)
ac_response = guest_login_ac(wifi.mobile, settings.GUEST_WIFI_PASSWORD, wifi.userip, acserver=wifi.nasip)
ac_response["data"] = wifi.mobile
if wifi.dingcode:
"""钉钉扫码登录"""
qrcode_user = get_dingtalk_userinfo_qrcode(wifi.dingcode)
if qrcode_user["errcode"] != 0:
return respon.resp_error(respon.DINGTALK_CODE_ERROR)
logger.info(qrcode_user['user_info']['nick'])
res = dingtalk_tools.client.user.get_userid_by_unionid(qrcode_user['user_info']["unionid"])
if res["errcode"] != 0:
return respon.resp_error(respon.NOT_GRANT_PRIVILEGE)
userinfo = dingtalk_tools.client.user.get(res["userid"])
if userinfo['email'] not in valid_usernames:
crud.radcheck.create(radius_db_session,obj_in = crud.crud_radius.RadcheckCreate(
username=userinfo['email'],
value=settings.GUEST_WIFI_PASSWORD))
logger.info(f"{userinfo['email']}, {wifi.usermac}, {wifi.userip}, {wifi.nasip}")
ac_response = guest_login_ac(userinfo['email'], settings.GUEST_WIFI_PASSWORD, wifi.userip, acserver=wifi.nasip)
ac_response["data"] = qrcode_user['user_info']['nick']
if ac_response["code"] == 0:
crud.crud_wifi.create(db_session=db_session, obj_in=WifiCreate(
userip = wifi.userip,
nasip = wifi.nasip,
usermac = wifi.usermac,
mobile = ac_response["data"],
smscode = wifi.smscode,
dingcode = wifi.dingcode
))
return respon.resp_success(ac_response["data"])
return respon.resp_error(respon.ErrorBase(code=str(ac_response["code"]),msg=ac_response["errmsg"]))
def query_guest_wifi_offline(wifi: WifiBase):
"""踢下线操作"""
ac_response = guest_logout_ac(userip=wifi.userip,acserver=wifi.nasip)
obj = crud.crud_wifi.get(db_session=db_session, queries=[WifiLog.userip == wifi.userip, WifiLog.nasip == wifi.nasip])
if ac_response["code"] == 0:
crud.crud_wifi.update(db_session=db_session, db_obj=obj, obj_in=WifiUpdate(
userip = wifi.userip,
nasip = wifi.nasip,
usermac = wifi.usermac,
offline_time = datetime.now()
))
return respon.resp_success()
return respon.resp_error(respon.ErrorBase(code=str(ac_response["code"]), msg=ac_response["errmsg"]))前端提供portal页面
钉钉扫码
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430<template>
<body id="poster">
<div v-if="loginType!=='ding' && loginType !=='loading'" class="login-container">
<el-form ref="postForm" :model="postForm" class="login-form">
<div v-show="loginType==='sms'">
<h3 class="login_title">短信登录</h3>
<el-alert
title="手机报备:"
description="钉钉--控制台--OA--流程中心--IT系统维护--访客(Guest)网络权限申请"
type="warning"
:closable="false"
/>
<br>
<el-form-item prop="mobile" required>
<el-input ref="mobile" v-model="postForm.mobile" placeholder="手机号" name="mobile" type="text" />
</el-form-item>
<el-form-item prop="smscode">
<el-input ref="smscode" v-model.number="postForm.smscode" placeholder="验证码" name="smscode" type="text" />
<el-button style="padding-right:10px" type="text" :disabled="!show" @click="onSendCode">
<span v-show="show">获取验证码</span>
<span v-show="!show">{{ count }} s</span>
</el-button>
</el-form-item>
<el-form-item style="width: 100%">
<el-button type="primary" style="width: 100%;border: none" :disabled="clicked" @click="onSubmit">登录</el-button>
</el-form-item>
<el-form-item style="width: 100%">
<el-button type="info" style="width: 100%;border: none" :disabled="clicked" @click="reset">返回主界面</el-button>
</el-form-item>
</div>
<div v-show="!loginType && !validLogin">
<h3 class="login_title">访客登录</h3>
<el-form-item style="width: 100%">
<el-button type="success" style="width: 100%;background: #41cb6d;border: none; font-size: 20px;" :disabled="clicked" @click="ddLogin">员工钉钉扫码</el-button>
</el-form-item>
<el-form-item style="width: 100%">
<el-link :underline="false" :disabled="clicked" style="width: 100%;border: none;font-size: 12px " @click="smsLogin">手机验证码登录(OA提前审批)</el-link>
</el-form-item>
</div>
<div v-if="validLogin">
<h3 class="login_title">成功登录</h3>
<el-form-item prop="mobile">
<el-input v-model="postForm.mobile" prefix-icon="el-icon-user-solid" disabled type="text" />
</el-form-item>
<el-form-item style="width: 100%">
<el-button type="danger" style="width: 100%;border: none" @click="offline">下线</el-button>
</el-form-item>
</div>
</el-form>
</div>
<div v-show="loginType==='ding'" class="login-container">
<h3 class="login_title">钉钉登录</h3>
<el-alert
title="员工钉钉扫码"
type="warning"
:closable="false"
center
/>
<div id="login_container" />
<el-button type="info" style="width: 100%;border: none" :disabled="clicked" @click="reset">返回主界面</el-button>
</div>
<div v-if="loginType==='loading'" class="login-container">
<h3 class="login_title">正在登录</h3>
<span class="login_title">正在验证身份...</span>
</div>
</body>
</template>
<script>
import { guest_wifi, guest_wifi_offline, send_guest_code } from '@/api/wifi'
export default {
data() {
return {
postForm: {
smscode: undefined,
mobile: undefined
},
clicked: false,
show: true,
timer: 0,
count: 0,
error_msg: undefined,
ok_msg: undefined,
loginType: undefined,
validLogin: false,
redirect: undefined,
appid: process.env.VUE_APP_ID,
portalUrl: `${process.env.VUE_APP_WEB_URL}/#/guest_wifi`, //portal页面
// 钉钉生成的二维码配置
dingCodeConfig: {
id: 'login_container',
style: 'border:none;background-color:rgba(0,0,0,0); margin:0 auto; padding: 0 40px 0 0;',
width: '350',
height: '400'
}
}
},
computed: {
getRedirectUrl() {
return encodeURIComponent(this.redirectUrl)
},
getAuthUrl() {
return `https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=${this.appid}&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=${this.getRedirectUrl}`
},
getGoto() {
return encodeURIComponent(this.getAuthUrl)
},
getDingCodeConfig() {
return { ...this.dingCodeConfig, goto: this.getGoto }
}
},
// watch: {
// $route: {
// handler: function(route) {
// // this.redirect = route.query && route.query.redirect
// this.dingLogin = route.query && route.query.dingLogin
// this.validLogin = route.query && route.query.validLogin
// console.log('dingLogin: ', this.dingLogin, 'validLogin: ', this.validLogin)
// },
// immediate: true
// }
// },
created() {
this.initDingJs()
},
mounted() {
this.userip = this.$route.query.userip
this.nasip = this.$route.query.nasip
this.usermac = this.$route.query.usermac
this.redirectUrl = `${this.portalUrl}?userip=${this.userip}&usermac=${this.usermac}&nasip=${this.nasip}`
if (this.nasip) {
if (this.nasip.indexOf('?code') !== -1) {
this.authorize_wifi()
}
}
},
methods: {
sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time))
},
// 手机验证码登录相关
onSubmit() {
this.$refs.postForm.validate(valid => {
if (valid) {
if (!this.postForm.smscode) {
this.$message.warning('请输入验证码...')
return
}
//虽然这里是手机短信验证码登录,但是因为钉钉扫码会追加?code=xxx,所以需要把这部分去掉,否则钉钉扫码不通过切换到短信验证码登录,就没办法取到正确的nasip, 其实就是AC的ip
const [nasip] = this.$route.query.nasip.split('?code=')
const data = Object.assign({}, this.postForm, {
'userip': this.$route.query.userip,
'usermac': this.$route.query.usermac,
'nasip': nasip
})
this.clicked = true
this.loginType = 'loading'
guest_wifi(data).then((response) => {
const { code, msg } = response
this.clicked = false
if (code === 0) {
this.sleep(5000).then(() => {
this.loginType = undefined
this.validLogin = true
this.$notify({
title: '登录成功',
message: msg,
type: 'success',
duration: 5000
})
})
} else {
this.loginType = undefined
this.validLogin = false
this.$notify({
title: '失败',
message: msg,
type: 'error',
duration: 5000
})
}
})
}
})
},
//下线
offline() {
this.$refs.postForm.validate(valid => {
if (valid) {
const [nasip] = this.$route.query.nasip.split('?code=')
// const nasip = this.queryNasip()
const data = {
'userip': this.$route.query.userip,
'nasip': nasip,
'usermac': this.$route.query.usermac
}
this.clicked = true
guest_wifi_offline(data).then((response) => {
const { code, msg } = response
this.clicked = false
if (code === 0) {
this.validLogin = false
this.postForm.mobile = undefined
} else {
this.validLogin = true
this.$notify({
title: '断开网络失败',
message: msg,
type: 'error',
duration: 5000
})
}
})
}
})
},
// 手机验证码登录相关
onSendCode() {
this.$refs.postForm.validate(valid => {
if (valid) {
send_guest_code(this.postForm.mobile).then((response) => {
const { code, msg } = response
if (code === 0) {
this.$notify({
title: '成功',
message: msg,
type: 'success',
duration: 5000
})
if (!this.timer) {
this.count = 300
this.show = false
this.timer = setInterval(() => {
if (this.count > 0 && this.count <= 300) {
this.count--
} else {
this.show = true
clearInterval(this.timer)
this.timer = null
}
}, 1000)
}
} else {
this.$notify({
title: '失败',
message: msg,
type: 'error',
duration: 5000
})
}
})
}
})
},
// 初始化钉钉扫码element
initDingJs() {
!(function(window, document) {
function d(a) {
var e; var c = document.createElement('iframe')
var d = 'https://login.dingtalk.com/login/qrcode.htm?goto=' + a.goto
// eslint-disable-next-line no-sequences
d += a.style ? '&style=' + encodeURIComponent(a.style) : '',
d += a.href ? '&href=' + a.href : '',
c.src = d,
c.frameBorder = '0',
c.allowTransparency = 'true',
c.scrolling = 'no',
c.width = a.width ? a.width + 'px' : '365px',
c.height = a.height ? a.height + 'px' : '400px',
e = document.getElementById(a.id),
e.innerHTML = '',
e.appendChild(c)
}
window.DDLogin = d
}(window, document))
},
// 轮询检测是否有扫码
addDingListener() {
const self = this
const handleLoginTmpCode = function(loginTmpCode) {
window.location.href = self.getAuthUrl + `&loginTmpCode=${loginTmpCode}`
}
const handleMessage = function(event) {
if (event.origin === 'https://login.dingtalk.com') {
handleLoginTmpCode(event.data)
}
}
if (typeof window.addEventListener !== 'undefined') {
window.addEventListener('message', handleMessage, false)
} else if (typeof window.attachEvent !== 'undefined') {
window.attachEvent('onmessage', handleMessage)
}
},
initDingLogin() {
window.DDLogin(this.getDingCodeConfig)
},
queryString() {
const url = window.location.href
const codeIndex = url.indexOf('&state=') // Find the index of 'code=' in the URL
if (codeIndex !== -1) { // Check if 'code=' was found
const codeStart = codeIndex - 32
return url.substring(codeStart, codeIndex !== -1 ? codeIndex : undefined) // Extract the code substring
}
return null
},
authorize_wifi() {
this.loginType = 'loading'
const [nasip] = this.$route.query.nasip.split('?code=')
const code = this.queryString()
// const start = window.location.href.indexOf('&state=')
// const end = start - 32
// const code = window.location.href.substring(start, end)
if (code) {
const data = {
'userip': this.$route.query.userip,
'usermac': this.$route.query.usermac,
'nasip': nasip,
'dingcode': code
}
guest_wifi(data).then((response) => {
const { code, msg, data } = response
if (code === 0) {
this.sleep(5000).then(() => {
this.loginType = undefined
this.validLogin = true
this.postForm.mobile = data
})
} else {
this.validLogin = false
this.loginType = undefined
this.$notify({
title: '失败',
message: msg,
type: 'error',
duration: 5000
})
}
}).catch((res) => {
this.validLogin = false
this.loginType = undefined
this.$notify({
title: '失败',
message: '内部错误,请联系系统管理员',
type: 'error',
duration: 5000
})
})
}
},
ddLogin() {
this.loginType = 'ding'
this.addDingListener()
this.initDingLogin()
// this.authorize_wifi()
},
smsLogin() {
this.loginType = 'sms'
},
reset() {
this.loginType = undefined
this.validLogin = false
}
}
}
</script>
<style lang="scss" scoped>
#poster {
background: url(~@/assets/login_images/wifi.jpeg) center;
height: 100%;
width: 100%;
background-size: cover;
position: fixed;
}
body{
margin: 0;
padding: 0;
}
.login-container {
border-radius: 15px;
background-clip: padding-box;
margin: 150px auto;
width: 350px;
background: #fff;
padding: 15px 15px 15px 15px;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #ffffff;
}
.login-container .el-input input {
background: transparent;
-webkit-appearance: none;
border-radius: 15px;
padding: 15px 15px 15px 30px !important;
color: #289bcc;
height: 47px;
caret-color: #ffffff !important;
}
.login-container .login-content {
border-radius: 15px;
background-clip: padding-box;
margin: 150px auto;
padding: 15px 15px 15px 15px;
text-align: center;
background: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #ffffff;
}
.login_title {
margin: 0 auto 40px auto;
text-align: center;
color: #505458;
}
</style>
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 河边小菜!

