IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> 宝安水环境管控平台(Ionic/Angular 移动端) 问题记录 -> 正文阅读

[游戏开发]宝安水环境管控平台(Ionic/Angular 移动端) 问题记录

目录

一. 登录前的验证码校验功能

1. 添加滑动校验 Angular 组件

1.1?verifySlipping.component.html

1.2?verifySlipping.component.css

1.3?verifySlipping.component.ts

1.4 verify.js

1.5?aes-encrypt-decrypt.ts

1.6?verify.service.ts

2. 添加登录时的验证码校验逻辑

2.1 保持 loginService 服务为单例,避免路由守卫出 bug

2.2 获取密钥时,Angular Http 请求发送失败?

2.3 点击登录按钮,初始化验证码

2.4 初始化验证码

2.5?验证码校验成功后 执行真正的登录

2.6 效果展示

二. 修改密码功能

1. 进入页面后,加密登录名,获取新密钥

2. 校验密码,要求包含大小写字母、数字、特殊字符

3. 修改密码,传参格式 FORM['USER_ID'] 的写法


一. 登录前的验证码校验功能

验证码校验功能参考开源项目?AJ-Captcha

将验证码功能,嵌入你的项目里,需要添加以下依赖:

yarn add?crypto-js

yarn add?jwt-decode

yarn add?jquery

1. 添加滑动校验 Angular 组件

AJ-Captcha: 行为验证码(滑动拼图、点选文字),前后端(java)交互,包含vue/h5/Android/IOS/flutter/uni-app/react/php/微信小程序的源码和实现https://gitee.com/anji-plus/captcha

三点说明:

  • 我下载了上面仓库中的 Angular 前端示例,使用 ng serve 运行项目
  • 此示例和我项目中的实际逻辑,区别较大,我几乎只用到了组件
  • 需要参考的前提:项目后端系统已经配备了相应后端接口

下面介绍下验证码组件相关内容

1.1?verifySlipping.component.html

注释掉的部分:验证码提示框的头部,一般情况下用不上,会导致显示混乱

此处需要注意两个ID,这俩 ID 在组件中会被 jquery 操作控制显隐:

  • mpanel1:用于盛放验证码图片、拼图、滑块之类的容器
  • slipping:用于标识这是 滑动校验组件,不是 点选文字校验组件

文件地址:src\app\components\verify\verifySlipping.component.html

<div
  class="modal fade"
  id="slipping"
  tabindex="-1"
  role="dialog"
  aria-labelledby="myLargeModalLabel"
>
  <div class="modal-dialog modal-dialog-centered" role="document">
    <div class="modal-content">
      <!-- <div class="modal-header">
        <h5 class="modal-title" id="exampleModalLabel">请完成滑动安全验证</h5>
        <button
          type="button"
          class="close"
          data-dismiss="modal"
          aria-label="Close"
        >
          <span aria-hidden="true">&times;</span>
        </button>
      </div> -->

      <div class="modal-body">
        <div class="box">
          <div class="verifybox-top">
            <div class="box">
              <div id="mpanel1"></div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

1.2?verifySlipping.component.css

验证码弹框组件的基本样式

注意:验证码弹框的宽度是写死的,这会导致屏幕小的手机,显示不全验证码弹框

由于时间紧促,我没研究改哪些宽度百分比,而是直接使用了 scale(0.85) 缩小移动端弹框

文件地址:src\app\components\verify\verifySlipping.component.css

.btn {
  border: none;
  outline: none;
  width: 300px;
  height: 40px;
  line-height: 40px;
  text-align: center;
  cursor: pointer;
  background-color: #409eff;
  color: #fff;
  font-size: 16px;
  letter-spacing: 1em;
}

.modal-dialog {
  width: 466px;
}

global.scss 下,补充了全局验证码弹框样式

之前打算使用 important 强行覆盖 jq?动态写入的验证码弹框宽度,后来因为要修改的地方太多,懒得改了,所以采用了?transform: translate(-50%, -50%) scale(0.85);

文件地址:src\global.scss

/**
 * 安全验证
*/
.verify-code {
  font-size: 20px;
  text-align: center;
  cursor: pointer;
  margin-bottom: 5px;
  border: 1px solid #ddd;
}

.cerify-code-panel {
  height: 100%;
  overflow: hidden;
}

.verify-code-area {
  float: left;
}

.verify-input-area {
  float: left;
  width: 60%;
  padding-right: 10px;
}

.verify-change-area {
  line-height: 30px;
  float: left;
}

.varify-input-code {
  display: inline-block;
  width: 100%;
  height: 25px;
}

.verify-change-code {
  color: #337ab7;
  cursor: pointer;
}

.verify-btn {
  width: 200px;
  height: 30px;
  background-color: #337ab7;
  color: #ffffff;
  border: none;
  margin-top: 10px;
}

.verifybox {
  position: relative;
  box-sizing: border-box;
  border-radius: 2px;
  background-color: #fff;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) scale(0.85);
}
.verifybox-top {
  padding: 0 15px;
  /* height: 50px; */
  line-height: 50px;
  text-align: left;
  font-size: 16px;
  color: #45494c;
  box-sizing: border-box;
}
.verifybox-bottom {
  padding: 15px;
  box-sizing: border-box;
}
.verifybox-close {
  position: absolute;
  top: 13px;
  right: 9px;
  width: 24px;
  height: 24px;
  line-height: 24px;
  text-align: center;
  cursor: pointer;
}
.mask {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1001;
  width: 100%;
  height: 100vh;
  background: rgba(0, 0, 0, 0.3);
  /* display: none; */
  transition: all 0.5s;
  display: none;
}

.verify-tips {
  position: absolute;
  display: none;
  left: 0px;
  bottom: -35px;
  width: 100%;
  height: 30px;
  /* transition: all .5s; */
  line-height: 30px;
  color: #fff;
  /* animation:move 1.5s linear; */
}

@keyframes move {
  0% {
    bottom: -35px;
  }
  50%,
  80% {
    bottom: 0px;
  }
  100% {
    bottom: -35px;
  }
}

.suc-bg {
  background-color: rgba(92, 184, 92, 0.5);
  filter: progid:DXImageTransform.Microsoft.gradient(startcolorstr=#7f5CB85C, endcolorstr=#7f5CB85C);
}
.err-bg {
  background-color: rgba(217, 83, 79, 0.5);
  filter: progid:DXImageTransform.Microsoft.gradient(startcolorstr=#7fD9534F, endcolorstr=#7fD9534F);
}

/*滑动验证码*/
.verify-bar-area {
  position: relative;
  background: #ffffff;
  text-align: center;
  -webkit-box-sizing: content-box;
  -moz-box-sizing: content-box;
  box-sizing: content-box;
  border: 1px solid #ddd;
  -webkit-border-radius: 4px;
}

.verify-bar-area .verify-move-block {
  position: absolute;
  top: 0px;
  left: 0;
  background: #fff;
  cursor: pointer;
  -webkit-box-sizing: content-box;
  -moz-box-sizing: content-box;
  box-sizing: content-box;
  box-shadow: 0 0 2px #888888;
  -webkit-border-radius: 1px;
}

.verify-bar-area .verify-move-block:hover {
  background-color: #337ab7;
  color: #ffffff;
}

.verify-bar-area .verify-left-bar {
  position: absolute;
  top: -1px;
  left: -1px;
  background: #f0fff0;
  cursor: pointer;
  -webkit-box-sizing: content-box;
  -moz-box-sizing: content-box;
  box-sizing: content-box;
  border: 1px solid #ddd;
}

.verify-img-panel {
  margin: 0;
  -webkit-box-sizing: content-box;
  -moz-box-sizing: content-box;
  box-sizing: content-box;
  border: 1px solid #ddd;
  border-radius: 3px;
  position: relative;
}

.verify-img-panel .verify-refresh {
  width: 25px;
  height: 25px;
  text-align: center;
  padding: 5px;
  cursor: pointer;
  position: absolute;
  top: 0;
  right: 0;
  z-index: 2;
}

.verify-img-panel .icon-refresh {
  font-size: 20px;
  color: #fff;
}

.verify-img-panel .verify-gap {
  background-color: #fff;
  position: relative;
  z-index: 2;
  border: 1px solid #fff;
}

.verify-bar-area .verify-move-block .verify-sub-block {
  position: absolute;
  text-align: center;
  z-index: 3;
  /* border: 1px solid #fff; */
}

.verify-bar-area .verify-move-block .verify-icon {
  font-size: 18px;
}

.verify-bar-area .verify-msg {
  z-index: 3;
}

/* 字体图标的css */
@font-face {
  font-family: "iconfont";
  src: url("./assets/font/iconfont.eot?t=1508229193188"); /* IE9*/
  src: url("./assets/font/iconfont.eot?t=1508229193188#iefix")
      format("embedded-opentype"),
    /* IE6-IE8 */
      url("data:application/x-font-woff;charset=utf-8;base64,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")
      format("woff"),
    url("./assets/font/iconfont.ttf?t=1508229193188") format("truetype"),
    /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
      url("./assets/font/iconfont.svg?t=1508229193188#iconfont") format("svg"); /* iOS 4.1- */
}

.iconfont {
  font-family: "iconfont" !important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.icon-check:before {
  content: "\e645";
}

.icon-close:before {
  content: "\e646";
}

.icon-right:before {
  content: "\e6a3";
}

.icon-refresh:before {
  content: "\e6a4";
}

1.3?verifySlipping.component.ts

官网示例中,初始化验证码是在组件里进行的

但是,我的项目在 初始化验证码请求图片 时,需要传入额外的参数;因此,我注释了组件内的 初始化验证码 逻辑

文件地址:src\app\components\verify\verifySlipping.component.ts

import { Component } from '@angular/core';
import './verify/verify.js';

@Component({
  selector: 'verify-slipping',
  templateUrl: './verifySlipping.component.html',
  styleUrls: ['./verifySlipping.component.css'],
})
export class verifySlippingComponent {
  /**
   * 页面初始化
   */
  ngOnInit(): void {
    // 引入 promise
    if (!window.Promise) {
      document.writeln(
        '<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.min.js"><' +
          '/' +
          'script>'
      );
    }
    // 初始化验证码
    // this.getVerify();
  }

  /**
   * 初始化验证码 - 嵌入式
   */
  // getVerify() {
  //   (<any>$('#mpanel1')).slideVerify({
  //     // 请求后端的服务器地址, 默认地址为安吉服务器
  //     baseUrl: `${interfaceId.getSecurityKeyJWT}`,
  //     // 验证码类型 1)滑动拼图 blockPuzzle 2)文字点选 clickWord
  //     // captchaType: 'blockPuzzle',
  //     // 验证码的显示方式,弹出式 - pop,固定 - fixed,默认为 mode: 'pop'
  //     mode: 'pop',
  //     // 被点击之后出现行为验证码的元素id(mode 为 pop 模式时必填)
  //     containerId:'btn',
  //     // 图片的大小对象, 有默认值 { width: '310px', height: '155px' }, 可省略
  //     imgSize: {
  //       width: '400px',
  //       height: '200px',
  //     },
  //     // 下方滑块的大小对象, 有默认值 { width: '310px', height: '50px' }, 可省略
  //     barSize: {
  //       width: '400px',
  //       height: '40px',
  //     },
  //     // 验证码图片和移动条容器的间隔,单位是px,默认为 vSpace: 5
  //     // vSpace: 5,
  //     // 滑动条内的提示,不设置默认是:'向右滑动完成验证'
  //     // explain: '向右滑动完成验证',
  //     // 调用验证码前,检验参数合法性的函数,(mode = 'pop' 有效)
  //     beforeCheck: function () {
  //       let flag = true;
  //       // 实现: 参数合法性的判断逻辑, 返回一个 boolean 值
  //       return flag;
  //     },
  //     // 验证码初始化成功的回调函数
  //     ready: function () {},
  //     // 验证码匹配成功后的回调函数
  //     success: function (params: any) {
  //       // params 为返回的 二次验证参数
  //       // 需要在下面补充逻辑 —— 把二次验证参数回传服务器【例如: login($.extend({}, params))】
  //       console.log('验证码匹配成功后的回调函数 params ====', params);
  //     },
  //     // 验证码匹配失败后的回调函数
  //     error: function () {},
  //   });
  // }
}

1.4 verify.js

这是验证码核心逻辑文件了,我直接保留了官网 Angular 示例中的源文件

值得注意的一段逻辑:

  • 通过 jq 控制验证码弹框的显隐,实现交互
  • 验证码只需要初始化一次,页面中就会被动态写入 .mask DOM 元素,如果多次注册,会导致页面中有多个 .mask DOM元素
  • 因此我们只初始化一次,后面通过 jq 控制 验证码相关的 DOM 显隐即可
if (this.options.mode == 'pop') {
	_this.$element.find('.verifybox-close').on('click', function () {
		_this.$element.find(".mask").css("display", "none");
	});
	let clickBtn = document.getElementById(this.options.containerId);
	clickBtn && (clickBtn.onclick = function () {
		if (_this.options.beforeCheck()) {
			_this.$element.find(".mask").css("display", "block");
		}
	})
}

?

?下面这段是说:点击某按钮后,会执行滑动校验方法,进行验证码弹框初始化

// 在插件中使用 slideVerify 对象
$.fn.slideVerify = function (options, callbacks) {
	var slide = new Slide(this, options);
	if (slide.options.mode == "pop") {
		slide.init();
	} else if (slide.options.mode == "fixed") {
		slide.init();
	}
};

?

全部代码如下,如果放在 Angular 项目里,强烈建议把这些换成 JavaScript 写,避免使用 jquery,由于我的时间有限,所以就暂时不改了

文件地址:src\app\components\verify\verify\verify.js

import * as jQuery from 'jquery';
import aesEncrypt from "./ase.js";

/*! Verify&admin MIT License by anji-plus*/
(function ($, window, document, undefined) {
	//请求图片get事件
	function getPictrue(data, baseUrl) {
		return new Promise((resolve, reject) => {
			$.ajax({
				type: "post",
				contentType: "application/json;charset=UTF-8",
				url: baseUrl + "/captcha/get",
				dataType: 'json', 
				data: JSON.stringify(data),
				success: function (res) {
					resolve(res)
				},
				fail: function (err) {
					reject(err)
				}
			})
		})
	}
	//验证图片check事件
	function checkPictrue(data, baseUrl) {
		return new Promise((resolve, reject) => {
			$.ajax({
				type: "post",
				contentType: "application/json;charset=UTF-8",
				url: baseUrl + "/captcha/check",
				dataType: 'json', 
				data: JSON.stringify(data),
				success: function (res) {
					if (res.repCode == "0000") {
						resolve(res)
						setTimeout(function () {
							$('#click').hide(true)
							$('#slipping').hide(true)
							$(".modal-backdrop").css('display', 'none');
						}, 1000);
					}
					resolve(res)
				},
				fail: function (err) {
					reject(err)
				}
			})
		})
	}
	//定义Slide的构造函数
	var Slide = function (ele, opt) {
		this.$element = ele,
			this.backToken = null,
			this.moveLeftDistance = 0,
			this.secretKey = '',
			this.defaults = {
				baseUrl: "https://captcha.anji-plus.com/captcha-api",
				containerId: '',
				captchaType: "blockPuzzle",
				mode: 'fixed',	//弹出式pop,固定fixed
				vOffset: 5,
				vSpace: 5,
				explain: '向右滑动完成验证',
				imgSize: {
					width: '310px',
					height: '155px',
				},
				blockSize: {
					width: '50px',
					height: '50px',
				},
				circleRadius: '10px',
				barSize: {
					width: '310px',
					height: '50px',
				},
				beforeCheck() { return true },
				ready: function () { },
				success: function () { },
				error: function () { }

			},
			this.options = $.extend({}, this.defaults, opt)
	};
	//定义Slide的方法
	Slide.prototype = {
		init: function () {
			var _this = this;
			//加载页面
			this.loadDom();
			_this.refresh();
			this.options.ready();

			this.$element[0].onselectstart = document.body.ondrag = function () {
				return false;
			};

			if (this.options.mode == 'pop') {

				_this.$element.find('.verifybox-close').on('click', function () {
					_this.$element.find(".mask").css("display", "none");
					_this.refresh();
				});

				let clickBtn = document.getElementById(this.options.containerId);
				clickBtn && (clickBtn.onclick = function () {
					if (_this.options.beforeCheck()) {
						// debugger
						_this.$element.find(".mask").css("display", "block");
					}
				})
			}
			//按下
			this.htmlDoms.move_block.on('touchstart', function (e) {
				_this.start(e);
			});
			this.htmlDoms.move_block.on('mousedown', function (e) {
				_this.start(e);
			});
			//拖动
			window.addEventListener("touchmove", function (e) {
				_this.move(e);
			});
			window.addEventListener("mousemove", function (e) {
				_this.move(e);
			});
			//鼠标松开
			window.addEventListener("touchend", function () {
				_this.end();
			});
			window.addEventListener("mouseup", function () {
				_this.end();
			});
			//刷新
			_this.$element.find('.verify-refresh').on('click', function () {
				_this.refresh();
			});
		},
		//初始化加载
		loadDom: function () {
			this.status = false;	//鼠标状态
			this.isEnd = false;		//是够验证完成
			this.setSize = this.resetSize(this);	//重新设置宽度高度
			this.plusWidth = 0;
			this.plusHeight = 0;
			this.x = 0;
			this.y = 0;
			var panelHtml = '';
			var wrapHtml = '';
			var wrapStartHtml = '';
			var wrapEndHtml = '';
			this.lengthPercent = (parseInt(this.setSize.img_width) - parseInt(this.setSize.block_width) - parseInt(this.setSize.circle_radius) - parseInt(this.setSize.circle_radius) * 0.8) / (parseInt(this.setSize.img_width) - parseInt(this.setSize.bar_height));
			wrapStartHtml = `<div class="mask">
								<div class="verifybox" style="width:${parseInt(this.setSize.img_width) + 30}px">
									<div class="verifybox-top">
										请完成安全验证
										<span class="verifybox-close">
											<i class="iconfont icon-close"></i>
										</span>
									</div>
									<div class="verifybox-bottom" style="padding:15px">
										<div style="position: relative;">`

			if (this.options.mode == 'pop') {
				panelHtml = wrapStartHtml
			}
			panelHtml += `<div class="verify-img-out">
							<div class="verify-img-panel">
								<div class="verify-refresh" style="z-index:3">
									<i class="iconfont icon-refresh"></i>
								</div>
								<span class="verify-tips"  class="suc-bg"></span>
								<img src="" class="backImg" style="width:100%;height:100%;display:block">
							</div>
						</div>`;
			this.plusWidth = parseInt(this.setSize.block_width) + parseInt(this.setSize.circle_radius) * 2 - parseInt(this.setSize.circle_radius) * 0.2;
			this.plusHeight = parseInt(this.setSize.block_height) + parseInt(this.setSize.circle_radius) * 2 - parseInt(this.setSize.circle_radius) * 0.2;

			panelHtml += `<div class="verify-bar-area" style="{width:${this.setSize.img_width},height:${this.setSize.bar_height},'line-height':${this.setSize.bar_height}">
									<span  class="verify-msg">${this.options.explain}</span>
									<div class="verify-left-bar">
										<span class="verify-msg"></span>
										<div  class="verify-move-block">
											<i  class="verify-icon iconfont icon-right"></i>
											<div class="verify-sub-block">
												<img src="" class="bock-backImg" alt=""  style="width:100%;height:100%;display:block">
											</div>
										</div>
									</div>
								</div>`;
			wrapEndHtml = `</div></div></div></div>`
			if (this.options.mode == 'pop') {
				panelHtml += wrapEndHtml
			}
			this.$element.append(panelHtml);
			this.htmlDoms = {
				tips: this.$element.find('.verify-tips'),
				sub_block: this.$element.find('.verify-sub-block'),
				out_panel: this.$element.find('.verify-img-out'),
				img_panel: this.$element.find('.verify-img-panel'),
				img_canvas: this.$element.find('.verify-img-canvas'),
				bar_area: this.$element.find('.verify-bar-area'),
				move_block: this.$element.find('.verify-move-block'),
				left_bar: this.$element.find('.verify-left-bar'),
				msg: this.$element.find('.verify-msg'),
				icon: this.$element.find('.verify-icon'),
				refresh: this.$element.find('.verify-refresh')
			};
			this.$element.css('position', 'relative');
			this.htmlDoms.sub_block.css({
				'height': this.setSize.img_height, 'width': Math.floor(parseInt(this.setSize.img_width) * 47 / 310) + 'px',
				'top': -(parseInt(this.setSize.img_height) + this.options.vSpace) + 'px'
			})
			this.htmlDoms.out_panel.css('height', parseInt(this.setSize.img_height) + this.options.vSpace + 'px');
			this.htmlDoms.img_panel.css({ 'width': this.setSize.img_width, 'height': this.setSize.img_height });
			this.htmlDoms.bar_area.css({ 'width': this.setSize.img_width, 'height': this.setSize.bar_height, 'line-height': this.setSize.bar_height });
			this.htmlDoms.move_block.css({ 'width': this.setSize.bar_height, 'height': this.setSize.bar_height });
			this.htmlDoms.left_bar.css({ 'width': this.setSize.bar_height, 'height': this.setSize.bar_height });
		},
		//鼠标按下
		start: function (e) {
			if (!e.touches) {    //兼容移动端
				var x = e.clientX;
			} else {     //兼容PC端
				var x = e.touches[0].pageX;
			}
			this.startLeft = Math.floor(x - this.htmlDoms.bar_area[0].getBoundingClientRect().left);
			this.startMoveTime = new Date().getTime();
			if (this.isEnd == false) {
				this.htmlDoms.msg.text('');
				this.htmlDoms.move_block.css('background-color', '#337ab7');
				this.htmlDoms.left_bar.css('border-color', '#337AB7');
				this.htmlDoms.icon.css('color', '#fff');
				e.stopPropagation();
				this.status = true;
			}
		},
		//鼠标移动
		move: function (e) {
			if (this.status && this.isEnd == false) {
				if (!e.touches) {    //兼容移动端
					var x = e.clientX;
				} else {     //兼容PC端
					var x = e.touches[0].pageX;
				}
				var bar_area_left = this.htmlDoms.bar_area[0].getBoundingClientRect().left;
				var move_block_left = x - bar_area_left; //小方块相对于父元素的left值
				if (move_block_left >= (this.htmlDoms.bar_area[0].offsetWidth - parseInt(this.setSize.bar_height) + parseInt(parseInt(this.setSize.block_width) / 2) - 2)) {
					move_block_left = (this.htmlDoms.bar_area[0].offsetWidth - parseInt(this.setSize.bar_height) + parseInt(parseInt(this.setSize.block_width) / 2) - 2);
				}
				if (move_block_left <= parseInt(parseInt(this.setSize.block_width) / 2)) {
					move_block_left = parseInt(parseInt(this.setSize.block_width) / 2);
				}
				//拖动后小方块的left值
				this.htmlDoms.move_block.css('left', move_block_left - this.startLeft + "px");
				this.htmlDoms.left_bar.css('width', move_block_left - this.startLeft + "px");
				this.htmlDoms.sub_block.css('left', "0px");
				this.moveLeftDistance = move_block_left - this.startLeft
			}
		},
		//鼠标松开
		end: function () {
			this.endMovetime = new Date().getTime();
			var _this = this;
			//判断是否重合
			if (this.status && this.isEnd == false) {
				var vOffset = parseInt(this.options.vOffset);
				this.moveLeftDistance = this.moveLeftDistance * 310 / parseInt(this.setSize.img_width)
				//图片滑动

				let data = {
					captchaType: this.options.captchaType,
					"pointJson": this.secretKey ? aesEncrypt(JSON.stringify({ x: this.moveLeftDistance, y: 5.0 }), this.secretKey) : JSON.stringify({ x: this.moveLeftDistance, y: 5.0 }),
					"token": this.backToken
				}
				var captchaVerification = this.secretKey ? aesEncrypt(this.backToken + '---' + JSON.stringify({ x: this.moveLeftDistance, y: 5.0 }), this.secretKey) : this.backToken + '---' + JSON.stringify({ x: this.moveLeftDistance, y: 5.0 })
				checkPictrue(data, this.options.baseUrl).then(res => {
					// 请求反正成功的判断
					if (res.repCode == "0000") {
						this.htmlDoms.move_block.css('background-color', '#5cb85c');
						this.htmlDoms.left_bar.css({ 'border-color': '#5cb85c', 'background-color': '#fff' });
						this.htmlDoms.icon.css('color', '#fff');
						this.htmlDoms.icon.removeClass('icon-right');
						this.htmlDoms.icon.addClass('icon-check');
						//提示框
						this.htmlDoms.tips.addClass('suc-bg').removeClass('err-bg')
						this.htmlDoms.tips.css({ "display": "block", animation: "move 1s cubic-bezier(0, 0, 0.39, 1.01)" });
						this.htmlDoms.tips.text(`${((this.endMovetime - this.startMoveTime) / 1000).toFixed(2)}s验证成功`)
						this.isEnd = true;
						setTimeout(res => {
							_this.$element.find(".mask").css("display", "none");
							this.htmlDoms.tips.css({ "display": "none", animation: "none" });
							_this.refresh();
						}, 1000)
						this.options.success({ captchaVerification });
					} else {
						this.htmlDoms.move_block.css('background-color', '#d9534f');
						this.htmlDoms.left_bar.css('border-color', '#d9534f');
						this.htmlDoms.icon.css('color', '#fff');
						this.htmlDoms.icon.removeClass('icon-right');
						this.htmlDoms.icon.addClass('icon-close');

						this.htmlDoms.tips.addClass('err-bg').removeClass('suc-bg')
						this.htmlDoms.tips.css({ "display": "block", animation: "move 1.3s cubic-bezier(0, 0, 0.39, 1.01)" });
						this.htmlDoms.tips.text(res.repMsg)
						setTimeout(function () {
							_this.refresh();
						}, 400);

						setTimeout(() => {
							this.htmlDoms.tips.css({ "display": "none", animation: "none" });
						}, 1300)
						this.options.error(this);
					}
				})
				this.status = false;
			}
		},
		resetSize: function (obj) {
			var img_width, img_height, bar_width, bar_height, block_width, block_height, circle_radius;	//图片的宽度、高度,移动条的宽度、高度
			var parentWidth = obj.$element.parent().width() || $(window).width();
			var parentHeight = obj.$element.parent().height() || $(window).height();

			if (obj.options.imgSize.width.indexOf('%') != -1) {
				img_width = parseInt(obj.options.imgSize.width) / 100 * parentWidth + 'px';
			} else {
				img_width = obj.options.imgSize.width;
			}

			if (obj.options.imgSize.height.indexOf('%') != -1) {
				img_height = parseInt(obj.options.imgSize.height) / 100 * parentHeight + 'px';
			} else {
				img_height = obj.options.imgSize.height;
			}
			if (obj.options.barSize.width.indexOf('%') != -1) {
				bar_width = parseInt(obj.options.barSize.width) / 100 * parentWidth + 'px';
			} else {
				bar_width = obj.options.barSize.width;
			}
			if (obj.options.barSize.height.indexOf('%') != -1) {
				bar_height = parseInt(obj.options.barSize.height) / 100 * parentHeight + 'px';
			} else {
				bar_height = obj.options.barSize.height;
			}
			if (obj.options.blockSize) {
				if (obj.options.blockSize.width.indexOf('%') != -1) {
					block_width = parseInt(obj.options.blockSize.width) / 100 * parentWidth + 'px';
				} else {
					block_width = obj.options.blockSize.width;
				}
				if (obj.options.blockSize.height.indexOf('%') != -1) {
					block_height = parseInt(obj.options.blockSize.height) / 100 * parentHeight + 'px';
				} else {
					block_height = obj.options.blockSize.height;
				}
			}
			if (obj.options.circleRadius) {
				if (obj.options.circleRadius.indexOf('%') != -1) {
					circle_radius = parseInt(obj.options.circleRadius) / 100 * parentHeight + 'px';
				} else {
					circle_radius = obj.options.circleRadius;
				}
			}
			return { img_width: img_width, img_height: img_height, bar_width: bar_width, bar_height: bar_height, block_width: block_width, block_height: block_height, circle_radius: circle_radius };
		},
		//刷新
		refresh: function () {
			var _this = this;
			this.htmlDoms.refresh.show();
			this.$element.find('.verify-msg:eq(1)').text('');
			this.$element.find('.verify-msg:eq(1)').css('color', '#000');
			this.htmlDoms.move_block.animate({ 'left': '0px' }, 'fast');
			this.htmlDoms.left_bar.animate({ 'width': parseInt(this.setSize.bar_height) }, 'fast');
			this.htmlDoms.left_bar.css({ 'border-color': '#ddd' });
			this.htmlDoms.move_block.css('background-color', '#fff');
			this.htmlDoms.icon.css('color', '#000');
			this.htmlDoms.icon.removeClass('icon-close');
			this.htmlDoms.icon.addClass('icon-right');
			this.$element.find('.verify-msg:eq(0)').text(this.options.explain);
			this.isEnd = false;
			getPictrue({ captchaType: "blockPuzzle" }, this.options.baseUrl).then(res => {
				if (res.repCode == "0000") {
					this.$element.find(".backImg")[0].src = 'data:image/png;base64,' + res.repData.originalImageBase64
					this.$element.find(".bock-backImg")[0].src = 'data:image/png;base64,' + res.repData.jigsawImageBase64
					this.secretKey = res.repData.secretKey
					this.backToken = res.repData.token
				}
			});
			this.htmlDoms.sub_block.css('left', "0px");
		},
	};
	//定义Points的构造函数
	var Points = function (ele, opt) {
		this.$element = ele,
			this.backToken = null,
			this.secretKey = '',
			this.defaults = {
				baseUrl: "https://captcha.anji-plus.com/captcha-api",
				captchaType: "clickWord",
				containerId: '',
				mode: 'fixed',	//弹出式pop,固定fixed
				checkNum: 3,	//校对的文字数量
				vSpace: 5,	//间隔
				imgSize: {
					width: '310px',
					height: '155px',
				},
				barSize: {
					width: '310px',
					height: '50px',
				},
				beforeCheck() { return true },
				ready: function () { },
				success: function () { },
				error: function () { }
			},
			this.options = $.extend({}, this.defaults, opt)
	};
	//定义Points的方法
	Points.prototype = {
		init: function () {
			var _this = this;
			//加载页面
			_this.loadDom();

			_this.refresh();
			_this.options.ready();

			this.$element[0].onselectstart = document.body.ondrag = function () {
				return false;
			};

			if (this.options.mode == 'pop') {

				_this.$element.find('.verifybox-close').on('click', function () {
					_this.$element.find(".mask").css("display", "none");
				});

				let clickBtn = document.getElementById(this.options.containerId);
				clickBtn && (clickBtn.onclick = function () {
					if (_this.options.beforeCheck()) {
						_this.$element.find(".mask").css("display", "block");
					}
				})
			}
			// 注册点击验证事件
			_this.$element.find('.back-img').on('click', function (e) {

				_this.checkPosArr.push(_this.getMousePos(this, e));

				if (_this.num == _this.options.checkNum) {
					_this.num = _this.createPoint(_this.getMousePos(this, e));
					//按比例转换坐标值
					_this.checkPosArr = _this.pointTransfrom(_this.checkPosArr, _this.setSize);
					setTimeout(() => {
						let data = {
							captchaType: _this.options.captchaType,
							"pointJson": _this.secretKey ? aesEncrypt(JSON.stringify(_this.checkPosArr), _this.secretKey) : JSON.stringify(_this.checkPosArr),
							"token": _this.backToken
						}
						var captchaVerification = _this.secretKey ? aesEncrypt(_this.backToken + '---' + JSON.stringify(_this.checkPosArr), _this.secretKey) : _this.backToken + '---' + JSON.stringify(_this.checkPosArr)
						checkPictrue(data, _this.options.baseUrl).then(res => {
							if (res.repCode == "0000") {
								_this.$element.find('.verify-bar-area').css({ 'color': '#4cae4c', 'border-color': '#5cb85c' });
								_this.$element.find('.verify-msg').text('验证成功');
								// _this.$element.find('.verify-refresh').hide();
								_this.$element.find('.verify-img-panel').unbind('click');
								setTimeout(res => {
									_this.$element.find(".mask").css("display", "none");
									_this.refresh();
								}, 1000)
								_this.options.success({ captchaVerification });
							} else {
								_this.options.error(_this);
								_this.$element.find('.verify-bar-area').css({ 'color': '#d9534f', 'border-color': '#d9534f' });
								_this.$element.find('.verify-msg').text('验证失败');
								setTimeout(function () {
									_this.$element.find('.verify-bar-area').css({ 'color': '#000', 'border-color': '#ddd' });
									_this.refresh();
								}, 400);
							}
						})
					}, 400);
				}
				if (_this.num < _this.options.checkNum) {
					_this.num = _this.createPoint(_this.getMousePos(this, e));
				}
			});

			//刷新
			_this.$element.find('.verify-refresh').on('click', function () {
				_this.refresh();
			});
		},
		//加载页面
		loadDom: function () {
			this.fontPos = [];	//选中的坐标信息
			this.checkPosArr = [];	//用户点击的坐标
			this.num = 1;	//点击的记数
			var panelHtml = '';
			var wrapStartHtml = '';
			var wrapEndHtml = '';
			this.setSize = Slide.prototype.resetSize(this);	//重新设置宽度高度
			wrapStartHtml = `<div class="mask">
								<div class="verifybox" style="width:${parseInt(this.setSize.img_width) + 30}px">
									<div class="verifybox-top">
										请完成安全验证
										<span class="verifybox-close">
											<i class="iconfont icon-close"></i>
										</span>
									</div>
									<div class="verifybox-bottom" style="padding:15px">
										<div style="position: relative;">`

			if (this.options.mode == 'pop') {
				panelHtml = wrapStartHtml
			}
			panelHtml += `<div class="verify-img-out">
							<div class="verify-img-panel">
								<div class="verify-refresh" style="z-index:3">
									<i class="iconfont icon-refresh"></i>
								</div>
								<img src='' class="back-img" width="${this.setSize.img_width}" height="${this.setSize.img_height}">
							</div>
						</div>
						<div class="verify-bar-area" style="{width:${this.setSize.img_width},height:${this.setSize.bar_height},'line-height':${this.setSize.bar_height}">
							<span  class="verify-msg"></span>
					</div>`;
			wrapEndHtml = `</div></div></div></div>`
			if (this.options.mode == 'pop') {
				panelHtml += wrapEndHtml
			}
			this.$element.append(panelHtml);

			this.htmlDoms = {
				back_img: this.$element.find('.back-img'),
				out_panel: this.$element.find('.verify-img-out'),
				img_panel: this.$element.find('.verify-img-panel'),
				bar_area: this.$element.find('.verify-bar-area'),
				msg: this.$element.find('.verify-msg'),
			};
			this.$element.css('position', 'relative');

			this.htmlDoms.out_panel.css('height', parseInt(this.setSize.img_height) + this.options.vSpace + 'px');
			this.htmlDoms.img_panel.css({ 'width': this.setSize.img_width, 'height': this.setSize.img_height, 'background-size': this.setSize.img_width + ' ' + this.setSize.img_height, 'margin-bottom': this.options.vSpace + 'px' });
			this.htmlDoms.bar_area.css({ 'width': this.setSize.img_width, 'height': this.setSize.bar_height, 'line-height': this.setSize.bar_height });

		},
		//获取坐标
		getMousePos: function (obj, event) {
			var e = event || window.event;
			var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft;
			var scrollY = document.documentElement.scrollTop || document.body.scrollTop;
			var x = e.clientX - ($(obj).offset().left - $(window).scrollLeft());
			var y = e.clientY - ($(obj).offset().top - $(window).scrollTop());

			return { 'x': x, 'y': y };
		},
		//创建坐标点
		createPoint: function (pos) {
			this.htmlDoms.img_panel.append(`<div class="point-area" style="background-color:#1abd6c;color:#fff;z-index:9999;width:20px;height:20px;text-align:center;line-height:20px;border-radius: 50%;position:absolute;
			   										top:${parseInt(pos.y - 10)}px;left:${parseInt(pos.x - 10)}px;">${this.num}</div>`);
			return ++this.num;
		},
		//刷新
		refresh: function () {
			var _this = this;
			this.$element.find('.point-area').remove();
			this.fontPos = [];
			this.checkPosArr = [];
			this.num = 1;
			getPictrue({ captchaType: "clickWord" }, _this.options.baseUrl).then(res => {
				if (res.repCode == "0000") {
					this.htmlDoms.back_img[0].src = 'data:image/png;base64,' + res.repData.originalImageBase64
					this.backToken = res.repData.token
					this.secretKey = res.repData.secretKey
					let text = '请依次点击【' + res.repData.wordList.join(",") + '】'
					_this.$element.find('.verify-msg').text(text);
				}
			})

		},
		pointTransfrom: function (pointArr, imgSize) {
			var newPointArr = pointArr.map(p => {
				let x = Math.round(310 * p.x / parseInt(imgSize.img_width))
				let y = Math.round(155 * p.y / parseInt(imgSize.img_height))
				return { x, y }
			})
			return newPointArr
		}
	};
	//在插件中使用slideVerify对象
	$.fn.slideVerify = function (options, callbacks) {
		var slide = new Slide(this, options);
		if (slide.options.mode == "pop") {
			slide.init();
		} else if (slide.options.mode == "fixed") {
			slide.init();
		}
	};
	//在插件中使用clickVerify对象
	$.fn.pointsVerify = function (options, callbacks) {
		var points = new Points(this, options);
		if (points.options.mode == "pop") {
			points.init();
		} else if (points.options.mode == "fixed") {
			points.init();
		}
	};
})(jQuery, window, document);

?

1.5?aes-encrypt-decrypt.ts

AES 加密方法

需要注意 密钥、偏移量 都是可以动态传入的哦

文件地址:src\app\components\verify\verify\ase-encrypt-decrypt.ts

import { enc, mode, AES, pad } from 'crypto-js';

// 加密密钥(长度必须是 16 的整数倍,此处为 32 位)
const secretKey = '5405xxxx778e38acbxxxxxxx002fb4ce';
// 偏移量
const iv = 'xxxxxxxx';

/**
 * AES加密
 * @description 使用加密秘钥,对 需要加密的参数 进行加密
 * @param {string} word - 需要加密的参数
 * @param {string} key - 加密密钥(长度必须是 16 的整数倍)
 * @param {string} offset - 偏移量
 */
export function aseEncryptParams(word: any, key = secretKey, offset = iv) {
  // 未加密的参数 - 从 UTF-8编码 解析出原始字符串
  const wordUTF8 = enc.Utf8.parse(word);
  // 密钥 - 从 UTF-8编码 解析出原始字符串
  const keyUTF8 = enc.Utf8.parse(key);
  // 偏移量(在此公司内是固定的) - 从 UTF-8编码 解析出原始字符串
  const offsetUTF8 = enc.Utf8.parse(offset);

  // 补充
  // 把字符串转成 UTF-8编码 —— enc.Utf8.stringify(word);

  const encrypted = AES.encrypt(wordUTF8, keyUTF8, {
    iv: offsetUTF8,
    mode: mode.CBC,
    padding: pad.Pkcs7,
  });

  return encrypted.toString();
}

/**
 * AES解密
 * @description 使用加密秘钥,对 需要解密的参数 进行解密
 * @param {string} encryptedWord - 需要解密的参数
 * @param {string} key - 加密密钥(长度必须是 16 的整数倍)
 * @param {string} offset - 偏移量
 */
export function aesDecryptParams(
  encryptedWord: any,
  key = secretKey,
  offset = iv
) {
  // 密钥 - 从 UTF-8编码 解析出原始字符串
  const keyUTF8 = enc.Utf8.parse(key);
  // 偏移量(在此公司内是固定的) - 从 UTF-8编码 解析出原始字符串
  const offsetUTF8 = enc.Utf8.parse(offset);

  const bytes = AES.decrypt(encryptedWord, keyUTF8, {
    iv: offsetUTF8,
    mode: mode.CBC,
    padding: pad.Pkcs7,
  });

  return bytes.toString(enc.Utf8);
}

1.6?verify.service.ts

这是验证码校验相关的接口逻辑,基本 cv 了官网 Angular 示例项目

包含了两个方法:

  • 获取验证码图片
  • 校验验证码是否拼接正确

修改的地方:把默认的接口地址,改成了公司后台给的验证码校验接口地址

文件地址:src\app\service\verify.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { interfaceId } from '../types/interfaceId';

@Injectable()
export class VerifyService {
  // 设置验证地址
  private VerifyesUrl = `${interfaceId.getSecurityKeyJWT}`;

  constructor(private http: HttpClient) {}

  /**
   * 请求图片 - get事件
   * @returns
   */
  getVerify(params: any): Promise<any> {
    return this.http
      .post(this.VerifyesUrl + '/captcha/get', params, {
        responseType: 'json',
      })
      .toPromise()
      .then((response: any) => response)
      .catch(this.handleError);
  }

  /**
   * 验证图片 - check事件
   * @returns
   */
  getVerifyCheck(params: any): Promise<any> {
    return (
      this.http
        .post(this.VerifyesUrl + '/captcha/check', params, {
          responseType: 'json',
        })
        .toPromise()
        // .then((response: any) => response.json())
        .then((response: any) => response)
        .catch(this.handleError)
    );
  }

  /**
   * 异常处理
   * @returns
   */
  private handleError(error: any): Promise<any> {
    console.error('滑动校验验证码接口请求发生错误', error);
    return Promise.reject(error.message || error);
  }
}

2. 添加登录时的验证码校验逻辑

2.1 保持 loginService 服务为单例,避免路由守卫出 bug

login.service.ts 登录相关的服务

需要注意以下代码,全局注入了登录服务

@Injectable({
  providedIn: 'root',
})

在任何组件的 module.ts 中,都不应该重复注入 loginService 服务,这会导致 loginService 服务非单例,项目中的路由守卫就容易出现问题

可以看见,下面的代码里判断了登陆服务内的 loginOn,如果登录了,则进行跳转,否则仍然停留在登录页

	private checkLogin(url: string): boolean {
		// 登录成功后,修改单例模式中的登录标志,进而阻止路由守卫
		if (this.loginService.loginOn) {
			return true;
		}
		this.loginService.url = url;
		this.router.navigate([ 'login' ]);
		return false;
	}

如果我再 login.module.ts 中,还要再依赖注入一次 loginService,就会导致整个项目中有两个 loginService 实例

那么你即使修改了一个 loginOn,另一个实例中的l oginOn 没有修改,也会导致 路由守卫持续拦截

跳转到应用正式页面时,有没有路由守卫,可以看路由文件

文件地址:src\app\app-routing.module.ts

{
	path: 'tabs',
	loadChildren: () => import('./tabs/tabs.module').then((m) => m.TabsPageModule),
	canActivate: [ LoginGuard ],
	canActivateChild: [ LoginGuard ],
},

解决这个问题踩了这两个坑,汇总下:

  • 路由跳转失败,要考虑是不是路由守卫导致的,可以从路由配置文件里看
  • 不要重复引入 loginService 服务,让他保持单例,防止路由守卫误判

2.2 获取密钥时,Angular Http 请求发送失败?

https://www.jianshu.com/p/64e10c705eb9?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendationicon-default.png?t=M666https://www.jianshu.com/p/64e10c705eb9?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

解决方案:添加响应类型 responseType,如下代码所示

  /**
   * 获取包含AES加密秘钥 securityKey 的 securityKeyJWT
   */
  getSecurityKeyJWT(loginName: string) {
    return this.http.get<any>(
      `${interfaceId.getSecurityKeyJWT}/${loginName}/authkey`,
      {
        // Http failure during parsing
        responseType: 'text',
      } as any
    );
  }

2.3 点击登录按钮,初始化验证码

在?login.module.ts 文件中,需要引入滑动验证码组件、验证码服务(再次强调,不要引入登录服务,原因见 2.1)

// 验证码
import { verifySlippingComponent } from '../../components/verify/verifySlipping.component';
// 验证服务(一定不要引入登录服务,因为登陆服务已经全局注入了,为了避免产生多个实例,导致修改登录状态失败,路由守卫跳转失败的问题)
import { VerifyService } from '../../service/verify.service';

@NgModule({
  declarations: [LoginComponent, LoginIframeComponent, verifySlippingComponent],
  providers: [VerifyService],
})

在?login.component.html 文件中,需要引入滑动验证码组件,需要给登录按钮添加指定ID

添加 id、data-target 都是为了保持跟 Angular 官网示例一致,如果把这些ID名改了,你还要修改 verify.js 中的变量名,非常费劲,因此强烈建议与官网示例保持一致

<!-- 登录按钮 -->
<i id="btn" data-target="#slipping" class="submit" (click)="clickLoginBtn()"></i>

<!-- 滑动验证 -->
<verify-slipping></verify-slipping>

点击按钮之后,要执行的逻辑:

  • 判断输入参数合法性
  • 根据用户名获取包含AES加密秘钥 securityKey 的 securityKeyJWT
  • 解析 securityKeyJWT,获取 aes 加密秘钥 securityKey
  • 根据加密密钥,初始化验证码
  • 始化完成后,使用用户名和加密后的密码,调用认证服务

需要注意全局定义一个?loadFinished ,用于标识验证码是否已经初始化过了

  • 如果没初始化,则走初始化验证码的方法,并控制验证码弹窗显示(只能初始化一次)
  • 如果初始化了,控制显示验证码弹框,验证码组件内部会监听到,走校验验证码并登录的方法

无论走哪一步,都需要操作 DOM,控制验证码弹窗显隐

// 初始化完成后,展示验证码弹窗

$('.mask').css('display', 'block');

// 直接展示验证码弹窗

$('.mask').css('display', 'block');

$('#slipping').css('display', 'block');

$('.modal-backdrop').css('display', 'block');

下面是点击登录按钮后,执行的方法?

  /**
   * 点击登录按钮
   * @description 判断输入参数合法性
   * @description 根据用户名获取包含AES加密秘钥 securityKey 的 securityKeyJWT
   * @description 解析 securityKeyJWT,获取 aes 加密秘钥 securityKey
   * @description 初始化验证码
   * @description 始化完成后,使用用户名和加密后的密码,调用认证服务
   */
  async clickLoginBtn() {
    // 判空处理
    this.controlOption.type = 'password';
    this.toolsService.hiddenBottomTabs = false;
    for (const i of Object.keys(this.formData.controls)) {
      this.formData.controls[i].markAsDirty();
      this.formData.controls[i].updateValueAndValidity();
    }
    // 如果表单符合规则
    if (this.formData.valid) {
      // console.log('this.formData.value ===', this.formData.value)
      // 根据用户名获取包含AES加密秘钥 securityKey 的 securityKeyJWT
      this.loginService
        .getSecurityKeyJWT(this.formData.value.loginName)
        .subscribe(async (res) => {
          // console.log(
          //   '根据用户名获取包含AES加密秘钥 securityKey 的 securityKeyJWT ===',
          //   res
          // );

          // 解析 securityKeyJWT,获取 aes 加密秘钥 securityKey
          const { securityKey } = jwtDecode(res as any) as any;
          // console.log(
          //   '解析 securityKeyJWT,获取 aes 加密秘钥 securityKey ===',
          //   securityKey
          // );

          // 如果没有初始化验证码,则先初始化
          if (!this.loadFinished) {
            console.log('初始化验证码');
            // 初始化验证码
            await this.initVerifyCodeImage(securityKey, res);
            // 初始化完成后,展示验证码弹窗
            $('.mask').css('display', 'block');
            // 验证码是否加载成功
            this.loadFinished = true;
          } else {
            console.log('已经初始化过验证码了,直接认证');
            // 直接展示验证码弹窗
            $('.mask').css('display', 'block');
            $('#slipping').css('display', 'block');
            $('.modal-backdrop').css('display', 'block');
          }
        });
    }
  }

2.4 初始化验证码

初始化验证码是官网提供的方法,里面存放了很多配置

由于 个别配置 是个回调函数,需要调用当前文件的一些方法,this 指向会存在问题,因此方法内第一行代码就是存储下 this

beforeCheck:会对登录参数进行校验,因为我再点击按钮的时候就已经校验了,因此此处直接 return true

验证码校验成功后,接口会返回一个 params 里面包含接口 二次验证 需要的参数,因此,我把这个参数存到全局里了,也就是?that.paramsFirstSuccess

注意:我尝试过使用 JavaScript 直接获取?#mpanel1,但是这会导致无法调用?slideVerify 方法初始化验证码,因此我还是继续采用 jquery 获取 DOM 元素

  /**
   * 初始化验证码
   * @description 初始化完成后,使用用户名和加密后的密码,调用认证服务
   * @param securityKey aes 加密秘钥 securityKey
   * @param securityKeyJWT 根据用户名获取包含AES加密秘钥 securityKey 的 securityKeyJWT
   */
  initVerifyCodeImage(securityKey, securityKeyJWT) {
    const that: any = this;

    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
    (<any>$('#mpanel1')).slideVerify({
      // 请求后端的服务器地址, 默认地址为安吉服务器
      baseUrl: `${interfaceId.getSecurityKeyJWT}`,
      // 验证码的显示方式,弹出式 - pop,固定 - fixed,默认为 mode: 'pop'
      mode: 'pop',
      // 被点击之后出现行为验证码的元素id(mode 为 pop 模式时必填)
      containerId: 'btn',
      // 图片的大小对象, 有默认值 { width: '310px', height: '155px' }, 可省略
      imgSize: {
        width: '400px',
        height: '200px',
      },
      // 下方滑块的大小对象, 有默认值 { width: '310px', height: '50px' }, 可省略
      barSize: {
        width: '400px',
        height: '40px',
      },
      beforeCheck: () => {
        return true;
      },
      // 验证码匹配成功后的回调函数
      success: (params: any) => {
        // console.log('that', that)
        // console.log('验证码匹配成功后的回调函数 params ====', params);

        // 首次 验证码匹配成功后的回调函数 params
        that.paramsFirstSuccess = params;
        // 使用用户名和加密后的密码,调用认证服务
        that.handleAuth(securityKey, securityKeyJWT);
      },
      // 验证码匹配失败后的回调函数
      error: () => {
        console.log('验证码匹配失败后的回调函数');
      },
    });
  }

?

2.5?验证码校验成功后 执行真正的登录

真正的登录,请求地址是拼接出来的,包含了:

  • 登录名
  • 加密密码
  • 密钥
  • 验证码成功后接口返回的二次校验参数
  /**
   * 使用用户名和加密后的密码,调用认证服务
   * @description 此处执行了真正的登录
   * @param securityKey aes 加密秘钥 securityKey
   * @param securityKeyJWT 根据用户名获取包含AES加密秘钥 securityKey 的 securityKeyJWT
   */
  async handleAuth(securityKey, securityKeyJWT) {
    // 使用 ASE加密秘钥 对 用户密码 进行加密
    const encryptionPassword = aseEncryptParams(
      this.formData.value.password,
      securityKey
    );
    // eslint-disable-next-line max-len
    const url = `${interfaceId....cation}`;
    this.loginService
      .loginLast(
        url,
        this.formData.value,
        this.controlOption.remember,
        this.controlOption.autoLogin
      )
      .subscribe((res) => {
        console.log('使用用户名和加密后的密码,调用认证服务 ===', res);
        this.loginFinal(res);
      });
  }

?

真正的登录接口,在返回响应之前,需要进行一些额外的操作:

  • 权限验证,根据用户信息,决定跳转的路由,路由存在 loginService 里
  • 开启极光推送服务
  • 修改全局登录状态
  • 存储用户信息、记住密码、自动登录

可以通过管道 pipe 处理响应,处理完了在返回前台

注意此处,指定接口返回类型 json(后端要求的)

loginLast(
  url: string,
  data: { loginName: string; password: string },
  remember: boolean,
  autoLogin: boolean
): any {
  return this.http
    .get(url, {
      responseType: 'json',
    })
    .pipe(
      tap(async (res: any) => {
        if (res.message === '登录成功') {
          // 权限验证,根据用户信息,决定跳转的路由,路由存在 loginService 里

          // 开启极光推送服务
          this.jPushAddEventListener(res.user.userId);

          // 修改全局登录状态
          this.loginOn = true;

          // 存储用户信息、记住密码、自动登录
      }),
      map((res: any) => res)
    );
}

登录成功后,根据 pipe 处理过的接口响应信息,进行跳转

this.router.navigate([this.loginService.url]);

2.6 效果展示

输入用户名密码,校验验证码

验证码校验成功后,正儿八经登录,验证成功后,根据不同用户跳转到不同首页

?

二. 修改密码功能

1. 进入页面后,加密登录名,获取新密钥

  /**
   * @description:通过加密登录名,获取的新密钥
   * @return
   */
  async getNewSecurityKeyByLoginName() {
    // 如果没有登录名 或者没有 用户ID,则无法修改密码
    if (!this.formData.value.loginName || !this.formData.value.userId) {
      // 判断是否可以修改密码
      this.canEditPwd = false;
      this.toolSerive.presentToastWithOptions(
        '缺少登录名或者用户ID,无法修改密码'
      );
      return;
    }

    // 加密登录名
    const aesLoginName = aseEncryptParams(
      this.formData.value.loginName,
      this.aesLoginNameKey
    );

    // 根据加密的登录名,获取密钥
    await this.loginService
      .getChangePwdKey(aesLoginName)
      .subscribe((res: any) => {
        console.log('根据加密的登录名,获取密钥 ===', res);
        // 通过加密登录名,获取的新密钥
        this.securityKey = res.securityKey;
      });
  }

2. 校验密码,要求包含大小写字母、数字、特殊字符

  /**
   * 正则匹配,检查密码复杂度
   * @param s 密码
   * @returns
   */
  checkPass(s) {
    let ls = 0;
		// 密码长度
    if (s.length < 8) {
      return 0;
    }
		// 包含数字
    if (s.match(/([0-9])+/)) {
      ls++;
    }
		// 包含小写字母
    if (s.match(/([a-z])+/)) {
      ls++;
    }
		// 包含大写字母
    if (s.match(/([A-Z])+/)) {
      ls++;
    }
		// 包含数字/小写字母/大写字母
    if (s.match(/[^a-zA-Z0-9]+/)) {
      ls++;
    }
    return ls;
  }

  /**
   * 根据正则匹配结果,确定密码不合格的提示信息
   */
  passwordLevel() {
    // 不符合规则的密码,返回什么提示
    let message = '';
    // 用户新密码的安全等级
    const userNewPwdLevel = this.checkPass(
      this.formData.get('repeatPassword').value
    );
    // 系统要求的密码安全等级
    const requirePwdLevel = 4;
    // 如果 用户新密码的安全等级,小于系统要求的安全等级,则根据情况进行提示
    if (userNewPwdLevel < requirePwdLevel) {
      switch (userNewPwdLevel) {
        case 0:
          message = '密码长度不得少于8位';
          break;
        case 1:
        case 2:
        case 3:
        case 4:
          message =
            '密码需要包含数字、小写字母、大写字母、特殊字符';
          break;
        default:
          break;
      }
      return message;
    }
	return message;
  }

3. 修改密码,传参格式 FORM['USER_ID'] 的写法

页面中定义了这些参数

      const data = {
        // 通过加密登录名,获取的新密钥(使用它加密新旧密码)
        securityKey: this.securityKey,
        // 用户ID
        USER_ID: this.formData.get('userId').value,
        // 登录名
        LOGIN_NAME: this.formData.get('loginName').value,
        // 原密码(使用新密钥加密的)
        PASSWORD_ENCRYPT: aseEncryptParams(
          this.formData.get('originPassword').value,
          this.securityKey
        ),
        // 新密码(使用新密钥加密的)
        NEW_PASSWORD_ENCRYPT: aseEncryptParams(
          this.formData.get('repeatPassword').value,
          this.securityKey
        ),
      };

接口这么定义

  /**
   * 保存修改的密码
   */
   savePwd(data: any) {
    const form = new FormData();
    // form.append('securityKey', data.securityKey);
    // form.append('USER_ID', data.USER_ID);
    // form.append('LOGIN_NAME', data.LOGIN_NAME);
    // form.append('PASSWORD_ENCRYPT', data.PASSWORD_ENCRYPT);
    // form.append('NEW_PASSWORD_ENCRYPT', data.NEW_PASSWORD_ENCRYPT);
    form.append('form[\'securityKey\']', data.securityKey);
    form.append('form[\'USER_ID\']', data.USER_ID);
    form.append('form[\'LOGIN_NAME\']', data.LOGIN_NAME);
    form.append('form[\'PASSWORD_ENCRYPT\']', data.PASSWORD_ENCRYPT);
    form.append('form[\'NEW_PASSWORD_ENCRYPT\']', data.NEW_PASSWORD_ENCRYPT);
    return this.http.post<Meta>(`${interfaceId.savePwdUrl}`, form);
  }

最终效果

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2022-08-19 19:36:23  更:2022-08-19 19:36:33 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 6:44:25-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码