文章目录
- 一、引言
- 二、设计
- 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();
}
@Override
public 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);
}
- 返回结果
@Override
protected 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) { // Code
this.checkCode = value;
},
onRegView() { // router -> Reg
this.$router.push('/reg')
},
async login() { // email and pass Login
if (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-uuid
const { data: res } = await this.$http.get("xpq/qr/sq")
var string = JSON.stringify(res.data)
console.log(string)
this.disLogin = !this.disLogin
if (!this.disLogin) {
this.textLogin = "扫 码 登 录"
// uuid -> qrcode
if (string.length > 5) {
this.showCodeUrl = string
this.codeUrl = res.data.key
// addtimer
this.setTimer()
}
} else {
this.textLogin = "用 户 登 录"
// deltimer
clearInterval(this.timer)
this.timer = null
}
},
async sx_qr() { // 点击 qr 刷新
const { data: res } = await this.$http.post(this.codeHttpUrl)
var string = JSON.stringify(res)
// uuid -> qrcode
if (string.length > 5) {
this.showCodeUrl = string
this.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 {
@Resource
RedisTemplate redisTemplate;
@Resource
UserService userService;
@Resource
private 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代码地址之后会更新出来,我得确定一下项目开源机制