【全栈开发】基于Spring BootVueAndroid扫码授权登录

news/2025/2/1 19:43:36/

文章目录

    • 一、引言
    • 二、设计
      • 1、移动端(Android)
        • (1)库
        • (2)依赖
        • (3)使用
      • 2、前端(Vue)
        • (1)库
        • (2)使用
      • 3、后端(Spring Boot)
    • 三、附件

一、引言

  • 描述:如何通过移动设备向网页授权登录。
  • 难度:中级
  • 知识点:
    1、ZXing(Android库)
    2、QrCode(Vue库)
    3、Redis过期策略
    4、JWT令牌技术
  • 效果
    在这里插入图片描述

二、设计

1、移动端(Android)

zxing是谷歌推出的识别多种条形码的开源项目

(1)库

感谢开源者们的付出(地址:https://github.com/zxing/zxing)

(2)依赖

implementation 'com.google.zxing:core:3.3.0'

(3)使用

  • cv工程
    在这里插入图片描述
  • 配置权限
    <uses-permission android:name="android.permission.INTERNET" /> <!-- 网络权限 --><uses-permission android:name="android.permission.VIBRATE" /> <!-- 震动权限 --><uses-permission android:name="android.permission.CAMERA" /> <!-- 摄像头权限 --><uses-feature android:name="android.hardware.camera.autofocus" /> <!-- 自动聚焦权限 -->
  • 判断权限
    private static final String DECODED_CONTENT_KEY = "codedContent";private static final String DECODED_BITMAP_KEY = "codedBitmap";private static final int REQUEST_CODE_SCAN = 0x0000;//动态权限申请if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CAMERA}, 1);} else {goScan();}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {switch (requestCode) {case 1:if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {goScan();} else {Toast.makeText(this, "你拒绝了权限申请,可能无法打开相机扫码哟!", Toast.LENGTH_SHORT).show();}break;default:}}
  • 跳转方法
	/*** 跳转到扫码界面扫码*/private void goScan(){Intent intent = new Intent(MainActivity.this, CaptureActivity.class);startActivityForResult(intent, REQUEST_CODE_SCAN);}
  • 返回结果
    @Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {super.onActivityResult(requestCode, resultCode, data);// 扫描二维码/条码回传if (requestCode == REQUEST_CODE_SCAN && resultCode == RESULT_OK) {if (data != null) {//返回的文本内容String content = data.getStringExtra(DECODED_CONTENT_KEY);//返回的BitMap图像Bitmap bitmap = data.getParcelableExtra(DECODED_BITMAP_KEY);tv_scanResult.setText("你扫描到的内容是:" + content);}}}

2、前端(Vue)

关于router和store请自主学习

(1)库

  • npm下载
npm install qrcode --save

(2)使用

  • 模块:生成二维码(ValidateCode.vue)
<template><canvas ref="canvas" @click="draw" width="140" height="40" style="cursor: pointer;"></canvas>
</template>
<script>
export default {data() {return {codes: [],ctx: "",colors: ["red", "yellow", "blue", "green", "pink", "black"],code_Len: 4};},mounted() {this.draw();},computed: {codeString() {let result = "";for (let i = 0; i < this.codes.length; i++) {result += this.codes[i];}return result.toUpperCase();}},watch: {codeString: function (newValue) {this.$emit("change", newValue);}},methods: {generateRandom() {this.codes = [];const chars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";const charsArr = chars.split("");for (let i = 0; i < this.code_Len; i++) {const num = Math.floor(Math.random() * charsArr.length);this.codes.push(charsArr[num]);}},draw() {this.generateRandom();this.drawText();},drawLine() {const lineNumber = 3; // 线条条数const lineX = 140;const lineY = 30; // 最大线条坐标for (let i = 0; i < lineNumber; i++) {this.ctx.strokeStyle = this.colors[Math.floor(Math.random() * 5)];this.ctx.beginPath();this.ctx.moveTo(Math.floor(Math.random() * lineX),Math.floor(Math.random() * lineY));this.ctx.lineTo(Math.floor(Math.random() * lineX),Math.floor(Math.random() * lineY));this.ctx.stroke();}},drawText() {const canvas = this.$refs["canvas"];this.ctx = canvas.getContext("2d");this.ctx.fillStyle = "#BFEFFF";this.ctx.fillRect(0, 0, 140, 40);this.ctx.font = "20px Verdana";let x = 15;for (let i = 0; i < this.code_Len; i++) {this.ctx.fillStyle = this.colors[Math.floor(Math.random() * 5)];this.ctx.fillText(this.codes[i], x, 25);x = x + 30;}this.drawLine();}}
};
</script>
  • UI界面
<template><div class="login"><el-button type="text" class="book" @click="book">功能介绍</el-button><div class="main"><div class="login-title">{{ textLogin }}</div><!-- 输入账号和密码 --><div class="login_1" v-show="disLogin"><div class="input-data"><input type="text" required="" id="userName" v-model="email" /><div class="underlineN"></div><label>E-mail</label></div><div class="input-data"><input type="password" required="" id="userPass" v-model="pass" /><div class="underlineP"></div><label>Password</label></div><div class="login-yzm"><validate-code id="canvas" ref="ref_validateCode" @change="changeCode" /><div id="yzm-text">验证码:</div><input type="text" id="yzm-input" v-model="inputVal"></div><div class="login-btn"><button @click="login"><span>登录</span></button><button @click="onRegView"><span>注册</span></button></div></div><!-- 二维码登录 --><div class="login_2" v-show="!disLogin"><!-- qrcode-vue --><qrcode-vue :value="showCodeUrl" :size="size" level="H" @click="sx_qr" /></div><div class="btn_qr" @click="login_qr"><img src="@/assets/saoyisao2.png" style="width: 20px; height: 20px;" alt="扫码登录"></div></div></div>
</template><script>
import ValidateCode from '@/components/part/ValidateCode.vue';
import QrcodeVue from 'qrcode.vue'export default {name: 'LoginView',components: {ValidateCode,QrcodeVue},data() {return {// 验证码组件数据inputVal: "",checkCode: "",// 基本数据email: '',pass: '',// 切换登录方式textLogin: "用  户  登  录",disLogin: true,// 二维码codeHttpUrl: this.$store.state.webMapping.qr.findUuid,showCodeUrl: 'www.xpq.com',codeUrl: '',size: 180,// 定时器timer: null,qrHttpTimer: this.$store.state.webMapping.qr.findTime,}},methods: {changeCode(value) {     // Codethis.checkCode = value;},onRegView() {          // router -> Regthis.$router.push('/reg')},async login() {       // email and pass Loginif (this.email == '') {this.$open.openInfo('请输入邮箱!')} else if (this.pass == '') {this.$open.openInfo('请输入密码!')} else if (this.inputVal == '') {this.$open.openInfo('请输入验证码!')} else {if (this.inputVal.toUpperCase() === this.checkCode) {// this.$open.openOk('验证码正确!')var data = new FormData()data.append('email', this.email)data.append('pass', this.pass)const { data: res } = await this.$http.post('xpq/user/login', data)console.log(res)if (res.code == 0) {this.$open.openNo(res.msg)this.inputVal = "";this.$refs["ref_validateCode"].draw();} else {this.tzLogin(res.data)}} else {this.$open.openNo('验证码错误!')this.inputVal = "";this.$refs["ref_validateCode"].draw();}}},async login_qr() {    // btn qrCode Login// web-uuidconst { data: res } = await this.$http.get("xpq/qr/sq")var string = JSON.stringify(res.data)console.log(string)this.disLogin = !this.disLoginif (!this.disLogin) {this.textLogin = "扫  码  登  录"// uuid -> qrcodeif (string.length > 5) {this.showCodeUrl = stringthis.codeUrl = res.data.key// addtimerthis.setTimer()}} else {this.textLogin = "用  户  登  录"// deltimerclearInterval(this.timer)this.timer = null}},async sx_qr() {       // 点击 qr 刷新const { data: res } = await this.$http.post(this.codeHttpUrl)var string = JSON.stringify(res)// uuid -> qrcodeif (string.length > 5) {this.showCodeUrl = stringthis.codeUrl = res.key}},async setTimer() {    // 二维码定时器if (this.timer == null) {this.timer = setInterval(() => {// 循环运行this.getTimer()}, 1000)}},async getTimer() {     // 查看二维码是否激活var data = new FormData()data.append('key', this.codeUrl)console.log(this.codeUrl)const { data: res } = await this.$http.post("xpq/qr/login", data)if (res.code == 0) {if (res.msg.length == 1) {sx_qr()}} else {// 停止计时器clearInterval(this.timer)this.timer = null// 跳转界面this.tzLogin(res.data)}},tzLogin(res) {this.$open.openOk('欢迎回来!')// 将数据存储到sessionStorage中sessionStorage.setItem('id', res.id)sessionStorage.setItem('jwt', res.token)this.$router.push('/home')},book() {this.$router.push('/book')}},created: function () {     // 每次进入此界面,清除定时器,保证运行不受干扰clearInterval(this.timer)this.timer = null}
}
</script><style scoped>
/* 设置自适应屏幕大小 */.login {display: flex;align-items: center;justify-content: center;min-height: 100vh;background: linear-gradient(-135deg, #50c8c2, #4158d0);
}/* 标题设计 */.login-title {text-align: center;font-size: 20px;padding-bottom: 20px;
}/* 标题设计 */.login-title {text-align: center;font-size: 20px;padding-bottom: 20px;
}/* 输入框设计 */.main {width: 450px;background-color: #fff;padding: 30px;box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
}/* 模块规格 *//* 输入框、验证码 */.main .input-data,
.login-yzm {width: 100%;height: 40px;margin: 10px;position: relative;
}/* 输入框规格设计 */.main .input-data input {width: 100%;height: 100%;border: none;border-bottom: 2px solid silver;font-size: 17px;
}/* 动画效果 */.input-data input:focus~label,
.input-data input:valid~label {transform: translateY(-20px);font-size: 15px;color: #4158D0;
}/* 输入框文本提醒动画 */.main .input-data label {position: absolute;bottom: 10px;left: 0;color: grey;pointer-events: none;transition: all 0.3s ease;
}/* 动画设计 */.main .input-data .underlineN {position: absolute;bottom: 0px;height: 2px;width: 100%;
}.input-data .underlineN::before {/* margin-left: -50%; */background: #4158D0;position: absolute;content: "";height: 100%;width: 100%;transform: scaleX(0);transition: transform 0.3s ease;
}.input-data input:focus~.underlineN:before,
.input-data input:valid~.underlineN:before {transform: scaleX(1);
}.main .input-data .underlineP {position: absolute;bottom: 0px;height: 2px;width: 100%;
}.input-data .underlineP::before {/* margin-left: -50%; */background: #4158D0;position: absolute;content: "";height: 100%;width: 100%;transform: scaleX(0);transition: transform 0.3s ease;
}.input-data input:focus~.underlineP:before,
.input-data input:valid~.underlineP:before {transform: scaleX(1);
}/* 按钮设计 */.login-btn {text-align: center;
}.login-btn button {background: radial-gradient(circle, rgba(247, 150, 192, 1) 0%, rgba(118, 174, 241, 1) 100%);border: none;color: #fff;font-family: 'Lato', sans-serif;border-radius: 10px;cursor: pointer;padding: 10px 30px;margin: 10px;position: relative;top: 20px;
}/* 按钮触摸和移出 */.login-btn button:hover {background: transparent;color: #76aef1;
}.login-btn button::before,
.login-btn button::after {content: '';position: absolute;width: 1px;height: 1px;box-shadow: -1px -1px 20px 0px rgba(255, 255, 255, 1), -4px -4px 5px 0px rgba(255, 255, 255, 1), 10px 10px 20px 0px rgba(0, 0, 0, .4), 6px 6px 5px 0px rgba(0, 0, 0, .3);transition: all 0.8s ease;padding: 0;
}.login-btn button::before {top: 0;right: 0;
}.login-btn button::after {bottom: 0;left: 0;
}.login-btn button:hover::before,
.login-btn button:hover::after {height: 100%;
}.login-btn button span::before,
.login-btn button span::after {position: absolute;content: '';width: 0px;box-shadow: -1px -1px 20px 0px rgba(255, 255, 255, 1), -4px -4px 5px 0px rgba(255, 255, 255, 1), 10px 10px 20px 0px rgba(0, 0, 0, .4), 6px 6px 5px 0px rgba(0, 0, 0, .3);transition: all 0.8s ease;
}.login-btn button span::before {top: 0;left: 0;
}.login-btn button span::after {bottom: 0;right: 0;
}.login-btn button span:hover::before,
.login-btn button span:hover::after {width: 100%;
}/* 验证码 *//* 布局 */.login-yzm #canvas,
#yzm-text {float: left;
}.login-yzm #yzm-text {position: relative;left: 25px;top: 10px;
}/* 输入框样式 */.login-yzm #yzm-input {position: relative;top: 7px;left: 18px;height: 30px;width: 160px;
}/* qrCode button */.btn_qr {width: 100%;text-align: right;
}.login_2 {width: 100%;text-align: center;
}.book {position: fixed;top: 50px;right: 0;margin-right: 40px;color: #ffffff;font-size: 15px;
}
</style>

3、后端(Spring Boot)

其实最优解应该是Redis+Spring Task,但这里我没打算使用Spring Task,对初学者多少有点不友好,我之后会专门出一篇专门讲Spring Task

  • 关于JWT令牌技术,博客地址:http://t.csdn.cn/fSa0y
  • 关于Redis的使用,博客地址:http://t.csdn.cn/7x6ls
@RestController
@RequestMapping("/xpq/qr")
@Slf4j
public class QrController {@ResourceRedisTemplate redisTemplate;@ResourceUserService userService;@Resourceprivate JwtProperties jwtProperties;@GetMapping("{id}")public Result<QrInfoVo> qrInfo(@PathVariable Long id) {String key = UUID.randomUUID().toString();QrInfoVo qrInfoVo = new QrInfoVo();qrInfoVo.setId(id);qrInfoVo.setKey(key);log.info("二维码信息:{}", qrInfoVo);// 存入Rides中,设置过期时间为 1分钟redisTemplate.opsForValue().set(key, "0",1, TimeUnit.MINUTES);return Result.success(qrInfoVo);}@PostMapping("appLogin")public Result appLogin(QrInfoVo qrInfoVo) {log.info("获取到app用户信息:{}", qrInfoVo);redisTemplate.opsForValue().set(qrInfoVo.getKey(), String.valueOf(qrInfoVo.getId()),1, TimeUnit.MINUTES);return Result.success();}@PostMapping("/login")public Result<UserLoginVO> login(String key) {log.info("获取key:{}", key);String string = (String) redisTemplate.opsForValue().get(key);if (string.equals("0")) {return Result.error("");} else if(string.equals(null)) {return Result.error("0");} else {log.info("得到key:{}", key);Long keys = Long.valueOf(string).longValue();log.info("用户登录:{}", keys);UserInfoVo u = userService.queryById(keys);// 扫码成功后,生成jwt令牌Map<String, Object> claims = new HashMap<>();claims.put(JwtClaimsConstant.EMP_ID, u.getId());String token = JwtUtil.createJWT(jwtProperties.getAdminSecretKey(),jwtProperties.getAdminTtl(),claims);log.info("jwt令牌:{}", token);UserLoginVO userLoginVO = UserLoginVO.builder().id(u.getId()).userName(u.getName()).image(u.getImage()).token(token).build();return Result.success(userLoginVO);}}}

三、附件

git代码地址之后会更新出来,我得确定一下项目开源机制


http://www.ppmy.cn/news/295813.html

相关文章

购买一套WMS仓储管理系统要多少钱

随着电商行业的快速发展&#xff0c;仓储物流行业也逐渐成为了人们关注的焦点。WMS仓储管理系统作为物流管理领域的重要工具&#xff0c;在提高仓库管理效率、降低运营成本方面具有重要作用。那么&#xff0c;购买一套WMS仓储管理系统要多少钱呢&#xff1f; 首先&#xff0c;我…

Ansible判断执行失败

比如我们要安装java&#xff0c;但如果已经安装了&#xff0c;就没必要再执行了&#xff0c;怎么实现呢&#xff1f; 如下图&#xff0c;把shell执行结果输出到变量 res - name: Check javashell: source /etc/profile && java -versionregister: resignore_errors: …

什么是AI业务流程质检,如何用它做好销售和服务过程监督

近几年&#xff0c;随着语音转写、语义理解和机器学习等技术的成熟&#xff0c;越来越多的企业开始部署基于AI技术的智能质检系统&#xff0c;来帮助坐席、销售和服务团队提高沟通质量管理能力&#xff0c;同时提升沟通中的客户体验。 不过&#xff0c;不论是最初的人工质检&a…

Spring框架-面试题核心概念

目录 1.Spring框架的作用是什么&#xff1f; 2. 什么是DI&#xff1f; 3.什么是AOP&#xff1f; 4.Spring常用注解 5.Spring中的设计模式 6.Spring支持的几种bean的作用域 7.Spring中Bean的生命周期&#xff1f; 8.Spring中的事务管理 9.Spring中的依赖注入方式有几种 10.Sprin…

Spark数据倾斜解决方案三:随机key双重聚合

什么是随机key双重聚合 随机Key双重聚合是指Spark分布式计算对RDD调用reduceByKey等聚合类Shuffle算子进行计算,使用对Key值随机数前缀的处理技巧,对Key值进行二次聚合。 第一次聚合(局部聚合):对每个Key值加上一个随机数,执行第一次reduceByKey聚合操作。第二次聚合(双…

从Android UI收集流的更安全方法

从Android UI收集流的更安全方法 在安卓应用中&#xff0c;通常从UI层收集Kotlin flows以显示屏幕上的数据更新。但是&#xff0c;为了确保不做过多的工作、浪费资源&#xff08;包括CPU和内存&#xff09;或在视图转到后台时泄漏数据&#xff0c;您需要收集这些flows。 在本…

【PCIE体系结构十三】LTSSM

&#x1f449;个人主页&#xff1a;highman110 &#x1f449;作者简介&#xff1a;一名硬件工程师&#xff0c;持续学习&#xff0c;不断记录&#xff0c;保持思考&#xff0c;输出干货内容 参考书籍&#xff1a;《PCI.EXPRESS系统体系结构标准教材 Mindshare》 目录 概览…

笔记本加装内存条,更换散热风扇(以联想y7000为例)

先附上视频地址&#xff1a;联想y7000笔记本加装内存条&#xff0c;更换散热风扇拆机视频_哔哩哔哩_bilibili 步骤1&#xff1a;拆螺丝&#xff0c;卸后盖&#xff08;先洗手清除身上静电&#xff09; 步骤2&#xff1a;将固定内存条两边的杠杆往外掰&#xff0c;内存条自动弹…