Android — 实现扫码登录功能

现在大部分网站都有扫码登录功能,搭配相应的App就能免去输入账号密码实现快速登录。本文简单介绍如何实现扫码登录功能。

实现扫码登录

之前参与过一个电视App的开发,采用扫码登录,需要使用配套的App扫码登录后才能进入到主页。那么扫码登录该怎么实现呢?大致流程如下:

  1. 被扫端展示一个二维码,二维码包含被扫端的唯一标识(如设备id),并与服务端保持通讯(轮询、长连接、推送)。
  2. 扫码端扫描二维码之后,使用获取到的被扫端的唯一标识(如设备id)调用服务端扫码登录接口。
  3. 服务端接收扫码端发起的扫码登录请求,处理(如验证用户信息)后将登录信息发送到被扫端。

PS: 此为大致流程,具体使用需要根据实际需求进行调整。

接下来简单演示一下此流程。

添加依赖库

添加需要的SDK依赖库,在项目app module的build.gradle中的dependencies中添加依赖:

scss 复制代码
dependencies { 
    // 实现服务端(http、socket)
    implementation("org.nanohttpd:nanohttpd:2.3.1") 
    implementation("org.nanohttpd:nanohttpd-websocket:2.3.1")
    
    // 与服务端通信
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    
    // 扫描解析、生成二维码
    implementation("com.github.jenly1314:zxing-lite:3.1.0")
}

服务端

使用NanoHttpD实现Socket服务端(与被扫端通信)和Http服务端(与扫码端通信),示例代码如下:

Socket服务

与被扫端保持通讯,在Http服务接收并处理完扫码登录请求后,将获取到的用户id发送给被扫端。

kotlin 复制代码
class ServerSocketClient : NanoWSD(9090) {

    private var serverWebSocket: ServerWebSocket? = null

    override fun openWebSocket(handshake: IHTTPSession?): WebSocket {
        return ServerWebSocket(handshake).also { serverWebSocket = it }
    }

    private class ServerWebSocket(handshake: IHTTPSession?) : WebSocket(handshake) {
        override fun onOpen() {}

        override fun onClose(code: WebSocketFrame.CloseCode?, reason: String?, initiatedByRemote: Boolean) {}

        override fun onMessage(message: WebSocketFrame?) {}

        override fun onPong(pong: WebSocketFrame?) {}

        override fun onException(exception: IOException?) {}
    }

    override fun stop() {
        super.stop()
        serverWebSocket = null
    }

    fun sendMessage(message: String) {
        serverWebSocket?.send(message)
    }
}

Http服务

接收并处理来自扫码端的扫码登录请求,通过设备id和用户id判断被扫端是否可以登录。

kotlin 复制代码
const val APP_SCAN_INTERFACE = "loginViaScan"

const val USER_ID = "userId"
const val EXAMPLE_USER_ID = "123456789"

const val DEVICE_ID = "deviceId"
const val EXAMPLE_DEVICE_ID = "example_device_id0001"

class ServerHttpClient(private var scanLoginSucceedListener: ((userId: String) -> Unit)? = null) : NanoHTTPD(8080) {

    override fun serve(session: IHTTPSession?): Response {
        val uri = session?.uri
        return if (uri == "/$APP_SCAN_INTERFACE" &&
            session.parameters[USER_ID]?.first() == EXAMPLE_USER_ID &&
            session.parameters[DEVICE_ID]?.first() == EXAMPLE_DEVICE_ID
        ) {
            scanLoginSucceedListener?.invoke(session.parameters[USER_ID]?.first() ?: "")
            newFixedLengthResponse("Login Succeed")
        } else {
            super.serve(session)
        }
    }
}

服务控制类

启动或停止Socket服务和Http服务。

kotlin 复制代码
object ServerController {

    private var serverSocketClient: ServerSocketClient? = null
    private var serverHttpClient: ServerHttpClient? = null

    fun startServer() {
        (serverSocketClient ?: ServerSocketClient().also {
            serverSocketClient = it
        }).run {
            if (!isAlive) {
                start(0)
            }
        }

        (serverHttpClient ?: ServerHttpClient {
            serverSocketClient?.sendMessage("Login Succeed, user id is $it")
        }.also {
            serverHttpClient = it
        }).run {
            if (!isAlive) {
                start(NanoHTTPD.SOCKET_READ_TIMEOUT, true)
            }
        }
    }

    fun stopServer() {
        serverSocketClient?.stop()
        serverSocketClient = null

        serverHttpClient?.stop()
        serverHttpClient = null
    }
}

被扫端

Socket辅助类

使用OkHttp与服务端进行Socket通信。

kotlin 复制代码
class DevicesSocketHelper(private val messageListener: ((message: String) -> Unit)? = null) {

    private var webSocket: WebSocket? = null

    private val webSocketListener = object : WebSocketListener() {
        override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
            super.onMessage(webSocket, bytes)
            messageListener?.invoke(bytes.utf8())
        }

        override fun onMessage(webSocket: WebSocket, text: String) {
            super.onMessage(webSocket, text)
            messageListener?.invoke(text)
        }
    }

    fun openSocketConnection(serverPath: String) {
        val okHttpClient = OkHttpClient.Builder()
            .connectTimeout(120, TimeUnit.SECONDS)
            .readTimeout(120, TimeUnit.SECONDS)
            .build()
        val request = Request.Builder().url(serverPath).build()
        webSocket = okHttpClient.newWebSocket(request, webSocketListener)
    }

    fun release() {
        webSocket?.close(1000, "")
        webSocket = null
    }
}

被扫端示例页面

先展示二维码,接收到服务端的消息后,显示用户id。

kotlin 复制代码
class DeviceExampleActivity : AppCompatActivity() {

    private lateinit var binding: LayoutDeviceExampleActivityBinding

    private var socketHelper: DevicesSocketHelper? = DevicesSocketHelper() { message ->
        // 接收到服务端发来的消息,改变显示内容
        runOnUiThread {
            binding.tvUserInfo.text = message
            binding.ivQrCode.visibility = View.GONE
            binding.tvUserInfo.visibility = View.VISIBLE
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = LayoutDeviceExampleActivityBinding.inflate(layoutInflater).also {
            setContentView(it.root)
            it.includeTitle.tvTitle.text = "Device Example"
        }

        lifecycleScope.launch(Dispatchers.IO) {
            // 使用设备id生成二维码
            CodeUtils.createQRCode(EXAMPLE_DEVICE_ID, DensityUtil.dp2Px(200)).let { qrCode ->
                withContext(Dispatchers.Main) {
                    binding.ivQrCode.setImageBitmap(qrCode)
                }
            }
        }

        socketHelper?.openSocketConnection("ws://localhost:9090/")
    }

    override fun onDestroy() {
        super.onDestroy()
        socketHelper?.release()
        socketHelper = null
    }
}

扫描端

扫码页

继承zxing-lite库的BarcodeCameraScanActivity类,简单实现扫描与解析二维码。

kotlin 复制代码
class ScanQRCodeActivity : BarcodeCameraScanActivity() {

    override fun initCameraScan(cameraScan: CameraScan<Result>) {
        super.initCameraScan(cameraScan)
        // 播放扫码音效
        cameraScan.setPlayBeep(true)
    }

    override fun createAnalyzer(): Analyzer<Result> {
        return QRCodeAnalyzer(DecodeConfig().apply {
            // 设置仅识别二维码
            setHints(DecodeFormatManager.QR_CODE_HINTS)
        })
    }

    override fun onScanResultCallback(result: AnalyzeResult<Result>) {
        // 已获取结果,停止识别二维码
        cameraScan.setAnalyzeImage(false)
        // 返回扫码结果
        setResult(Activity.RESULT_OK, Intent().apply {
            putExtra(CameraScan.SCAN_RESULT, result.result.text)
        })
        finish()
    }
}

扫描端示例页面

提供扫码入口,提供输入框用于输入服务端IP,获取到扫码结果后发送给服务端。

kotlin 复制代码
class AppScanExampleActivity : AppCompatActivity() {

    private lateinit var binding: LayoutAppScanExampleActivityBinding

    private var serverIp: String = ""

    private val scanQRCodeLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        if (it.resultCode == Activity.RESULT_OK) {
            it.data?.getStringExtra(CameraScan.SCAN_RESULT)?.let { deviceId ->
                sendRequestToServer(deviceId)
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = LayoutAppScanExampleActivityBinding.inflate(layoutInflater).also {
            setContentView(it.root)
        }

        OkHttpHelper.init()

        binding.btnScan.setOnClickListener {
            // 获取输入的服务端ip(两台设备在同一WIFI下,直接通过IP访问服务端)
            serverIp = binding.etInputIp.text.toString()
            if (serverIp.isEmpty()) {
                showSnakeBar("Server ip can not be empty")
                return@setOnClickListener
            }
            hideKeyboard(binding.etInputIp)
            scanQRCodeLauncher.launch(Intent(this, ScanQRCodeActivity::class.java))
        }
    }

    private fun sendRequestToServer(deviceId: String) {
        OkHttpHelper.sendGetRequest("http://${serverIp}:8080/${APP_SCAN_INTERFACE}", mapOf(Pair(USER_ID, EXAMPLE_USER_ID), Pair(DEVICE_ID, deviceId)), object : RequestCallback {
            override fun onResponse(success: Boolean, responseBody: ResponseBody?) {
                showSnakeBar("Scan login ${if (success) "succeed" else "failure"}")
            }

            override fun onFailure(errorMessage: String?) {
                showSnakeBar("Scan login failure")
            }
        })
    }

    private fun hideKeyboard(view: View) {
        view.clearFocus()
        WindowInsetsControllerCompat(window, view).hide(WindowInsetsCompat.Type.ime())
    }

    private fun showSnakeBar(message: String) {
        runOnUiThread {
            Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show()
        }
    }
}

示例入口页

提供被扫端和扫码端入口,打开被扫端时同时启动服务端。

kotlin 复制代码
class ScanLoginExampleActivity : AppCompatActivity() {

    private lateinit var binding: LayoutScanLoginExampleActivityBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = LayoutScanLoginExampleActivityBinding.inflate(layoutInflater).also {
            setContentView(it.root)
            it.includeTitle.tvTitle.text = "Scan Login Example"
            it.btnOpenDeviceExample.setOnClickListener {
                // 打开被扫端同时启动服务
                ServerController.startServer()
                startActivity(Intent(this, DeviceExampleActivity::class.java))
            }
            it.btnOpenAppExample.setOnClickListener { startActivity(Intent(this, AppScanExampleActivity::class.java)) }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        ServerController.stopServer()
    }
}

效果演示与示例代码

最终效果如下图:

被扫端 扫码端

演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee

相关推荐
阿巴斯甜1 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker2 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95273 小时前
Andorid Google 登录接入文档
android
黄林晴4 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab17 小时前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿19 小时前
Android MediaPlayer 笔记
android
Jony_20 小时前
Android 启动优化方案
android
阿巴斯甜20 小时前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇20 小时前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android