随着公司来访员工,防止被社工,网络安全性加强建设的推进,需要部署一套专门的网络供来客访问,并通过OA申请流程自动化处理。开发了目前这一套系统.

演示

后端服务验证身份 Radius

docker-compose.yaml 添加相关service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
freeradius:
image: "2stacks/freeradius"
ports:
- "1812:1812/udp"
- "1813:1813/udp"
volumes:
- /data/smartoa/radius/clients.conf:/etc/raddb/clients.conf:rw
- /data/smartoa/radius/sql:/etc/raddb/mods-available/sql:rw
environment:
- DB_NAME=radius
- DB_HOST=mysql
- DB_USER=root
- DB_PASS=gJH47HCe9CyMDZN
- DB_PORT=3306
- RADIUS_KEY=Qingmu@2023
- RAD_CLIENTS=10.0.0.0/24
- RAD_DEBUG=yes
depends_on:
- mysql
links:
- mysql
restart: always
networks:
- backend

client.conf 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
client localhost {
ipaddr = 127.0.0.1
proto = *
secret = secret
require_message_authenticator = no
shortname = localhost
limit {
max_connections = 16
lifetime = 0
idle_timeout = 30
}
}
client gzac {
ipaddr = 172.16.3.251/24
secret = secret
}
client guitianac {
ipaddr = 172.16.70.253/24
secret = secret
}

sql 配置文件

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
sql {
driver = "rlm_sql_mysql"
dialect = "mysql"
server = "mysql"
port = 3306
login = "root"
password = "gJH47HCe9CyMDZN"
radius_db = "radius"
acct_table1 = "radacct"
acct_table2 = "radacct"
postauth_table = "radpostauth"
authcheck_table = "radcheck"
groupcheck_table = "radgroupcheck"
authreply_table = "radreply"
groupreply_table = "radgroupreply"
usergroup_table = "radusergroup"
delete_stale_sessions = yes
pool {
start = ${thread[pool].start_servers}
min = ${thread[pool].min_spare_servers}
max = ${thread[pool].max_servers}
spare = ${thread[pool].max_spare_servers}
uses = 0
retry_delay = 30
lifetime = 0
idle_timeout = 60
}
client_table = "nas"
group_attribute = "SQL-Group"
$INCLUDE ${modconfdir}/${.:name}/main/${dialect}/queries.conf
}

更改完配置文件后,需要在mysql中导入db 表结构 /etc/raddb/mods-config/sql/main/mysql/schema.sql

AC 配置

取了重要部分 ,添加了Guest-test网络为游客访问的网络,设置了web验证模板portal页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version AC_RGOS 11.9(2)B2P15, Release(09182118)
hostname AC
!
wlan-config 4 Guest-test
ssid-code utf-8
!
http redirect direct-site 172.16.9.232
!
web-auth template eportalv2
ip 172.16.9.232
url http://172.16.9.232:8080/#/guest_wifi
fmt custom encry none user-ip userip user-mac usermac mac-format line user-vid uservlan nas-ip nasip url firsturl additional portaltype=custom
!
ip radius source-interface VLAN 3
radius-server host 172.16.9.232 key secret
!
redundancy
!
end

后端验证身份

  1. 发送账号密码与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
    352
    import 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")

  2. 后端url接口

    routers.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    from fastapi import APIRouter, Request, Body
    from app.schema.wifi import Wifi

    router = APIRouter()

    @router.post("/guest_wifi", name="访客登录网络获取临时账号密码")
    def guest_wifi(wifi: Wifi):
    response = query_guest_wifi_account(wifi)
    return response


    @router.post("/guest_wifi_offline", name="访客登录网络获取临时账号密码")
    def guest_wifi_offline(wifi: Wifi):
    response = query_guest_wifi_offline(wifi)
    return response

    @router.post("/guest_send_smscode", name="访客获取验证码", summary="访客获取验证码" )
    def send_smscode(mobile: str = Body(...),):
    response = guest_smscode(mobile)
    return response

    utils.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
    144
    import 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. 钉钉扫码

      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>