step 1: 网页端提供二维码
step 2: 手机端登录,并保存token,手机扫码后向网页端发送token
step 3: 网页端通过jstoken解析token,向后端服务器获取用户信息
step 4: 最总实现扫码登录
即:需要被扫码客户端向服务端请求创建websocket(后称ws),服务端返回房间号也就是id,然后让可以扫码的设备登录,获取token(登录靠token实现,不用账号和密码,只有在生成token时才需要这些隐私信息),再通过创建的房间号加入ws,手动授权后向加入的房间发送token,最终实现扫码登录
搭建服务端websocket.js
const WebSocket = require('ws')
const wss = new WebSocket.Server({
port: 3001
})
var id = 1 // ws 链接的最开始id
wss.on('connection', function connection (ws){
// 服务器广播
ws.on('message', function message(data) {
// 处理消息
try{
data = JSON.parse(data)
}catch(e){
console.log('拦截到一条消息')
return
}
// 是连接状态
if (ws.readyState === WebSocket.OPEN) {
switch(data.event){ // 判断消息事件
case 'heartbeat': // 心跳检测
if(data.msg === 'ping'){ // 客户端检测
ws.send(JSON.stringify({
event: 'heartbeat',
msg: 'pong'
}))
}
else if(data.msg === 'pong'){ // 服务端检测
ws.isAlive = true
}
return; // 事件处理完成
case 'succeedLogin':
console.log('收到登录成功请求')
wss.clients.forEach((client) => {
// 给客户端发送登录成功提示!
if(client.id + ''=== data.id + ''){
client.send(JSON.stringify({
event: 'succeedLogin',
id: data.id
}))
console.log('服务器将消息发给了' + client.id)
}
})
return; // 事件处理完成
}
}
console.log("服务器收到消息:")
console.log(data)
wss.clients.forEach((client) => {
// 是连接状态
if(client.readyState === WebSocket.OPEN) {
// 发给自己
if(ws === client){
switch(data.event){
// 创建房间
case 'setUp':
ws.send(JSON.stringify({
event: 'setUp',
id: id
}))
ws.isAlive = true
ws.id = id + ''
ws.type = 'serve'
console.log('已经创建ID为: '+id+' 链接WS')
id++
return
}
}
// 不发给自己,发给指定id
else if(ws !== client && client.id ===data.id){
switch (data.event){
case 'login':
console.log('开始发送登陆信息')
client.send(JSON.stringify({
event: 'login',
token: data.token
}))
console.log('服务器将消息发给了' + client.id)
return
case 'scanfed': // 用户扫码登录入口
ws.type = 'client' // 设置扫描端为客户端
ws.id = data.id + ''
ws.isAlive = true
console.log('开始发送扫描事件信息')
client.send(JSON.stringify({
event: 'scanfed',
id: data.id
}))
console.log('服务器将消息发给了' + client.id)
return
}
}
}
})
console.log('服务器发送此消息成功!')
});
ws.on('close', function() {
console.log(ws.id + ' exit')
console.log('现在还有一下客户端:')
wss.clients.forEach((client) => {
console.log('id: ' + client.id + 'type: ' + client.type)
})
})
})
const timeIntervl = 3000 // 发送心跳检测的时间
setInterval(() => {
wss.clients.forEach((ws) =>{
if(!ws.isAlive){
console.log(ws.id + '无反应,已结束了与它的链接')
return ws.terminate()
}
ws.isAlive = false // 不收到pong就会断开与客户端的链接
// console.log("向ws: "+ws.id+"发送心跳")
ws.send(JSON.stringify({
event: 'heartbeat',
msg: 'ping'
}))
})
}, timeIntervl);
export default wss
Router.js
import Router from 'koa-router'
const qr = require('qr-image');
class PublicController {
constructor() { }
async getQR(ctx){
var text = ctx.query.text;
var img = qr.image(text, {size: 100})
try {
ctx.type= 'image/png';
ctx.body = img;
} catch (e) {
ctx.type='text/html;charset=utf-8';
ctx.body='Text Too Large';
}
}
}
const router = new Router()
router.get('/getQR', PublicController.getQR)
export default router
index.js
import koa from 'koa'
import JWT from 'koa-jwt'
import statics from 'koa-static'
const app = new koa()
// 定义公共路径,不需要jwt鉴权
const jwt = JWT({
secret: config.JWT_SECRET,//相当于解析token需要的密码
}).unless({
path: [
//getQR/
]})
const middleware = compose([
statics(path.join(__dirname, '../public')), // 静态文件开始路径
jwt //
])
app.use(middleware)
app.use(router())
app.listen(3000)
需要登录端
我使用的是iview-admin 2.0 的框架,我就只写我修改的部分,大家可以按照这个思路自己编写一个页面.因为我自己写的页面太丑了,就不展示了展示别人优秀的页面
LoginScanfQRForm.vue
.qr {
padding: auto auto;
margin: auto auto;
}
使用方法,在需要添加扫码登录的地方导入这个组件,实列
父组件接收子组件的参数
演示代码
@import 'login.less'; 输入您的账号信息!
效果
现在就差实现一个扫码的移动端
移动端实现 在需要添加此功能的Android项目中导入以下依赖:代码:
// websocket
compile "org.java-websocket:Java-WebSocket:1.3.7"
// 相机识别二维码
implementation('com.journeyapps:zxing-android-embedded:4.1.0')
页面实现
1,扫码登录页面
(1)界面(很简陋),后期优化一下
解析: 真机可通过开始扫描扫描二维码加入,模拟器可以手动输入房间号
(2),layout代码
(3),java代码
public class ScanfQR extends AppCompatActivity {
private Context context = ScanfQR.this;
private Activity activity = ScanfQR.this;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scanf_q_r);
findViewById(R.id.begainScan).setonClickListener(new View.onClickListener() {
@Override
public void onClick(View v) {
// 创建IntentIntegrator对象
IntentIntegrator intentIntegrator = new IntentIntegrator(ScanfQR.this);
// 开始扫描
intentIntegrator.initiateScan();
}
});
findViewById(R.id.ScanOfQr_id_submit).setonClickListener(new View.onClickListener() {
@Override
public void onClick(View v) {
setUpConnection();
}
});
}
// 处理扫码结果
private boolean handleScanResult(int requestCode, int resultCode, Intent data){
IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
if (result != null) { // 获取拍照结果并处理
if (result.getContents() == null) {
Toast.makeText(this, "取消扫描", Toast.LENGTH_LONG).show();
}else {
JsonHelper jsonHelper = new JsonHelper(result.getContents());
if(jsonHelper.getStringValue("tag").equals("QRLogin")){
User user = (User) getApplication();
if(user.isLogin()){
Toast.makeText(context, "正在跳转到登录授权", Toast.LENGTH_SHORT).show();
String host = jsonHelper.getStringValue("host");
int id = jsonHelper.getIntValue("id");
qr_websocket = new QR_Websocket(host,user,id+"");
initScanQR();
}else { // 未登录
//tip
Toast.makeText(context, "此二维码为扫码登录码,需要登录后才能扫码,请登录后重新扫描!", Toast.LENGTH_SHORT).show();
User.alertLogin(this);
}
return true;
}
}
}
return false;
}
// 初始化扫码,并监听扫码链接结果,如果成功链接则跳转到授权页面
private void initScanQR(){
qr_websocket.connect();
qr_websocket.getIsConnect().observe(this, new Observer() {
@Override
public void onChanged(Boolean aBoolean) {
if(aBoolean){
User.getAuth(activity); // 跳转用户授权界面
}
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// 获取二维码解析结果并处理
boolean isHandle = handleScanResult(requestCode, resultCode, data);
if(isHandle){
return;
}
// //登录结果
// boolean isLogin = User.handleLoginState(context,requestCode,resultCode);
// if(isLogin){
// setUpConnection();
// return;
// }
// 授权状态
String token = User.handleAuthState(context,requestCode,resultCode);
if(token!=null){
qr_websocket.sendLoginInfo(token);
return;
}
}
QR_Websocket qr_websocket;
// 通过输入框链接ws,因为模拟器无法打开相机,可以通过手动输入房间id加入
public void setUpConnection() {
User user = (User)getApplication(); // 获取全局用户信息
EditText editText = findViewById(R.id.ScanOfQr_id);
String id = editText.getText().toString();
if(id.length()!=0){
qr_websocket = new QR_Websocket( SettingsActivity.getHost(context), user,id);
initScanQR();
}else {
Toast.makeText(context, "no id", Toast.LENGTH_SHORT).show();
}
}
// 重写返回方法,关闭开启的ws
@Override
public void onBackPressed() {
if(qr_websocket.isOpen()){
qr_websocket.closeConnection(1,"返回关闭");
}
super.onBackPressed();
}
}
2,注我将授权跳转和处理授权全写在了一个类中,方便调用
3,授权界面
(1) 布局
(2) Layout代码
(3)Java代码
public class ScanLoginSubmit extends AppCompatActivity {
public static final int SUBMIT = 0;
public static final int SUBJECT = 1;
public static final int CANCEL = 2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scan_login_submit);
User user = (User) getApplication();
ImageView avatar = findViewById(R.id.ScanLoginSubmit_avatar);
Glide.with(this).load(user.getUserData().avatar).into(avatar);
TextView username = findViewById(R.id.ScanLoginSubmit_username);
username.setText(user.getUserName());
findViewById(R.id.ScanLoginSubmit_Exit).setonClickListener(new View.onClickListener() {
@Override
public void onClick(View v) {
setResult(CANCEL);
finish();
}
});
findViewById(R.id.ScanLoginSubmit_submit).setonClickListener(new View.onClickListener() {
@Override
public void onClick(View v) {
setResult(SUBMIT);
finish();
}
});
findViewById(R.id.ScanLoginSubmit_subject).setonClickListener(new View.onClickListener() {
@Override
public void onClick(View v) {
setResult(SUBJECT);
finish();
}
});
}
@Override
public void onBackPressed() {
setResult(CANCEL);
super.onBackPressed();
}
}
终于写完了,感谢大家看到这里,我后期会专门出这三个模块的demo,有兴趣可以关注一下哦~,今天除夕夜,祝大家新年快乐!



