用Android和node.js实现扫码登录

实现思路

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='<h1>Text Too Large</h1>';
    }
  }
}

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/
  ]})

/**
 * 使用koa-compose 集成中间件
 */
const middleware = compose([
  statics(path.join(__dirname, '../public')), // 静态文件开始路径
  jwt //
])

app.use(middleware)
app.use(router())

app.listen(3000)

需要登录端

我使用的是iview-admin 2.0 的框架,我就只写我修改的部分,大家可以按照这个思路自己编写一个页面.因为我自己写的页面太丑了,就不展示了展示别人优秀的页面

LoginScanfQRForm.vue

<template>
  <div>
    <img
      v-show="loginScanfQR"
      class="qr"
      :src="loginScanfQR"
      alt="wait comebeing"
    />
    <Button @click="changeToPasswordLogin" type="primary" long>密码登陆</Button>
  </div>
</template>
<script>
import config from '../../config' 
/*
export default {
    host: '192.168.1.2',
}
*/
import { getQRImg } from '@/api/login'
/*
const getQRImg = (text) => {
  return 'http://192.168.1.2:3000/api/public/getQR?text=' + text
}
*/
export default {
  name: 'LoginScanfQRForm',
  props: {},
  data () {
    return {
      id: '',
      isEnter: false,
      loginScanfQR: null,
      ws: null,
      sendPingTimeInretval: null,
      reConnectTimeIntretval: null
    }
  },
  watch: {
  },
  mounted () {
    this.connectServe()
  },
  methods: {
    setQr () {
      const text = JSON.stringify({
        host: config.host,
        id: this.id,
        tag: 'QRLogin'
      })
      this.loginScanfQR = getQRImg(text)
    },
    sendPing () {
      if (this.ws.readyState === 1 && this.ws.isLife === true) { // 1 normal
        this.ws.send(JSON.stringify({
          event: 'heartbeat',
          msg: 'ping'
        }))
      } else { // 链接不正常
        this.reConnect()
      }
    },
    sendPong () {
      this.ws.send(JSON.stringify({
        event: 'heartbeat',
        msg: 'pong'
      }))
    },
    onopen: function () {
      clearInterval(this.reConnectTimeIntretval)
      console.log('open: ' + this.ws.readyState)
      this.$Message.info('已经链接到服务器')
      this.ws.send(JSON.stringify({ id: this.id, event: 'setUp' })) // 请求创建组
      this.sendPingTimeInretval = setInterval(() => {
        this.sendPing()
        this.ws.isLife = false
      }, 3000)
    },
    onclose: function (event) {
      // console.log('已经关闭')
      this.$Message.info('已经关闭')
      this.loginScanfQR = null
    },
    onmessage: function (event) {
      var data = event.data
      var obj = JSON.parse(data)
      switch (obj.event) {
        case 'heartbeat':
          if (obj.msg === 'pong') {
            this.ws.isLife = true
          } else if (obj.msg === 'ping') {
            this.sendPong()
          }
          break
        case 'login':
          this.ws.send(JSON.stringify({ event: 'succeedLogin', id: this.id }))
          this.$Message.info('成功获取授权!正在跳转到登陆界面')
          clearInterval(this.sendPingTimeInretval) // 不再定时发送ping
          this.ws.close()
          this.scanfSucceed(obj.token)
          break
        case 'setUp': // 返回创建组的编号
          this.id = obj.id
          this.ws.isLife = true
          this.setQr()
          this.$Message.info('可以开始扫描')
          break
        case 'scanfed':
          this.$Message.info('扫描成功!请在手机端上确定!')
          break
      }
    },
    onerror: function (errror) { // 链接错误
      this.$Message.errror('链接错误!')
    },
    reConnect () { // 重新链接
      clearInterval(this.sendPingTimeInretval)
      this.reConnectTimeIntretval = setInterval(() => {
        this.connectServe()
        this.$Message.info('正在尝试重新链接!')
      }, 3000)
    },
    connectServe () { // 创建一个ws链接
      this.ws = new WebSocket('ws://' + config.host + ':3001')
      this.ws.onopen = this.onopen
      this.ws.onmessage = this.onmessage
      this.ws.onclose = this.onclose
      this.ws.onerror = this.onerror
    },
    changeToPasswordLogin () { // 切换为密码登录
      clearInterval(this.sendPingTimeInretval)
      clearInterval(this.reConnectTimeIntretval)
      this.ws.close()
      this.$emit('changeToPasswordLogin')
    },
    scanfSucceed (token) { // 传递参数给父组件
      this.$emit('scanfSucceed', token)
    }
  }
}
</script>
<style lang="css">
.qr {
  padding: auto auto;
  margin: auto auto;
}
</style>

 使用方法,在需要添加扫码登录的地方导入这个组件,实列

父组件接收子组件的参数

演示代码

<style lang="less">
@import 'login.less';
</style>

<template>
  <div class="login">
    <div class="login-con">
      <Card icon="log-in" title="欢迎登录" :bordered="false">
        <div class="form-con" v-if="!qrLogin">
          <login-form
            @on-success-valid="handleSubmit"
            @changeToQrLogin="changeToQrLogin"
          ></login-form>
          <p class="login-tip">输入您的账号信息!</p>
        </div>
        <div class="form-con" v-else>
          <LoginScanfQRForm
            @changeToPasswordLogin="changeToPasswordLogin"
            @scanfSucceed="scanfSucceed"
          ></LoginScanfQRForm>
        </div>
      </Card>
    </div>
  </div>
</template>
<script>
import LoginForm from '_c/login-form'
import LoginScanfQRForm from '_c/login-QR'
import { mapActions } from 'vuex'
export default {
  components: {
    LoginForm,
    LoginScanfQRForm
  },
  data () {
    return {
      qrLogin: false
    }
  },
  methods: {
    ...mapActions(['handleLogin', 'getUserInfo']),
    handleSubmit ({ userLoginInfo }) {
      this.handleLogin({ userLoginInfo }).then(res => {
        if (res.code === 200) {
          this.$router.push({ name: this.$config.homeName })
        } else {
          alert(res.msg)
        }
      })
    },
    changeToQrLogin () { // 切换为二维码登录
      this.qrLogin = true
    },
    changeToPasswordLogin () { // 切换为账号和密码登录
      this.qrLogin = false
    },
    scanfSucceed (token) {
      this.$store.commit('setToken', token)
      this.getUserInfo().then(res => {
        console.log(res)
        if (res.code === 200) {
          this.$router.push({ name: this.$config.homeName })
        } else {
          alert(res.msg)
        }
      })
    }
  }
}
</script>
<style></style>

 效果

 现在就差实现一个扫码的移动端

移动端实现

在需要添加此功能的Android项目中导入以下依赖:

 代码:

// websocket
compile "org.java-websocket:Java-WebSocket:1.3.7"
// 相机识别二维码
implementation('com.journeyapps:zxing-android-embedded:4.1.0')

页面实现

1,扫码登录页面

(1)界面(很简陋),后期优化一下

解析: 真机可通过开始扫描扫描二维码加入,模拟器可以手动输入房间号

(2),layout代码

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.ScanfQR">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:layout_editor_absoluteX="263dp"
        tools:layout_editor_absoluteY="164dp">

        <Button
            android:id="@+id/begainScan"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="开始扫描" />

        <EditText
            android:id="@+id/ScanOfQr_id"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="id"
            ></EditText>

        <Button
            android:id="@+id/ScanOfQr_id_submit"
            android:text="加入"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"></Button>

    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

(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<Boolean>() {
            @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代码

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.login.ScanLoginSubmit">

    <LinearLayout
        android:id="@+id/linearLayout2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <Button
            android:id="@+id/ScanLoginSubmit_Exit"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="exit" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:orientation="vertical">
            <ImageView
                android:id="@+id/ScanLoginSubmit_avatar"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                tools:srcCompat="@tools:sample/avatars" />
            <TextView
                android:id="@+id/ScanLoginSubmit_username"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/app_name"></TextView>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="100dp"
                android:orientation="vertical">
                <Button
                    android:id="@+id/ScanLoginSubmit_submit"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="授权"
                    android:layout_gravity="center"></Button>
                <Button
                    android:id="@+id/ScanLoginSubmit_subject"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="拒绝"
                    android:layout_gravity="center"></Button>
            </LinearLayout>
        </LinearLayout>
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

(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,有兴趣可以关注一下哦~,今天除夕夜,祝大家新年快乐!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
### 回答1: 使用Vue和Node.js实现个人博客系统,可以让开发者快速搭建一个高效、灵活、易于维护的博客系统。Vue作为前端框架,可以提供良好的用户体验和交互效果,而Node.js则可以提供高效的后端支持,使得博客系统具有更好的性能和可扩展性。此外,Vue和Node.js都有着丰富的社区资源和插件库,可以帮助开发者快速解决问题和实现功能。 ### 回答2: 使用Vue和Node.js实现个人博客系统有以下几个原因: 1. 前后端分离:Vue是一款流行的前端框架,它提供了一种简单、高效的方式来构建用户界面。通过使用Vue,可以实现前后端分离,将前端和后端的开发过程分离,提高了开发效率。 2. Vue的组件化开发:Vue采用了组件化开发的思想,使得代码的复用性更高,开发更加模块化。通过将博客系统的各个功能模块拆分成独立的组件,可以方便地进行管理和维护。 3. Node.js的高效性能:Node.js是一种基于事件驱动、非阻塞I/O模型的服务器端JavaScript运行环境。相比传统的后端开发语言,Node.js具有更高的性能和并发能力,能够处理大量的并发请求,适合实现高负载的博客系统。 4. 可扩展性:Vue和Node.js都具有良好的可扩展性,可以根据博客系统的需求进行定制开发。Vue提供了丰富的插件和组件库,可以方便地进行功能扩展;而Node.js通过使用npm包管理器,可以引入各种第三方模块,快速构建博客系统。 5. 生态系统完善:Vue和Node.js都拥有庞大的开发者社区和活跃的生态系统。这意味着许多优秀的第三方库和工具已经被开发出来,并得到了长期维护和更新。这为博客系统的开发提供了更多的选择和支持。 综上所述,使用Vue和Node.js实现个人博客系统,既可以提高开发效率,又能够实现更高的性能和可扩展性,同时还能够借助强大的开发者社区和生态系统,为博客的开发提供更多的资源和支持。 ### 回答3: 使用Vue和Node.js实现个人博客系统有以下几个原因: 首先,Vue是一款轻量级的JavaScript框架,具有良好的响应式性能和组件化开发思想。Vue的虚拟DOM机制可以提高页面渲染性能,而且Vue的组件化开发可以使得代码更加模块化,便于维护和扩展。在个人博客系统中,我们可以将不同的功能模块封装成Vue组件,使得代码结构清晰,更易于开发和维护。 其次,Node.js是一个基于JavaScript的运行时环境,具有事件驱动和非阻塞I/O的特性。Node.js的高性能使得它非常适合处理实时性要求较高的应用,比如个人博客系统。在个人博客系统中,用户可以实时发布、编辑、删除文章,Node.js的事件驱动和非阻塞I/O特性使得这些操作可以高效地处理并及时地更新到页面上。 另外,Vue和Node.js都具有活跃的社区和丰富的生态系统。Vue社区有大量的插件和组件可供使用,可以方便地实现各种功能需求,比如路由管理、表单验证、数据可视化等。而Node.js的包管理工具npm也提供了大量的模块可供使用,可以加速开发过程,提高开发效率。 最后,Vue和Node.js都具有良好的文档和学习资源。Vue官方提供了详细的文档和教程,社区中也有很多优秀的博客和视频教程可供学习。而Node.js的官方文档和npm的官方网站也提供了很多的学习资源和示例代码,便于开发者快速上手和解决问题。 综上所述,使用Vue和Node.js实现个人博客系统能够方便地开发、维护和扩展系统,同时享受到活跃的社区和丰富的学习资源,提高开发效率和用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小周努力变优秀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值