简易扫码登录

起因

因为老师强制要求扫码登录。应室友请求,特此水文。

思路

电脑登录时,会生成一个全局唯一的id。并且把id信息存到二维码中。服务器也会把idjavax.websocket.Session存到map里。
当手机扫码时会跳转到一个特殊的网页。该网页自动把id和用户信息一并发到服务器。服务器根据id找到javax.websocket.Session。通过javax.websocket.Session向电脑端的浏览器发送用户信息并放行。

当然这里也可以不用websocketwebsocket的好处是双工通信。服务器可以向浏览器发送请求。否则只能用js轮询服务器了。

代码

后端

UUIDWebSocket是自己抽象了个接口,便于随时更换换实现。
@OnMessage 就是发送消息时调用的方法
@OnClose 是关闭连接时调用的方法


不感兴趣可以直接跳过。

这里用了一个轮换map来实现二维码失效。为啥不直接加个日期字段呢?加个日期字段你每个都要加个定时器。听上去性能就很差。你可能会说为扫码时再判断是否过期呢?这同样不行。如果有人光登录不扫码,会浪费内存。

这里的轮换map,只要一个定时器,兼具性能和体积。唯一的缺点就是有效期是个区间,不固定。

轮换map内部有两个map。插入时只会插入newMap。删除时只会删除oldMap,然后oldMapnewMap交换。

@Component
@ServerEndpoint(QR_SOCKET)
public class QRCodeWebSocket implements UUIDWebSocket {
    
    
    public QRCodeWebSocket() {
    
    
        new Timer().scheduleAtFixedRate(new TimerTask() {
    
    
            @Override
            public void run() {
    
    
                uuid2session.clear();
                session2uuid.clear();
            }
            // 二维码有效期5到10分钟
        }, 0, 300_000);
    }

    // 轮换map
    private static class RotationMap<T, R> {
    
    
        private Map<T, R> newMap = new ConcurrentHashMap<>(), oldMap = new ConcurrentHashMap<>();

        R get(T t) {
    
    
            return newMap.containsKey(t) ? newMap.get(t) : oldMap.get(t);
        }

        void put(T t, R r) {
    
    
            newMap.put(t, r);
        }

        void clear() {
    
    
            oldMap.clear();
            Map map = oldMap;
            oldMap = newMap;
            newMap = map;
        }

        void remove(T t) {
    
    
            if (newMap.containsKey(t)) {
    
    
                newMap.remove(t);
            } else
                oldMap.remove(t);
        }

    }

    final static RotationMap<String, Session> uuid2session = new RotationMap<>();
    final static RotationMap<Session, String> session2uuid = new RotationMap<>();

    @OnMessage
    @Override
    public void onMessage(String uuid, Session session) {
    
    
        uuid2session.put(uuid, session);
        session2uuid.put(session, uuid);
    }

    @Override
    public boolean callback(String uuid, String data) {
    
    
        Session session = uuid2session.get(uuid);
        if (session != null) {
    
    
            try {
    
    
                session.getBasicRemote().sendText(data);
                session.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
                return false;
            }
        }
        return true;
    }

    @OnClose
    @Override
    public void onClose(Session session) {
    
    
        String uuid = session2uuid.get(session);
        if (uuid != null) {
    
    
            uuid2session.remove(uuid);
            session2uuid.remove(session);
        }
    }
}

另外要配置一个bean

@Configuration
public class WebSocketConfig {
    
    

    /**
     * 注入一个ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint
     * 会导致测试不通过。应该是测试环境没配全的原因
     * 打包时直接跳过测试
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
    
    
        return new ServerEndpointExporter();
    }

}

扫码接口

@Controller
public class QRCodeController {
    
    
    ThemeConfig themeConfig;
    UUIDWebSocket QRWebSocket;

    public QRCodeController(ThemeConfig themeConfig, UUIDWebSocket QRWebSocket) {
    
    
        this.themeConfig = themeConfig;
        this.QRWebSocket = QRWebSocket;
    }

    // 回调
    @GetMapping(QR_LOGIN + "/{uuid}")
    String viewQRCode(@PathVariable("uuid") String uuid, HttpSession session, Model model) {
    
    
        User user = (User) session.getAttribute(USR);
        if (user != null) {
    
    
            QRWebSocket.callback(uuid, objectToString(user));
            return themeConfig.render(USER_INDEX);
        } else {
    
    
            user=new User();
            user.setId(-1);
            QRWebSocket.callback(uuid, objectToString(user));
            model.addAttribute(MSG, "请先登录");
            return themeConfig.render(LOGIN);
        }
    }

}

手机端扫码时会进入viewQRCode函数。
先通过 session.getAttribute(USR);获取手机端的用户消息。
如果没有就叫手机端登录。并给电脑端返回一个id为负无效的user;
如果有就通过 QRWebSocket.callback返回用户信息user

前端代码

这里的[[${QR_SOCKET}]]等是thymeleaf模板里的,不用深究。
layer 是库里的,不用管
chainjson2form是自己写的工具函数也不用管。
下面代码的意思就是建立个WebSocket通信。
当返回的用户id-1时报错。
成功时访问一下登录接口,并把用户信息发过去
登录成功就跳转到用户主页

// 向后端发送一个websocket连接请求
let ws = new WebSocket('ws://' + root_path + '[[${QR_SOCKET}]]');
ws.onmessage = function (event) {
    
    
    let data = JSON.parse(event.data);
    console.dir(data)
    if (data.id != -1) {
    
    
        chain({
    
    
            url: '[[${LOGIN}]]',
            method: 'post',
            data: json2form(data)
        }).then(
            () => {
    
    
                window.location.href = "[[${USER_INDEX}]]"
            }
        );
    } else {
    
    
        alert('扫码失败');
    }
}

function connect() {
    
    
    ws.send('[[${uuid}]]');
}

function qr_login() {
    
    
    layer.open({
    
    
        type: 1
        , area: ['300px', '300px']
        , title: '扫码登录'
        , anim: 1
        , content: '<center><img style="width: 100%" src="//api.pwmqr.com/qrcode/create/?url=http://' + root_path +
            '[[@{|${QR_LOGIN}/${uuid}|}]]"></center>'
    });
    connect();
}

工具包

const root_path = "[[${ #httpServletRequest.getServerName() + ':' + #request.getServerPort()  + #request.getContextPath() } ]]"

function json2form(json) {
    
    
    let ks = Object.keys(json), i = 0;
    ks.forEach((ele, i, arr) => arr[i] += "=" + json[arr[i]]);
    return ks.join("&");
}

function chain(option) {
    
    
    return new Promise((resolve, reject) => {
    
    
        axios(option).then(r => resolve(r.data)).catch(e => reject(e));
    });
}

猜你喜欢

转载自blog.csdn.net/qq_45256489/article/details/121106487
今日推荐