0%

注册/登陆模块--案例

简单的javaweb项目!

测试:

  • 测试地址:传送门

  • 改进后的测试地址(添加验证码功能、RSA加密):传送门

  • 注册+登陆,测试地址:传送门

  • 测试账户:用户名:pete,密码:12345

怎么使用?

  • 只有用户名、密码验证的可以参考后边的demo(jquery)
  • 改进后的可以参考后边“改进”部分的“使用方法”(jquery)
  • 注册接口的使用可以参考后边的“–注册–”部分的”使用方法“(vue)

注意,本文代码较多,建议使用PC观看,可以点击左侧的目录快速跳转

介绍

前端页面发送用户名密码两个参数给服务器,服务器端通过访问数据库中的数据,验证账号密码是否正确,最后返回验证状态用户名两个参数给前端页面。

更新:添加注册API

环境

开发工具:IDEA、Maven

前端:jsp、Ajax – 登陆验证界面(异步)

后端:centos7、Javaweb、Tomcat、mysql – 提供登陆验证接口

前后端规定好:前端送两个参数过来userpasswd,要验证的用户名和密码。后端处理后(访问数据库进行验证),返回一个JSON类型的数据,其中包括两个参数statususer, 验证的结果(true,验证通过。false,验证不通过)和用户名。

我们在IDEA中使用Maven模板创建一个web应用程序。这里web应用程序项目名设置为Login(关于如何创建,可以访问这里)。(失误,感觉这个名字起的不是很好,例如:interface就还行)

- - 登陆 - -

前端页面

使用一个from表单进行搭建,没有设置action、method属性。数据提交和处理服务器返回的数据,通过给按钮添加监听器结合Ajax实现。

  • 文本输入框,设置id名,通过let user = document.getElementById("user"); user.value获取框中的文本(数据)

  • 通过Ajax,使用POST请求提交数据给服务器(后端接口)

  • 通过Ajax的回调函数onreadystatechange来处理服务器返回的数据

  • 为了方便调试,服务器返回的数据直接显示在页面上(form表单前边的h1、h2标签)

  • 在这部分使用的是原生的Ajax,在后边的demo中使用了jquery封装的Ajax。

前端页面搭建,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>login</title>
</head>
<body>
<h1></h1>
<h2></h2>
<form style="width: 500px;height: 200px" >
<fieldset>
<legend>登陆</legend>
用户名:<input type="text" id="user"><br>
密码:<input type="password" id="passwd"><br>
<input type="button" value="提交" onclick="mysubmit()" >
</fieldset>
</form>

</body>
<script type="text/javascript">
function mysubmit(){
let user = document.getElementById("user");
let passwd = document.getElementById("passwd");
if (user.value.length != 0 && passwd.value.length != 0){//输入框非空才会发送给服务器
document.querySelector("h2").innerHTML="尝试发送数据到服务器!!!";
var xmlhttp;
if (window.XMLHttpRequest)
{
// IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码
xmlhttp=new XMLHttpRequest();
}
else
{
// IE6, IE5 浏览器执行代码
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{

document.querySelector("h1").innerHTML=xmlhttp.responseText;
document.querySelector("h2").innerHTML="收到服务器的响应!!!";
}
}
xmlhttp.open("POST","${pageContext.request.contextPath}/login",true);
xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xmlhttp.send("user="+user.value+"&passwd="+passwd.value);//将表单数据发送给服务器
}
}
</script>
</html>

Ajax部分

${pageContext.request.contextPath}为EL表达式,pageContext为JSP页面的内置对象,用于获取当前页面(文件)所处的目录(路径)。前后端分离的话,这里需要改成后端接口的访问地址(本案例中,后端接口访问地址为http://qsdbl.site:8080/Login/login,这里post请求的地址更改为这个也可以。

注意:我一开始没有考虑到跨域问题(CDR错误),在本博客后边的demo中有提供修改后的源码,需要了解怎么解决跨域问题可以查看这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var xmlhttp;
if (window.XMLHttpRequest)
{
// IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码
xmlhttp=new XMLHttpRequest();
}
else
{
// IE6, IE5 浏览器执行代码
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{

document.querySelector("h1").innerHTML=xmlhttp.responseText;
document.querySelector("h2").innerHTML="收到服务器的响应!!!";
}
}
xmlhttp.open("POST","${pageContext.request.contextPath}/login",true);
xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xmlhttp.send("user="+user.value+"&passwd="+passwd.value);//将表单数据发送给服务器

open方法的第三个参数设置为true,意思是异步处理请求。服务器响应后,执行 onreadystatechange 事件中定义的函数(若设置为false,则不写onreadystatechange事件对应的函数)。

setRequestHeader为固定设置。send方法中,填写要发送给服务器处理的数据,键值对的形式。

Ajax教程,见菜鸟教程

后端

准备工作

阿里云ECS的域名为:qsdbl.site

mysql数据库

在阿里云ECS,Centos7上配置好MySql数据库。MySQL数据库的默认端口为3306,所以访问地址为qsdbl.site:3306。(数据库安装和建库建表详细过程访问这里:传送门

  • 创建一个普通账号:

    • 用户名:jack
    • 密码:12345Qsdbl--
  • 创建一个数据库,数据库名为qsdbl

  • 创建一个数据表,表名为account,列有id、user、passwd、type、time、timeup。

  • 表中添加几条数据(只添加用户名和密码,其他数据自动生成)

Tomcat

在阿里云ECS,Centos7上配置好Tomcat服务。Tomcat服务的默认端口为8080,所以访问地址为http://qsdbl.site:8080。(Tomcat服务的详细安装过程访问这里:传送门

编写后端接口

数据库操作

DAO模式

使用DAO(Data Access Object)模式操作数据库。将数据库的操作部分提取出来,封装成一个可被调用的程序单元,提高代码的重用性。使程序专注于处理业务逻辑。(关于DAO,可以看看这两篇文章:文章1文章2

需要导入两个第三方jar包,Mysql连接驱动和C3P0数据源。使用C3P0数据源,可以将连接池和连接池管理合二为一,提高访问数据库的效率。(整个案例中使用到的所有依赖,见博客最后边的源码)

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 数据库连接,数据源-->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.5</version>
</dependency>
<!-- 数据库连接,mysql,JDBC -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>

在项目中创建包com.qsdbl.connect;,放与操作数据库相关的类。

DAO父类

创建类BaseDao,作为DAO的父类。用于获得数据库连接对象,有两个构造方法,一定要提供的实参有:

  • 数据库所在服务器的IP地址或域名,下边的address属性
  • 所要操作的数据库名,下边的database_name属性
  • 有权限操作该数据库的账户与密码,下边的user属性和passwd属性

源码如下:

若出现中文乱码问题,可查看这篇博客,在下边代码45行处的setJdbcUrl中添加一些参数。

?useUnicode=true&characterEncoding=utf-8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.qsdbl.connect;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.beans.PropertyVetoException;
import java.sql.Connection;
import java.sql.SQLException;
//qsdbl
//使用C3P0数据源(连接池和连接池管理合二为一),提高访问数据库的效率(第三方jar包)
//DAO(数据访问对象)的父类
public class BaseDao {
private String address;//数据库的地址或域名,例如:qsdbl.site
private String database_name;//数据库名
private String user;//有权限访问数据库的用户,用户名
private String passwd;//对应的密码
private int maxPoolSize = 40;//连接池最大连接数(这里给个默认值)
private int minPoolSize = 2;//连接池最小连接数
private int initialPoolSize = 10;//初始化时获取的连接数
private int maxStatements = 180;//一个连接的缓存Statement对象的最大数
//数据源
public ComboPooledDataSource ds;
//构造方法
public BaseDao(String address,String database_name,String user,String passwd) throws PropertyVetoException {
this.address = address;
this.database_name = database_name;
this.user = user;
this.passwd = passwd;
init();
}
//该方法 用于返回数据源对象(C3P0)
public void init() throws PropertyVetoException {
ds = new ComboPooledDataSource();
//连接mysql数据库
ds.setDriverClass("com.mysql.cj.jdbc.Driver");
//数据库URL,database_name为数据库名
ds.setJdbcUrl(String.format("jdbc:mysql://%s:3306/%s",address,database_name));
//设置有权限访问数据库的账户和密码
ds.setUser(user);
ds.setPassword(passwd);
//设置连接池中保留的最大/最小连接数
ds.setMaxPoolSize(maxPoolSize);
ds.setMinPoolSize(minPoolSize);
//设置初始化时获取的连接数
ds.setInitialPoolSize(initialPoolSize);
//设置连接池中一个连接的缓存Statement对象的最大数
ds.setMaxStatements(maxStatements);
}
//获取数据库连接对象
public Connection getConnection() throws SQLException {
return ds.getConnection();//使用c3p0数据源,返回一个Connection对象(java.sql.Connection;)
}
}

DAO子类

根据业务需要定义DAO的不同子类,操作数据库。本案例中,要操作的数据表为数据库qsdbl中的account表。

类Table_account,实现对数据库的增删改查,继承类BaseDao。(不同的表,定义不同的DAO子类)

一些说明:

  • 对象con、pstmt可以放到try()-catch语句的()中,try()-catch中的代码 一般是对资源的申请,如果出了异常会被关闭(释放)。 – 下边没有使用这种结构

    • catch中使用SQLException对象.getErrorCode(),可获得数据查询的错误代码(可以看到查询数据库过程中到底是出了什么错误)
  • PreparedStatement对象继承自Statement类,将参数化的SQL语句发送到数据库。Statement对象发送的语句是一个功能明确而具体的语句。PreparedStatement与编译预处理有关,编译预处理是为了提高数据库存取的效率。(PreparedStatement可防止sql注入。可看看这篇博客

    • executeUpdate()方法,执行insert、update、delete语句(插入、更新、删除)

    • executeQuery();方法,执行select语句(查询),返回ResultSet对象

      • next(),判断返回的ResultSet对象中是否有数据

      • getString("列名或列在表中的序号"),返回该列的值

      • getInt(1);,如果使用count(*)函数查询数据(查询数量),使用该方法获取返回的数量(注意填1

  • ?为占位符,占位符的序号从左到右从1开始,具体的参数通过 类的setXX方法设置

  • Account对象,一个对象对应数据库中的account表的一行数据

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
package com.qsdbl.connect;
import java.beans.PropertyVetoException;
import java.util.Objects;
import java.sql.*;
//数据操作
//根据业务需要定义DAO的不同子类
public class Table_account extends BaseDao{
private String table = "account";//该类,用于操作数据表account
public void setTable(String table) {
this.table = table;
}
public Table_account() throws PropertyVetoException {
super("qsdbl.site","qsdbl","jack","12345Qsdbl--");
}
//添加账户
public String add(Account account) throws SQLException {
String sql = String.format("insert into %s ( user,passwd,type,mail) values (?,?,?,?);",table);
Connection con = null;
PreparedStatement pstmt = null;
try {
con = ds.getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, account.getUser());
pstmt.setString(2, account.getPasswd());
pstmt.setString(3, account.getType());
pstmt.setString(4, account.getMail());
pstmt.executeUpdate(); //执行更新
} catch (SQLException throwables) {
throwables.printStackTrace();
return Integer.toString(throwables.getErrorCode());//返回错误代码
}finally {
if (pstmt != null){
pstmt.close();
}
if (con != null){
con.close();
}
}
return "success";
}
//删除一个账户
public String del(String username) throws SQLException {
// String sql = String.format("delete from %d where user=?; ",table);
// try {
// Connection con = ds.getConnection();
// PreparedStatement pstmt = con.prepareStatement(sql);
// pstmt.setString(1, account.getUser());
// pstmt.executeUpdate();
// pstmt.close();
// con.close();
// ds.close();
// } catch (SQLException throwables) {
// return Integer.toString(throwables.getErrorCode());//返回错误代码
// }
// return "success";
//这里不是真正删除,而是将用户类型标记为已注销即可
if (!Objects.equals(findByName(username),"")){//存在该用户才执行注销操作
Account account = new Account();
account.setUser(username);
account.setType("deprecated");
return alterType(account);//修改成功,则返回success
}
return null;
}
//修改密码(通过用户名)
public String alterPasswd(Account account) throws SQLException {
String sql = String.format("update %s set passwd=? where user=?; ",table);
Connection con = null;
PreparedStatement pstmt = null;
try {
con = ds.getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, account.getPasswd());
pstmt.setString(2, account.getUser());
pstmt.executeUpdate();
} catch (SQLException throwables) {
return Integer.toString(throwables.getErrorCode());//返回错误代码
}finally {
if (pstmt != null){
pstmt.close();
}
if (con != null){
con.close();
}
}
return "success";
}
//修改类型(通过用户名)
public String alterType(Account account) throws SQLException {
String sql = String.format("update %s set type=? where user=?; ",table);
Connection con = null;
PreparedStatement pstmt = null;
try {
con = ds.getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, account.getType());
pstmt.setString(2, account.getUser());
pstmt.executeUpdate();
} catch (SQLException throwables) {
throwables.printStackTrace();
return Integer.toString(throwables.getErrorCode());//返回错误代码
}finally {
if (pstmt != null){
pstmt.close();
}
if (con != null){
con.close();
}
}
return "success";
}
//修改类型与密码(通过用户名)
public String alterTypePasswd(Account account) throws SQLException {
String sql = String.format("update %s set type=?,passwd=? where user=?; ",table);
Connection con = null;
PreparedStatement pstmt = null;
try {
con = ds.getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, account.getType());
pstmt.setString(2, account.getPasswd());
pstmt.setString(3, account.getUser());
pstmt.executeUpdate();
} catch (SQLException throwables) {
return Integer.toString(throwables.getErrorCode());//返回错误代码
}finally {
if (pstmt != null){
pstmt.close();
}
if (con != null){
con.close();
}
}
return "success";
}
//通过用户名 检索信息
public Account findByName(String username) throws SQLException {
Account account = new Account();
String sql = String.format("select * from %s where user=?; ",table);
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rst = null;
try {
con = ds.getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, username);
rst = pstmt.executeQuery();
if (rst.next()){
//将获取到的数据添加到Account对象中
account.setId(Integer.parseInt(rst.getString("id")));
account.setUser(rst.getString("user"));
account.setPasswd(rst.getString("passwd"));
account.setType(rst.getString("type"));
account.setTime(rst.getDate("time"));
account.setTimeup(rst.getTimestamp("timeup"));
}
} catch (SQLException throwables) {
throwables.printStackTrace();
return null;
}finally {
if (rst != null){
rst.close();
}
if (pstmt != null){
pstmt.close();
}
if (con != null){
con.close();
}
}
return account;
}
//查看数据库中的用户数量
public int find_Sum(Account account) throws SQLException {
String sql = String.format("select count(*) from %s;",table);
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rst = null;
int sum = 0;
try {
con = ds.getConnection();
pstmt = con.prepareStatement(sql);
rst = pstmt.executeQuery();
if (rst.next()){
sum = rst.getInt(1);
}
} catch (SQLException throwables) {
return throwables.getErrorCode();//返回错误代码
}finally {
if (rst != null){
rst.close();
}
if (pstmt != null){
pstmt.close();
}
if (con != null){
con.close();
}
}
return sum;
}
//查看数据库中 是否存在 某用户,exist,存在,none,不存在,error,错误
public String find_name(String username) throws SQLException {
String sql = String.format("select count(*) from %s where user=?;",table);
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rst = null;
int sum = 0;
try {
con = ds.getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1,username);
rst = pstmt.executeQuery();
if (rst.next()){
sum = rst.getInt(1);
}
} catch (SQLException throwables) {
return "error";
}finally {
if (rst != null){
rst.close();
}
if (pstmt != null){
pstmt.close();
}
if (con != null){
con.close();
}
}
if (sum > 0){
return "exist";
}else {
return "none";
}
}
//检查 账号密码 是否正确
public boolean check_passwd(Account account) throws SQLException {
String sql = String.format("select count(*) from %s where user=? && passwd=?;",table);
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rst = null;
int sum = 0;
try {
con = ds.getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1,account.getUser());
pstmt.setString(2,account.getPasswd());
rst = pstmt.executeQuery();
if (rst.next()){
sum = rst.getInt(1);
}
} catch (SQLException throwables) {
throwables.printStackTrace();
return false;
}finally {
if (rst != null){
rst.close();
}
if (pstmt != null){
pstmt.close();
}
if (con != null){
con.close();
}
}
if (sum > 0){
return true;
}else {
return false;
}
}
}

本案例中用到的只是查询数据库的操作,没有考虑到事务回滚。关于事务,可以看看这篇博客复习一下。

bean类

Bean类,JavaBean是一种Java语言写成的可重用组件。(pojo,简单的Java对象)
针对不同的数据,设计不同的bean类,该类对应数据库中的account表的一行数据(一个account对象),用于存储一个account实例(一个账户)。类Account中的各个属性对应account数据表的各个列名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.qsdbl.connect;
import java.sql.Timestamp;
import java.sql.Date;
//针对不同的数据,设计不同的bean类
//该类对应数据库中的account表的一行数据(一个account对象),用于存储从数据库中返回的数据
public class Account {
private int id;
private String user;//用户名
private String passwd;
private String type = "common";
private Date time;
private Timestamp timeup;

public Account() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
...(get、set方法省略)
public void setTimeup(Timestamp timeup) {
this.timeup = timeup;
}
}

验证模块

bean类Account的对象用于保存一个账户信息。使用DAO子类可以对该账户信息进行增删改查等操作。

编写类CheckLogin,用于验证浏览器发送过来的数据(账号、密码),并返回验证结果和用户名。

将数据转换为json类型,我们需要使用到一个第三方的jar包:(整个案例中使用到的所有依赖,见博客最后边的源码)

1
2
3
4
5
6
<!-- 转换json数据 -->
<dependency>
<groupId>top.jfunc.common</groupId>
<artifactId>converter</artifactId>
<version>1.8.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.qsdbl.login;

import com.alibaba.fastjson.JSONObject;
import com.qsdbl.connect.Account;
import com.qsdbl.connect.Table_account;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.beans.PropertyVetoException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Objects;

public class CheckLogin extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//设置编码
req.setCharacterEncoding("utf-8");
resp.setCharacterEncoding("utf-8");
resp.setContentType("application/json; charset=utf-8");//响应的数据为json类型

//方便测试,数据库中内置用户有 pete,12345;lily,1314

//status -- true,验证成功;false,验证失败
//user -- 传递过来的 用户名
//passwd -- 密码
boolean status = false;
//获取客户端提交的数据
String user = req.getParameter("user");//用户名
String passwd = req.getParameter("passwd");//密码

//验证
if (!Objects.equals(user,null) && !Objects.equals(passwd,null)){
try {
Account account = new Account();//创建账户对象
account.setUser(user);
account.setPasswd(passwd);
status = new Table_account().check_passwd(account);//检查密码是否正确
} catch (PropertyVetoException e) {
e.printStackTrace();
}
}
//生成json数据
JSONObject o = new JSONObject();
o.put("user", user);
o.put("status",status);
//发送验证结果给客户端
PrintWriter writer = resp.getWriter();

//为了处理跨域问题,将json改成jsonp
// String funString = req.getParameter("callback");
// writer.write(funString + "("+o.toString()+")");

writer.write( o.toString());

writer.flush();
writer.close();

}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
}

该Servlet程序返回的数据是json类型,包括用户名和验证的状态。

最后不要忘了到web.xml中注册。(当然使用注解配置也是可以的)。注册的请求路径为/login,服务器的地址为qsdbl.site,将该web应用程序部署到服务器上后,该Servlet程序(后端接口)访问地址为http://qsdbl.site:8080/Login/login,该web应用程序项目名为Login,大写。(关于注册Servlet,可以访问这里复习)

1
2
3
4
5
6
7
8
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>com.qsdbl.login.CheckLogin</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>

总结一下编写该后端接口可能需要用到的maven依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!-- 转换json数据 -->
<dependency>
<groupId>top.jfunc.common</groupId>
<artifactId>converter</artifactId>
<version>1.8.0</version>
</dependency>

<!-- 数据库连接,数据源-->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.5</version>
</dependency>
<!-- 数据库连接,mysql,JDBC -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>

<!-- servlet依赖-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<!-- jsp依赖-->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
</dependency>
<!-- JSTL表达式的依赖-->
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl-api</artifactId>
<version>1.2</version>
</dependency>
<!-- standard标签库的依赖-->
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>

部署

将制作好的web应用程序放到服务器上Tomcat的webapps文件夹下即可。

点击工具栏的绿色按钮,运行web应用程序。在左侧我们可以看到有一个target文件夹生成,里边的web应用程序名.war就是编译生成的文件,还有一个与web应用程序同名的文件夹,war包解压后就是该文件。所以我们只需要将与web应用程序同名的文件夹拷贝到服务器上即可。(也可以将war包拷贝出来,解压后再放到服务器上)

这里web应用程序名为Login,服务器域名为qsdbl.site,Tomcat服务的默认端口号为8080,所以放到Tomcat服务器上的webapps文件夹下之后,我们可以通过地址http://qsdbl.site:8080/Login访问到前边编写的前端页面

运行结果

注意:需要了解怎么解决跨域问题可以查看这里

注意,本案例没有解决Tomcat的https问题,暂时只能使用http请求。

更新:可以使用http或https发起请求

demo

该登陆验证接口怎么使用可以参考下边这个demo。测试页面:传送门。点这里查看我提供的一些账户。需要添加更多的账户可以通过关于页面的联系方式联系我。

API地址:http://qsdbl.site:8080/Login/login

需要携带的参数有:user、passwd。分别为用户名、密码。

服务器返回的jsonp数据中,有两个参数:status、user。分别为验证状态、用户名。验证状态status为true说明form表单中输入的用户名和密码正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html>
<head>
<title>login</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<body>
<form id="loginForm" style="width: 500px;height: 200px" >
<fieldset>
<legend>登陆</legend>
用户名:<input type="text" name="user"><br>
密码:<input type="password" name="passwd"><br>
<input type="button" value="提交" onclick="submit_data()" >
</fieldset>
</form>
</body>
<script src="http://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<script type="text/javascript">
function submit_data(){
var formParam = $("#loginForm").serialize();//获取表单数据
$.ajax({
async:false,       
url:"http://qsdbl.site:8080/Login/login", //引号内为处理数据的后端程序接口地址
type:"post",
data:formParam,//绑定表单数据
dataType:"jsonp",//数据类型
success: function(data){//服务器正常响应后,执行
if(data.status == true){//服务器返回的参数status表示验证状态,true-通过
alert("登录成功");
}
else alert("账号或者密码错误");
},
error:function(error){
alert("登录失败");
}

});
}
</script>
</html>

使用cdn方式引入jquery:

1
<script src="http://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

通过id绑定form表单中的数据,控件中的name设置为前边约定好的字段名。使用jquery封装的Ajax发起请求。

1
2
var formParam = $("#loginForm").serialize();
data:formParam

改进

  • 解决跨域问题
    • 前端,接收后端数据使用的数据类型改为jsonp
    • 后端,设置响应头允许跨域
  • 添加验证码
  • RSA加密。为了保证密码能在网络上安全的进行传输,使用RSA 加密技术。前端加密,后端解密。

使用方法

新URI:http://qsdbl.site:8080/myinterfacehttps://qsdbl.site:8443/myinterface

以前的API还是可以使用的,旧URI还是http://qsdbl.site:8080/Login/

  • 获取公钥

    • API地址:/getkey
    • 发起请求时不需要携带参数
    • 服务器返回的jsonp数据中,参数publicKey为公钥。
  • 获取验证码图片URL与获取uuid

    • API地址:/verificode
    • 发起请求时需要携带的参数有:timeoutuuid
      • timeout,前端页面自动切换验证码图片的时间,单位秒。阿拉伯数字,10到100之间,不要带上单位。
      • uuid,用于标识客户端身份。第一次发起请求时可以为空,接收到服务器响应的uuid后保存起来,下一次发起该请求或发起登陆验证请求时带上服务器响应的uuid。
    • 服务器返回的jsonp数据中,参数有:vpathuuid
      • vpth,验证码图片在服务器上的相对路径,使用时带上前边的uri
      • uuid,服务器响应的uuid。用于标识客户端身份,如果发起请求时uuid参数不为空则这里返回的uuid与发起请求时的相同。
  • 登陆验证

    • API地址:/login
    • 发起请求时携带参数有:userpasswdverificodeuuid
      • user,用户名
      • passwd,密码
      • verificode,验证码
      • uuid,用于标识客户端身份的uuid,由获取验证码图片的API获取(这里将验证码和RSA整合在一起了)
    • 服务器返回的jsonp数据中,参数有:isVerificodestatususer
      • isVerificode,验证码验证状态。true-验证码填写正确(正确的用法应该是先检查参数isVerificode再检查参数status)
      • status,账户验证状态。true-密码与用户名正确
      • user,用户名
    • 更新:在后边的注册部分更新了登陆验证接口返回的参数,新增了一个type类型,具体作用见后边笔记
  • 注意:本接口支持http与https两种请求模式,建议不要混合使用。(jquery的引入、前端web部署的环境、后端接口的请求等等)

  • 具体使用案例,见下边的“前端”部分,包括HTML页面搭建和JS功能实现两个小部分。

前端

使用jquery封装的Ajax发起请求。使用cdn方式引入jquery:

1
<script src="http://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

在加密之前需要跟服务器获取公钥,加解密也需要一定时间所以速度没有原来的快。

前端加密依赖,jsencrypt.min.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script src="http://passport.cnblogs.com/scripts/jsencrypt.min.js"></script>

//vue-cli
npm install jsencrypt --save
//main.js导入
import {JSEncrypt} from 'jsencrypt' //引入加密工具
//main.js中定义一个加密函数并挂载到原型上
Vue.prototype.$encryptedData = function(publicKey, data) {
//new一个对象
let encrypt = new JSEncrypt();
//设置公钥
encrypt.setPublicKey(publicKey);
//data是要加密的数据
let result = encrypt.encrypt(data);
return result
}

如果部署在配置了SSL的环境中,则需要将http更改为https。

前端页面修改如下

HTML

在原来的基础上添加了一个验证码输入框和显示验证码图片的img标签。(因为要对数据进行加密,所以不能使用demo那种方式获取输入框中的数据。name又改成了id,使用id获取数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>login</title>
<body>
<form id="loginForm" style="width: 550px;height: 200px" >
<fieldset>
<legend>登陆</legend>
用户名:<input type="text" id="user"><br>
密码:<input type="password" id="passwd"><br>
验证码:<input type="text" id="verificode">
<img src="" id="verificode_img" style="margin:0 10px" />
<a href="#" style="font-size: 10px" onclick="changecode()">看不清?点这里切换一张</a><br>
<input type="button" value="提交" onclick="submit_data()" >
</fieldset>
</form>
</body>
</head>
</html>

JS

定义一些全局变量:myuri、uuid、publicKey、timeout。

  • myuri:http://qsdbl.site:8080/myinterface,在Tomcat服务器中部署后端接口程序的地址。Tomcat配置了SSL,所以这里可以使用http或https。本地测试则为http://localhost:8080/myinterface
  • uuid:存放服务器端发送来的uuid。由于要跨域使用,所以cookie、session不能使用,得使用一个标识来标志客户端。
  • publicKey:存放服务器端发送来的公钥。用于加密。
  • timeout:超时时间,单位秒。自动切换验证码的时间。

方法getKey(),用于获取服务器的公钥。

方法changecode(),用于获取uuid和自动切换验证码图片。

方法submit_data(),用于向服务器发起验证请求,当然这里也对数据进行加密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script type="text/javascript">
let myuri = "http://qsdbl.site:8080/myinterface";//为了方便调试,将uri设置为一全局变量。qsdbl.site 或 localhost
// let myuri = "https://qsdbl.site:8443/myinterface";//https
let uuid = "";//存放 服务器端发送来的uuid
let publicKey = "";//存放 服务器端发送来的公钥
let timeout = 45;//秒,超时时间即自动切换验证码的时间
window.onload = function (){
getKey();//获取 公钥
changecode(timeout);//加载验证码(同时获取uuid)
//自动切换验证码(45秒)
setInterval(function (){//使用一个周期定时器,定时切换验证码图片
changecode(timeout);
},timeout*1000);
}
//自动切换验证码 与 获取uuid
function changecode(timeout){
...
}
//提交数据给服务器进行 验证
function submit_data(){
...
}
//从服务器处获取公钥
function getKey(){
...
}
</script>

方法getKey(),用于获取服务器的公钥。请求地址为myuri+"/getkey"。服务器响应的数据中,参数publicKey为公钥。在接收到服务器响应的公钥后保存到全局变量publicKey中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//从服务器处获取公钥
function getKey(){
$.ajax({
async:true,
url: myuri+"/getkey",
type: "POST",
dataType: "jsonp",
success: function(data) {
if(data){
publicKey = data.publicKey;//拿到公钥
};
if(publicKey==null){
alert("获取publicKey失败,请联系管理员!");
return;
};
}
});
}

方法changecode(),用于获取uuid和自动切换验证码图片。发起获取验证码请求时带上参数timeoutuuid,服务器返回图片URL和uuid。请求地址为myuri+"/verificode"。服务器响应的数据中,参数vpath为验证码图片在服务器上的相对地址,需要加上myuri。还有一个参数uuid。

  • timeout为本地自动切换验证码图片的时间,后端程序根据这个时间删掉过期的验证码图片。

  • uuid用于标识客户端的身份,第一次请求时可以为空,在接收到服务器响应的uuid后要保存到全局变量中下次请求带上该uuid。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//自动切换验证码 与 获取uuid
function changecode(timeout){
let verificode_img = document.getElementById("verificode_img");
$.ajax({
async:true,
url:myuri+"/verificode",//引号内为处理数据的后端接口地址。
type:"POST",
data:{//发送给服务器的参数
"timeout":timeout,
"uuid":uuid
},
dataType:"jsonp",
success: function(resp){
verificode_img.src = myuri+resp.vpath;//更新页面上的验证码图片
uuid = resp.uuid;//保存uuid到本地
},
error:function(error){
alert("验证码获取失败,请联系管理员!");
}
});
}

方法submit_data(),用于向服务器发起验证请求,当然这里也对数据进行加密。请求地址为myuri+"/login"。服务器响应的数据中,参数isVerificode为验证码是否正确,布尔值。参数status为用户名、密码验证状态,布尔值。参数user为用户名。

在发起请求时要带上参数user、passwd、verificode、uuid,分别是用户名、密码、验证码、uuid,各个参数经过加密后再发起请求。方法submit_data,当点击“提交”按钮时执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//提交数据给服务器进行 验证
function submit_data(){
let user = (document.getElementById("user")).value;//获取用户名输入框中的数据
let passwd = (document.getElementById("passwd")).value;//密码
let verificode = (document.getElementById("verificode")).value;//验证码
if (user.length!= 0 && passwd.length!= 0 && verificode.length==7 && publicKey.length!=0) {
//输入框非空 且 获取到了公钥 才会向服务器发起验证请求
//对用户名、密码、验证码、uuid进行 加密
let jsencrypt = new JSEncrypt();
jsencrypt.setPublicKey(publicKey.trim());//trim,去除字符串的头尾空格
user = jsencrypt.encrypt(user.trim());
passwd = jsencrypt.encrypt(passwd.trim());
verificode = jsencrypt.encrypt(verificode.trim());
let myuuid = jsencrypt.encrypt(uuid.trim());
//发送数据给服务器,验证账户、密码是否正确
$.ajax({
async:true,
url:myuri+"/login",//引号内为处理数据的后端接口地址。
type:"POST",
data:{//发送给服务器的参数
"user":user,
"passwd":passwd,
"verificode":verificode,
"uuid":myuuid,
},
dataType:"jsonp",
success: function(resp){
if(resp.status === true && resp.isVerificode === true){
alert("登录成功!");
}else{
if (resp.isVerificode === false){
alert("验证码有误!");
}else{
alert("账号或者密码错误!");
}
}
},
error:function(error){
alert("登录失败");
}
});
}
}

后端

RSA加密

前端向后端请求公钥(GetKey),对数据进行加密。后端程序接收到前端发来的经过加密的数据,使用私钥进行解密。

需要添加两个类。类RSAutils,与类GetKey。RSAutils,负责RSA加密相关的工作。生成公钥、解密等。GetKey是一个Servlet程序,负责生成公钥并发送给客户端。

用到的Maven依赖如下:(整个案例中使用到的所有依赖,见博客最后边的源码)

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 加解密 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.14</version>
</dependency>
<!-- 轻量级密码术包;支持大量的加密算法 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.67</version>
</dependency>

RSAutils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.qsdbl.util;

import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPublicKey;

public class RSAutils {//RSA加密
private static final KeyPair keyPair = initKey();
private static KeyPair initKey() {
try {
Provider provider =new org.bouncycastle.jce.provider.BouncyCastleProvider();
Security.addProvider(provider);
SecureRandom random = new SecureRandom();
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", provider);
generator.initialize(1024,random);
return generator.generateKeyPair();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
public static String generateBase64PublicKey() {//生成Base64公钥(发送给浏览器端)
PublicKey publicKey = (RSAPublicKey)keyPair.getPublic();
return new String(Base64.encodeBase64(publicKey.getEncoded()));
}
public static String decryptBase64(String string) {//base64解密
return new String(decrypt(Base64.decodeBase64(string.getBytes())));
}
private static byte[] decrypt(byte[] byteArray) {
try {
Provider provider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
Security.addProvider(provider);
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", provider);
PrivateKey privateKey = keyPair.getPrivate();
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] plainText = cipher.doFinal(byteArray);
return plainText;
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}

GetKey:web.xml中注册的请求路径为/getkey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.qsdbl.util;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class GetKey extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setCharacterEncoding("utf-8");
resp.setContentType("application/text; charset=utf-8");
String publicKey = RSAutils.generateBase64PublicKey();//生成公钥
resp.getWriter().write(publicKey);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

RSA参考了这篇博客

servlet程序CheckLogin中的使用示例:

1
2
3
//获取客户端提交的数据(decryptBase64,解密)
String user = RSAutils.decryptBase64(req.getParameter("user").trim());//用户名
String passwd = RSAutils.decryptBase64(req.getParameter("passwd").trim());//密码

验证码

由于是跨域使用,所以不能使用session保存客户端的验证码(数字)。为了解决这个问题,这里使用uuid来标识客户端,将分发给客户端的uuid和验证码映射在一起保存到VerifiCode对象中,再保存在ServletContext中。当客户端发起验证请求时带上uuid,就可以通过uuid在ServletContext中查找对应的验证码与客户端发过来的验证码进行对比,判断验证码是否填写正确。

编写验证码实体类VerifiCode(或叫做been、pojo),保存验证码和uuid。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.qsdbl.util;
public class VerifiCode {
private String uuid;
private String verificode;

public VerifiCode(String uuid, String verificode) {
this.uuid = uuid;
this.verificode = verificode;
}
public String getUuid() {
return uuid;
}
...//省略get、set方法
public void setVerificode(String verificode) {
this.verificode = verificode;
}
}

Servlet程序VerifiCodeImg,处理客户端获取验证码请求。将生成的验证码图片保存在服务器上,返回验证码图片的URL和uuid。根据客户端的参数timeout,定时删掉保存在服务器上的验证码图片和保存在ServletContext中的验证码对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package com.qsdbl.util;
import com.alibaba.fastjson.JSONObject;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
import java.util.List;

public class VerifiCodeImg extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//uuid
String uuid;
if (req.getParameter("uuid") == null){//若客户端的请求中没有参数uuid
return;//直接退出servlet程序
}else {//有带上参数uuid
uuid = req.getParameter("uuid");//获取 客户端发过来的uuid
if (uuid.equals("")){//若客户端发过来的uuid为空,则创建一个
uuid = UUID.randomUUID().toString();//生成uuid码
}
}
//验证码超时时间
int timeout = 30;//默认值;
if (req.getParameter("timeout") != null){
timeout = Integer.valueOf(req.getParameter("timeout"));
}
if (timeout <10 || timeout > 100){
return;//验证码超时时间,只能是10~100。不符合要求,直接退出servlet程序
}
//生成验证码:
String vcode = getRandom();//生成随机 验证码
//添加验证码到Session中:
// req.getSession().setAttribute("verificode",vcode);//若跨域,session无法保存验证码
//在验证码之间增加空格(为了美观)
StringBuffer vcode_2=new StringBuffer();
for(int i=0;i<vcode.length();i++){
vcode_2.append(vcode.charAt(i)+" ");
}

//在内存中创建一个图片:
BufferedImage image = new BufferedImage(110,20,BufferedImage.TYPE_3BYTE_BGR);//宽、高、图片颜色类型

//准备绘制图片:
Graphics2D g = (Graphics2D)image.getGraphics();//笔
//绘制背景颜色:
g.setColor(Color.white);//设置画笔颜色
g.fillRect(0,0,110,20);//起始x、y,结束x、y坐标。可以理解为宽、高为80*20

//绘制验证码:
g.setColor(Color.black);//设置画笔颜色,即验证码(数字)的颜色
g.setFont(new Font(null,Font.BOLD,15));//设置字体大小
g.drawString(vcode_2.toString(),0,17);//绘制验证码

//生成干扰线条:
for(int i=0;i<10;i++){
Random random=new Random();
int xBegin =random.nextInt(80);
int yBegin =random.nextInt(20);
int xEnd=random.nextInt(xBegin+30);
int yEnd=random.nextInt(yBegin+30);
g.setColor(getColor());//方法getColor,生成随机的颜色
g.drawLine(xBegin, yBegin, xEnd, yEnd);//绘制线条
}
String vpath = saveImgToServer(image,timeout);//验证码图片 保存到服务器上(定时删掉)
saveCodeToServer(vcode,uuid,timeout);//验证码(数字)与uuid保存到ServletContext中(定时删掉)

resp.setContentType("application/jsonp; charset=utf-8");//响应的数据类型
//发送给浏览器:
JSONObject json = new JSONObject();//封装 json数据
json.put("vpath", vpath);//图片路径
json.put("uuid",uuid);//将uuid返回给客户端
//为了处理跨域问题,将json改成jsonp
String funString = req.getParameter("callback");

//发送验证结果给客户端
PrintWriter writer = resp.getWriter();
writer.write(funString + "("+json.toString()+")");
writer.flush();
writer.close();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
//获取7位数的随机数
private String getRandom(){
...
}
//生成随机颜色
private Color getColor(){
...
}
//保存 验证码 和 uuid 到ServletContext中
private void saveCodeToServer(final String vcode, final String uuid, final int timeout){
...
}
//保存验证码图片到服务器上
private String saveImgToServer(BufferedImage img, final int timeout) throws IOException {
...
}

方法getRandom,获取7位数的随机数,用于生成验证码图片。

1
2
3
4
5
6
7
8
9
10
//获取7位数的随机数
private String getRandom(){
String num = new Random().nextInt(1000000)+"";//取值范围是(包括前,不包括后):[0,n)
StringBuffer supplement = new StringBuffer();
for (int i = 1; i <= 7-num.length(); i++) {//如果生成的随机不是7位的,后边补0
supplement.append("0");
}
num = supplement.toString() + num;
return num;
}

方法getColor,获取随机颜色,用于生成干扰线条。

1
2
3
4
5
6
7
8
//生成随机颜色
private Color getColor(){
Random random=new Random();
int r =random.nextInt(256);
int g =random.nextInt(256);
int b =random.nextInt(256);
return new Color(r,g,b);
}

方法saveCodeToServer,保存验证码和uuid到ServletContext中,并定时删掉。将验证码和uuid保存在验证码对象VerifiCode中,将验证码对象发到一个ArrayList集合中,最后再将该集合添加到ServletContext中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//保存 验证码 和 uuid 到ServletContext中
private void saveCodeToServer(final String vcode, final String uuid, final int timeout){//vcode为验证码,数字。timeout为验证码失效时间
VerifiCode verifiCode = new VerifiCode(uuid,vcode);//创建 验证码对象(保存uuid和验证码)
List<VerifiCode> verificodelist;//保存到ServletContext的 验证码 集合
final ServletContext servletContext = this.getServletContext();
if (servletContext.getAttribute("verificode") == null){//若不存在 验证码集合,则创建一个
verificodelist = new ArrayList<>();//应使用并发容器(例如CopyOnWriteArrayList或Vector)代替普通集合
}else {//若存在,则从ServletContext中获取 验证码集合
verificodelist = (List)servletContext.getAttribute("verificode");
}

//通过客户端提供的uuid,判断是否是同一个客户端再次申请验证码图片到前端
boolean isUuid = false;//通过 一个标志位,判断是否是同一个客户端
ListIterator iter = verificodelist.listIterator();//遍历集合
while(iter.hasNext()) {
VerifiCode vc = (VerifiCode)iter.next();
if (Objects.equals(vc.getUuid(),uuid)){//存在相同的uuid,说明是同一个客户端的再次申请。
//不添加新验证码对象到集合中,而是修改集合中的验证码对象中保存的验证码
vc.setVerificode(vcode);//修改 验证码
isUuid = true;
}
}
if (!isUuid){//不是同一个客户端的再次申请,则添加新验证码对象到集合中
verificodelist.add(verifiCode);//添加 验证码对象 到集合中
}
servletContext.setAttribute("verificode",verificodelist);//更新 ServletContext中的集合,key=verificode

//定时删掉 该验证码对象(更新)
new Thread(){
@Override
public void run() {
super.run();
try {
sleep((timeout-3)*1000);//提前三秒删掉(考虑到网络延迟的问题)
List<VerifiCode> verificodelist = (List)servletContext.getAttribute("verificode");
ListIterator iter = verificodelist.listIterator();//遍历集合
while(iter.hasNext()) {
VerifiCode vc = (VerifiCode)iter.next();
if (Objects.equals(vc.getUuid(),uuid)){
iter.remove();
}
}
servletContext.setAttribute("verificode",verificodelist);//更新 ServletContext中的集合
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}

方法saveImgToServer,保存验证码图片到服务器上,返回验证码图片在服务器上的相对路径(相对于web应用程序)。这里保存验证码的目录为/img/vcodeImg,保存的图片类型为jpg。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//保存验证码图片到服务器上
private String saveImgToServer(BufferedImage img, final int timeout) throws IOException {
//验证码图片的 保存目录
String imgRelativePath = "/img/vcodeImg";//相对路径
String imgName = System.currentTimeMillis()+".jpg";//图片名
String vcodeParentPath = this.getServletContext().getRealPath(imgRelativePath);//绝对路径
File vppFile = new File(vcodeParentPath);
if (!vppFile.exists()){//判断目录是否存在
boolean ismk = vppFile.mkdirs();//不存在,则创建这个目录(创建多级目录,使用mkdirs)
}

//在服务器上的保存路径(保存路径 加 文件名;uuid作为图片名)
final String vcodePath = vcodeParentPath + "/" + imgName;

//将图片保存到服务器上:
//BufferedImage(内存中的验证码图片) 转 InputStream(输入流)
InputStream inputStream = null;
ByteArrayOutputStream bs = new ByteArrayOutputStream();
try {
ImageOutputStream imOut = ImageIO.createImageOutputStream(bs);
ImageIO.write(img, "jpg",imOut);
inputStream= new ByteArrayInputStream(bs.toByteArray());
} catch (IOException e) {
e.printStackTrace();
}

//创建一个文件输出流
FileOutputStream fos = new FileOutputStream(vcodePath);
//创建一个缓冲区
byte[] buffer = new byte[1024*1024];
//判断是否读取完毕
int len = 0;
//如果大于0,说明还存在数据(将图片保存到服务器上)
while ((len = inputStream.read(buffer)) > 0){
fos.write(buffer,0,len);
}
//关闭流
fos.flush();
fos.close();
inputStream.close();

//超时(timeout)后自动删掉验证码图片
new Thread(){
@Override
public void run() {
try {
sleep((timeout)*1000);//模拟 定时器
} catch (InterruptedException e) {
e.printStackTrace();
}
File vcodeImg = new File(vcodePath);
if (vcodeImg.exists()){
vcodeImg.delete();//删掉验证码图片
}
}
}.start();

return imgRelativePath+"/"+imgName;//返回 图片保存路径(相对路径)
}

验证模块

登陆验证CheckLogin的改动比较大,这里全部贴出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package com.qsdbl.login;

import com.alibaba.fastjson.JSONObject;
import com.qsdbl.connect.Account;
import com.qsdbl.connect.Table_account;
import com.qsdbl.util.RSAutils;
import com.qsdbl.util.VerifiCode;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.beans.PropertyVetoException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;

public class CheckLogin extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//查看客户端 发过来的 参数(字段)是否合法
if (!checkParameter(new String[]{"user","passwd","verificode","uuid"},req)){//false,表示缺少某个字段
return;//退出程序
}
//设置响应类型
resp.setContentType("application/jsonp; charset=utf-8");

//方便测试,内置用户有 pete,12345;lily,1314

//status -- true,验证成功;false,验证失败
//user -- 传递过来的 用户名
//passwd -- 密码
boolean status = false;
boolean isVerificode = false;
String type = "";
//获取客户端提交的数据(decryptBase64,解密)
String user = RSAutils.decryptBase64(req.getParameter("user").trim());//用户名
String passwd = RSAutils.decryptBase64(req.getParameter("passwd").trim());//密码
String user_verificode = RSAutils.decryptBase64(req.getParameter("verificode").trim());//用户提交的验证码
String user_uuid = RSAutils.decryptBase64(req.getParameter("uuid").trim());//用户提交的uuid

//String verificode = req.getSession().getAttribute("verificode").toString();
//Session中保存的验证码(跨域,无法使用session保存验证码)
//使用方法checkVcode实现(保存到ServletContext中)

if (checkVcode(user_verificode,user_uuid)){//验证码相同,才验证账号、密码
isVerificode = true;
//验证账号、密码
try {
Account account = new Account();//创建账户对象
account.setUser(user);
account.setPasswd(passwd);
//新增:添加判断账户状态
Account databaseAccount = new Table_account().findByName(account.getUser());
type = databaseAccount.getType();
if (type.equals("admin") || type.equals("common")){//deprecated,废弃(注销状态)。admin,管理员。common,普通用户,deprecated_new,新用户待激活
//当前账户,是admin账户或common账户才可以进行登陆操作
status = new Table_account().check_passwd(account);//检查密码是否正确
}
} catch (Exception e) {
e.printStackTrace();
}
}
//封装json数据
JSONObject json = new JSONObject();
json.put("user", user);
json.put("type",type);
json.put("status",status);
json.put("isVerificode",isVerificode);
//为了处理跨域问题,将json改成jsonp
String funString = req.getParameter("callback");
//发送验证结果给客户端
PrintWriter writer = resp.getWriter();
writer.write(funString + "("+json.toString()+")");
writer.flush();
writer.close();

}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
//验证 客户端发来的验证码是否正确
public static boolean checkVcode(String vcode,String uuid){
...
}
//检查 参数是否合法(是否漏了某个参数)
public static boolean checkParameter(String[] params,HttpServletRequest req){
boolean status = true;//false,表示缺少某个字段
for (int i = 0; i < params.length; i++) {
if (Objects.equals(req.getParameter(params[i]),null)){
status = false;
}
}
return status;
}
}

验证 客户端发来的验证码是否正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static boolean checkVcode(String vcode,String uuid){//vcode为验证码,数字;uuid,为客户端发送过来的(获取验证码时发过去给客户端的)
boolean status = false;//true,验证通过(验证码)
VerifiCode verifiCode = new VerifiCode(uuid,vcode);//创建 验证码对象(保存uuid和验证码)

final List<VerifiCode> verificodelist;//从ServletContext中获取 验证码 集合
final ServletContext servletContext = this.getServletContext();
if (servletContext.getAttribute("verificode") != null){//若存在 验证码集合
verificodelist = (List)servletContext.getAttribute("verificode");
ListIterator iter = verificodelist.listIterator();//遍历集合

while(iter.hasNext()) {
VerifiCode vc = (VerifiCode)iter.next();//获取具体的 验证码对象
if (Objects.equals(vc.getUuid(),uuid) && Objects.equals(vc.getVerificode(),vcode)){
//uuid与验证码都正确
status = true;//验证通过
}
}
servletContext.setAttribute("verificode",verificodelist);//更新 ServletContext中的集合
}
return status;
}

跨域

编写过滤器CorsFilter,解决跨域问题。作用范围为整个web应用程序,允许跨域请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.qsdbl.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

// 跨域请求设置
HttpServletResponse response =(HttpServletResponse)servletResponse;
response.setHeader("Access-Control-Allow-Origin", "*");// *代表允许任何网址请求
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");// 允许请求的类型
response.setHeader("Access-Control-Allow-Credentials","true");// 设置是否允许发送 cookies
response.setHeader("Access-Control-Allow-Headers","*");// *代表允许任何请求头字段
//顺便解决中文乱码
response.setCharacterEncoding("utf-8");
((HttpServletRequest)servletRequest).setCharacterEncoding("utf-8");

filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {

}
}

web.xml

web.xml文件中进行如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!-- 登陆验证接口 的请求路径为/login-->
<servlet>
<servlet-name>login</servlet-name>
<servlet-class>com.qsdbl.login.CheckLogin</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>login</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>

<!-- 获取公钥 的请求路径为/getkey-->
<servlet>
<servlet-name>getkey</servlet-name>
<servlet-class>com.qsdbl.util.GetKey</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>getkey</servlet-name>
<url-pattern>/getkey</url-pattern>
</servlet-mapping>

<!-- 获取验证码图片URL和uuid 的请求路径为/verificode-->
<servlet>
<servlet-name>VerificationCode</servlet-name>
<servlet-class>com.qsdbl.util.VerifiCodeImg</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>VerificationCode</servlet-name>
<url-pattern>/verificode</url-pattern>
</servlet-mapping>

<!-- 过滤器 的作用范围为/* ,即整个web应用程序-->
<filter>
<filter-name>cors</filter-name>
<filter-class>com.qsdbl.filter.CorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cors</filter-name>
<url-pattern>/*</url-pattern><!--注册过滤器-->
</filter-mapping>

测试

注意:测试的时候每次重启IDEA后要刷新浏览器页面更新公钥。

1
2
3
4
5
6
7
8
- - 同域 - -
访问部署在Tomcat中的web页面(验证模块部署在Tomcat的myinterface中):
http://qsdbl.site:8080/myinterface/login_http.html
https://qsdbl.site:8443/myinterface/login.html

- - 跨域 - -
访问部署在Apache中的web页面(验证模块部署在Tomcat的myinterface中):
https://www.qsdbl.site/private/myinterface/login.html

同域:

跨域:

- - 注册 - -

在原项目的基础上添加注册接口。注册时需要填写用户名、密码、邮箱,并通过邮件激活后才能进行登录,数据传输依然是使用RSA加密

注意:在新增注册模块时修复了前边案例中的一些bug(后端)和对部分代码进行优化,例如对前边的DAO部分进行了小小修改,本博客上的代码已更新不过图片上的就无法更改了。只是小修复不影响已部署的后端API的使用。

测试地址:传送门

使用方法

这里介绍一下注册接口的使用

  • 注册账户
    • URI:http://qsdbl.site:8080/myinterfacehttps://qsdbl.site:8443/myinterfacet
    • API地址:/register
    • 发起请求时携带参数有:userpasswdverificodeuuidmail
      • user,用户名
      • passwd,密码
      • verificode,验证码
      • uuid,用于标识客户端身份的uuid,由获取验证码图片的API获取
      • mail,邮箱地址。
    • 服务器返回的jsonp数据中,参数有:isVerificodeisExiststatususer
      • isVerificode,验证码验证状态。false-验证码填写错误
      • isExist,是否已经存在该账户。true-已存在该用户名,需要用户更改用户名重新注册
      • status,账户验证状态。true-密码与用户名正确
      • user,用户名
      • 先判断isVerificode,验证码是否正确。再判断isExist是否存在该账户。因为isVerificode为false时不会进行账户查重操作,isExist默认为true

登陆验证接口进行了小小的修改(不影响之前的项目),新增加了一个返回参数type。如果验证失败status=false,可以查看一下type的值,deprecated_new – 新注册未进行激活操作,deprecated – 已被注销的账户不能进行登陆,最后才是用户名、密码输入不正确。

前端页面

这里的前端页面示例中改用vue来完成数据交互部分,不使用前边登陆部分的jquery。

改变

1、校验数据合法性:前边的jquery案例中,是在提交数据的函数submit_data中对数据合法性进行校验,这里使用H5的新特性
required – 必填。pattern – 正则表达式,检查数据合法性。
在form表单的invalid事件中设置提示语,v-on:invalid.capture="formVali"formVali函数设置输入框 自定义提示语。
(注意是使用事件捕获模式,原生js在addEventListener()中最后一个参数填true,vue中要使用“.capture”事件修饰符)

2、提交数据的触发方式:前边的jquery案例中,提交数据是使用普通button按钮添加点击事件监听器实现。这里是使用form表单的submit按钮。
通过监听form表单的submit事件在其中写ajax替代默认的提交行为。
v-on:submit.prevent="mysubmit"mysubmit函数在form的submit事件中,用ajax提交数据。
事件修饰符prevent,阻止默认事件,在这里的作用是提交事件不重载(刷新)页面

3、页面切换:登陆页面与注册页面的切换
使用变量isLogin,判断当前页面应该显示登陆页面还是注册页面(true-登陆,false-注册)。
使用v-bind:class绑定样式,使用v-if:isLogin隐藏或显示相应的控件。见下边的例子:
只有登陆页面才有”忘记密码?”这个控件出现(点击第二个按钮切换两个页面)。
样式绑定:v-bind:class="{'Login_password':isLogin}"与v-bind:class="{ 'Login_a': isLogin }"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<span class="massage">密码:</span>
<input class="kuang" v-bind:class="{'Login_password':isLogin}" type="password" required="required" name="passwd" pattern="[\dA-Za-z]{5,16}" placeholder=" 长度为5-16个字符" v-model="passwd" />
<a href="" v-bind:class="{ 'Login_a': isLogin }" v-if="isLogin">忘记密码?</a>


.form_bigbox form .Login_password{
width: 180px;
margin-right: 10px;
}
.form_bigbox form .Login_a{
font-size: 12px;
text-decoration: none;
color: firebrick;
}

4、加载动画
变量isAnimation,结合v-show决定控件是否隐藏。默认状态为隐藏。当点击登陆或注册按钮(且数据合法)向服务器提交数据时显示动画。当服务器响应后隐藏动画。(函数mysubmit中)

1
2
3
<div v-show="isAnimation" v-on:click="isAnimation=!isAnimation" class="myanimation">
<canvas id="myanimation" width="100px" height="100px"></canvas>
</div>

5、Ajax
使用vue的axios而不是使用jquery。
由于要解决跨域问题,需要使用jsonp类型的数据进行交互,vue不支持这种类型的数据故需要自己对数据进行处理。不过我在网了找了一个别人写好的方法,直接拿来使用即可(取自这篇博客)。使用方法很简单,见源码中44-48行,94-103行的小例子。(或查看这里

源码

部分方法(函数)中的代码太长了,为了提高易读性我将它们单独贴在后边,点击左侧的标题可以快速定位过去

html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<body>
<div id="app">
<div class="form_bigbox">
<div class="form_title">
<h2>{{submit_btn}}账号</h2>
</div>
<!-- 事件修饰符capture,阻止捕获,添加事件侦听器时使用事件捕获模式。修饰符prevent,阻止默认事件在这里的作用是提交事件不重载(刷新)页面 -->
<form v-on:submit.prevent="mysubmit" v-on:invalid.capture="formVali">
<span class="massage">用户名:</span><input class="kuang" type="text" required="required" name="name" pattern="[-\w\u4E00-\u9FA5]{4,10}"
placeholder=" 长度为4-10个字符" autofocus="autofocus" v-model="user" />
<br /><br />
<span class="massage">密码:</span><input class="kuang" type="password" required="required" name="passwd" pattern="[\dA-Za-z]{5,16}"
placeholder=" 长度为5-16个字符" v-model="passwd" />
<span v-if="!isLogin"><br /><br />
<span class="massage">邮箱:</span><input class="kuang" type="email" required="required" name="mail" placeholder=" 可用于找回密码"
v-model="mail" /></span>
<br /><br />
<span class="massage">验证码:</span><input class="kuang vcode" type="text" required="required" name="vcode" pattern="[0-9]{7}"
placeholder=" 点击右侧图片可切换" v-model="verificode" /><img v-bind:src="vcodeImg" class="vcodeimg" v-on:click="changeVcode" />
<div class="button_box">
<input type="submit" v-bind:value="submit_btn" /><input type="button" v-on:click="changeFormStatus" v-bind:value="change_btn" />
</div>
</form>
</div>
<div v-show="isAnimation" v-on:click="isAnimation=!isAnimation" class="myanimation">
<canvas id="myanimation" width="100px" height="100px"></canvas>
</div>
</div>
</body>

js和css:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.staticfile.org/axios/0.18.0/axios.min.js"></script>
<script src="http://passport.cnblogs.com/scripts/jsencrypt.min.js"></script>
<!-- form表单的样式 -->
...(为了提高代码易读性,我将该部分单独放到后边的代码块中)
<script>
axios.defaults.baseURL = 'http://qsdbl.site:8080/myinterface'; //设置全局URL,qsdbl.site,localhost
//封装函数jsonp,解决跨域请求
axios.jsonp = (url, data) => {
...(为了提高代码易读性,我将该部分单独放到后边的代码块中)
}
var vue = new Vue({
el: '#app',
data: {
//form表单配置
isLogin: true,
submit_btn: '登陆',
change_btn: '立即注册',
//验证码配置
vcodeImg: '',
uuid: '',
timeout: 45,
//登陆验证,配置
publicKey: '',
user: '',
passwd: '',
verificode: '',
//注册,配置
mail: '',
//登陆动画
isAnimation: false
},
methods: {
//改变表单状态(登陆、注册,之间切换)
changeFormStatus: function() {
this.isLogin = !this.isLogin;
if (this.isLogin) {
//登陆样式
this.submit_btn = '登陆';
this.change_btn = '立即注册';

} else {
//注册样式
this.submit_btn = '注册';
this.change_btn = '有账号?点我登陆';
}
},
// 在form的submit事件中,用ajax提交数据
mysubmit: function() { //在form标签中进行绑定 v-on:submit.prevent="mysubmit"
...(为了提高代码易读性,我将该部分单独放到后边的代码块中)
},
//设置输入框 自定义提示语
formVali: function(event) { //在form标签中进行绑定 v-on:invalid.capture="formVali"
...(为了提高代码易读性,我将该部分单独放到后边的代码块中)
},
//切换验证码图片
changeVcode: function() {
//使用封装的jsonp函数
axios.jsonp("/verificode", {
timeout: this.timeout,
uuid: this.uuid
}).then(function(res) {
vue.uuid = res.uuid;
vue.vcodeImg = axios.defaults.baseURL + res.vpath;
}).catch(err => console.log("axios请求出错,err:" + err))
}
},
mounted() {//在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作
//加载验证码图片 和 定时切换验证码图片
this.changeVcode();
window.setInterval(function() {
vue.changeVcode();
}, this.timeout * 1000);

//获取公钥
axios.jsonp("/getkey").then(function(res) { //使用自己封装的jsonp函数
vue.publicKey = res.publicKey;
}).catch(err => console.log("axios请求出错,err:" + err))
}
});
</script>
<!-- 动画的js、css -->
...(为了提高代码易读性,我将该部分单独放到后边的代码块中)

封装的jsonp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
axios.jsonp = (url, data) => {
if (!url) {
throw new Error('url is necessary')
} else {
if (axios.defaults.baseURL != undefined) {
url = axios.defaults.baseURL + url; //修改处
}
}

const callback = 'CALLBACK' + Math.random().toString().substr(9, 18)
const JSONP = document.createElement('script')
JSONP.setAttribute('type', 'text/javascript')

const headEle = document.getElementsByTagName('head')[0]

let ret = '';
if (data) {
if (typeof data === 'string')
ret = '&' + data;
else if (typeof data === 'object') {
for (let key in data)
ret += '&' + key + '=' + encodeURIComponent(data[key]);
}
ret += '&_time=' + Date.now();
}
JSONP.src = `${url}?callback=${callback}${ret}`;
return new Promise((resolve, reject) => {
window[callback] = r => {
resolve(r)
headEle.removeChild(JSONP)
delete window[callback]
}
headEle.appendChild(JSONP)
})
}

提交数据mysubmit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 在form的submit事件中,用ajax提交数据
mysubmit: function() { //在form标签中进行绑定 v-on:submit.prevent="mysubmit"
if (this.isLogin) {
//登陆
vue.isAnimation = true; //启用动画
let jsencrypt = new JSEncrypt(); //加密工具
jsencrypt.setPublicKey(vue.publicKey.trim()); //trim,去除字符串的头尾空格
axios.jsonp("/login", {
user: jsencrypt.encrypt(vue.user.trim()),
passwd: jsencrypt.encrypt(vue.passwd.trim()),
verificode: jsencrypt.encrypt(vue.verificode.trim()),
uuid: jsencrypt.encrypt(vue.uuid.trim())
}).then(function(respon) {
//服务器正常响应
console.log(respon);
vue.isAnimation = false;
//isVerificode、status、user
if (!respon.isVerificode) {
alert("验证码输入错误!");
} else {
if (respon.status) {
alert("登陆成功!");
} else {
switch (respon.type) {
case "deprecated_new":
alert("请先激活账户再登陆,注意查收激活邮件📧");
break;
case "deprecated":
alert("当前账户已注销无法使用!!!");
break;
default:
alert("密码有误!登陆失败!");
break;
}
}
}
}).catch(function() {
//服务器 非正常响应
vue.isAnimation = false;
})
} else {
//注册
vue.isAnimation = true; //启用动画
console.log("向服务器发起 注册 请求。。。");
let jsencrypt = new JSEncrypt(); //加密工具
jsencrypt.setPublicKey(vue.publicKey.trim()); //trim,去除字符串的头尾空格
axios.jsonp("/register", {
user: jsencrypt.encrypt(vue.user.trim()),
passwd: jsencrypt.encrypt(vue.passwd.trim()),
verificode: jsencrypt.encrypt(vue.verificode.trim()),
uuid: jsencrypt.encrypt(vue.uuid.trim()),
mail: jsencrypt.encrypt(vue.mail.trim())
}).then(function(respon) {
//服务器正常响应
vue.isAnimation = false;
console.log(respon);
//isVerificode、status、user、isExist
if (!respon.isVerificode) {
alert("验证码输入错误!");
} else {
if (respon.isExist) {
alert("用户名" + respon.user + "已存在,请重新设置用户名!");
} else {
if (respon.status) {
alert("账户注册成功!账户需要激活才能正常登陆,激活链接已发至邮箱" + vue.mail + ",请留意查收!");
} else {
alert("账户注册失败,请联系网站客服!")
}
}

}
}).catch(function() {
//服务器 非正常响应
vue.isAnimation = false;
})

}
}

自定义提示语

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//设置输入框 自定义提示语
formVali: function(event) { //在form标签中进行绑定 v-on:invalid.capture="formVali"
var elem = event.target;
var vali = elem.validity;
var name = elem.name;
switch (name) {
case "name":
if (vali.valueMissing) {
elem.setCustomValidity("用户名不能为空!");
} else {
elem.setCustomValidity("");
}
break;
case "passwd":
if (vali.valueMissing) {
elem.setCustomValidity("密码不能为空!");
} else {
elem.setCustomValidity("");
}
break;
case "mail":
if (vali.valueMissing) {
elem.setCustomValidity("邮箱不能为空!");
} else {
elem.setCustomValidity("");
}
break;
case "vcode":
if (vali.valueMissing) {
elem.setCustomValidity("请输入验证码!");
} else {
if (vali.patternMismatch) {
elem.setCustomValidity("格式有误!");
}
elem.setCustomValidity("");
}
break;
}
}

动画的js+css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!-- 动画的js、css -->
<script type="text/javascript">
var pen = (document.querySelector('#myanimation')).getContext('2d');
pen.beginPath();
pen.lineWidth = 10;
var color = pen.createLinearGradient(20, 40, 78, 75);
color.addColorStop(0, '#212121');
color.addColorStop(1, '#ffffff');
pen.strokeStyle = color;
pen.arc(50, 50, 40, (3 / 4) * Math.PI, 2.25 * Math.PI);
pen.stroke();
</script>
<style type="text/css">
.myanimation {
width: 100vw;
height: 100vh;
background-color: rgba(255, 255, 255, 0.8);
position: fixed;
top: 0;
left: 0;
z-index: 10;
display: flex;
justify-content: center;
align-items: center;
}

.myanimation canvas {
animation: xz 0.7s linear infinite;
width: 50px;
}

@keyframes xz {
from {
transform: rotateZ(360deg);
}

to {
transform: rotateZ(0deg);
}
}
</style>

form表单样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<!-- form表单的样式 -->
<style>
* {
margin: 0;
padding: 0;
}

body {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}

.form_bigbox {
width: auto;
height: auto;
margin: auto;
}

.form_title {
border-bottom: 2px solid #d5d5d5;
height: 40px;
}

.form_title h2 {
border-left: 4px solid #000000;
padding-left: 5px;
}

.form_bigbox form {
margin-top: 30px;
}

.form_bigbox form .massage {
display: inline-block;
width: 70px;
height: 20px;
line-height: 20px;
text-align: right;
}

.form_bigbox form .kuang {
width: 250px;
margin-right: 5px;
height: 20px;
}

.form_bigbox form .button_box {
width: 100%;
text-align: center;
}

.form_bigbox form .button_box input {
width: 130px;
height: 25px;
margin: 30px 10px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
}

.form_bigbox form .vcode {
width: 130px;
margin-right: 10px;
}

.form_bigbox form .vcodeimg {
display: inline-block;
width: 100px;
cursor: pointer;
}

.form_bigbox form input:valid {
/* 输入的值合法时的样式 */
background-color: #d5d5d5;
}
</style>

后端接口

注册接口的大致工作流程如上图所示。我们需要编写两个Servlet程序,处理注册请求和激活账户操作。还需要对原数据库中的account表进行修改,添加列mail。

处理注册请求

编写类Register,处理注册请求。web.xml中配置的请求路径为”/register”。

CheckLogin类的方法checkParameter、方法checkVcode进行小小的修改,这里可以直接调用,检查参数合法性和检查验证码是否正确。参数user、passwd、mail、verificode、uuid都是必须的,否则直接退出程序。
saveData方法,保存用户数据到数据库(24小时内未进行激活操作,则删除账户),返回boolean值,true-当前用户名已存在即重名了。false-没有重名,账户已注册成功。
sendMail方法,发送邮件给用户,用于激活账户。单独写成一个类SendVerifyMail,用于发送邮件,使用多线程优化用户体验(减少等待时间)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.qsdbl.login;
import com.alibaba.fastjson.JSONObject;
import com.qsdbl.connect.Account;
import com.qsdbl.connect.Table_account;
import com.qsdbl.util.RSAutils;
import com.qsdbl.util.SendVerifyMail;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.beans.PropertyVetoException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Objects;

//注册
public class Register extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//查看客户端 发过来的 参数(字段)是否合法
if (!CheckLogin.checkParameter(new String[]{"user","passwd","verificode","uuid","mail"},req)){//调用类CheckLogin中的方法
//false,表示缺少某个字段
return;//退出程序
}
resp.setContentType("application/jsonp; charset=utf-8");
//接收用户请求,封装成对象
String user = RSAutils.decryptBase64(req.getParameter("user").trim());
String passwd = RSAutils.decryptBase64(req.getParameter("passwd").trim());
String mail = RSAutils.decryptBase64(req.getParameter("mail").trim());
String verificode = RSAutils.decryptBase64(req.getParameter("verificode").trim());
String uuid = RSAutils.decryptBase64(req.getParameter("uuid").trim());

boolean status = false;//注册状态,true-注册成功
boolean isVerificode = false;//验证码,是否正确
boolean isExist = true;//当前注册的用户名 是否已经存在。true,有重名

if (CheckLogin.checkVcode(verificode,uuid,req)){//判断 验证码 是否正确(调用类CheckLogin中的方法)
isVerificode = true;
try {
isExist = saveData(user,passwd,mail);//保存用户数据到数据库(返回布尔值,true-存在重名用户名)
} catch (Exception throwables) {
throwables.printStackTrace();
}
if (!isExist){
//不重名,则标记为注册成功
status = true;
sendMail(new Account(user, passwd, mail));//发送邮件给用户,用于激活账户
}
}
//返回注册状态给客户端
//封装json数据
JSONObject json = new JSONObject();
json.put("user", user);//当前注册用户名
json.put("status",status);//注册状态,true-注册成功
json.put("isExist",isExist);//是否重名,true-重名,需要用户更改用户名
json.put("isVerificode",isVerificode);//验证码,false-错误,需要重新填写验证码
//为了处理跨域问题,将json改成jsonp
String funString = req.getParameter("callback");
//发送验证结果给客户端
PrintWriter writer = resp.getWriter();
writer.write(funString + "("+json.toString()+")");
writer.flush();
writer.close();
}
//保存用户数据到数据库(24小时内未进行激活操作,则删除账户)
public boolean saveData(String user,String passwd,String mail){
...
}
//发送邮件给用户,用于激活账户
public void sendMail(Account account){//使用多线程,优化用户体验(减少等待时间)
new SendVerifyMail(account).start();
}
}

saveData

保存用户数据到数据库(24小时内未进行激活操作,则删除账户)。不是真正的删掉账户,而是将其账户类型更改为deprecated,代表已废弃已注销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//保存用户数据到数据库(24小时内未进行激活操作,则删除账户)
public boolean saveData(String user,String passwd,String mail){
boolean status = true;//是否存在重名,true-存在
//创建账户对象,标记为deprecated-已废弃,即需要激活才能使用(CheckLogin类,也需要更改一下验证要求,之前忘记了type)
final Account account = new Account(user, passwd, mail,"deprecated_new");//"_new",后缀,表示为刚刚注册未激活的用户(而不是管理员将其手动注销)
//保存数据
try {
Table_account table_account = new Table_account();
String isExist = table_account.find_name(account.getUser());
if (!isExist.equals("exist")){
//当前用户名,没有重名
String res = table_account.add(account);//往数据库中 添加数据
if (res.equals("success")){
status = false;
new Thread(){//24小时内未进行激活操作,则删除账户
@Override
public void run() {
try {
sleep(1000*60*60*24);//单位毫秒。一天,1000*60*60*24
Table_account table_account = new Table_account();
Account databaseAccount = table_account.findByName(account.getUser());
if (databaseAccount.getType().equals("deprecated_new")){//用户还未激活
String s = table_account.del(account.getUser());//删除账户(标记为deprecated)
}
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return status;
}

SendVerifyMail

发送邮件给用户,用于激活账户。关于发送邮件,详细笔记可查看这里

激活账户,使用超链接get请求传递用户名和密码给服务器的方式。激活账户使用到Servlet程序ActivateAccount,请求地址为“/activate”,见下边第67-72行。

邮件发送用到的maven依赖(整个案例中使用到的所有依赖,见博客最后边的源码):

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--    邮件发送-->
<!-- https://mvnrepository.com/artifact/javax.mail/mail -->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.5.0-b01</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.activation/activation -->
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package com.qsdbl.util;
import com.qsdbl.connect.Account;
import com.sun.mail.util.MailSSLSocketFactory;
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Properties;
//发送邮件给用户,用于激活账户
public class SendVerifyMail extends Thread {//使用多线程,优化用户体验(减少等待时间)
private String host = "smtp.163.com";//发送邮件的服务器地址
private String from = "qsdbl_wy@163.com";//发件邮箱
private String username = "qsdbl_wy@163.com";//发件人
private String authCode = "BZMIKFJCTFSTBPRQ";//授权码
private Account account;
public SendVerifyMail(Account account){
this.account = account;
}
@Override
public void run() {
try{
Properties prop = new Properties();
prop.setProperty("mail.host",host);
prop.setProperty("mail.transport.protocol","smtp");//邮件发送协议
prop.setProperty("mail.smtp.auth","true");//需要验证用户名密码

//QQ邮箱,还要设置SSL加密(其他邮箱不需要)
MailSSLSocketFactory sf = new MailSSLSocketFactory();
sf.setTrustAllHosts(true);
prop.put("mail.smtp.ssl.enable","true");
prop.put("mail.smtp.ssl.socketFactory",sf);

//使用JavaMail发送邮件的5个步骤
//1、创建定义整个应用程序所需的环境信息的Session对象
Session session = Session.getDefaultInstance(prop, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
//实参为发件人的账号与密码,密码填写授权码即可
return new PasswordAuthentication(username,authCode);
}
});
//开启Session的debug模式,这样可以实时查看Email的运行状态
// session.setDebug(true);
//2、通过Session对象得到transport对象
Transport ts = session.getTransport();
//3、使用邮箱的用户名 和 授权码 连上邮件服务器
ts.connect(host,username,authCode);
//4、创建邮件:写邮件
//注意需要传递Session
MimeMessage message = new MimeMessage(session);
//指明邮件的发件人
message.setFrom(new InternetAddress(from));
//指明邮件的收件人,若发件人与收件人一样则是发给自己(可以群发)
// message.setRecipient(Message.RecipientType.TO,new InternetAddress(account.getMail()));
message.setRecipients(Message.RecipientType.TO,new Address[]{new InternetAddress(account.getMail()),new InternetAddress(from)});//给自己也抄送一份,避免发送失败(网易要求比较严格,说不定什么时候就出岔子了)
//邮件的标题
message.setSubject("注册成功®️");//网易163邮箱对标题要求比较严格,若报错可以使用"注册成功®️"试试。
//邮件的文本内容
StringBuilder text = new StringBuilder();
text.append("<span style=\"display:block;width:100vw;height: 100vh;font-size: 1rem;\"><form><fieldset><legend style=\"color: red;\">激活账户</legend><span style=\"color: red;\">");
text.append(account.getUser());
text.append("</span>,您好,感谢您的注册🎉,请务必阅读以下内容。");
text.append("<br/>您的用户名:");
text.append(account.getUser());
text.append("<br/>您的密码:");
text.append(account.getPasswd());
text.append("<br/>请妥善保管,如有问题请联系网站客服!");
text.append("<br/>点击👉<a href=\"");//本地测试使用http,部署后更改为https,https://qsdbl.site:8443
text.append("http://localhost:8080");
text.append("/myinterface/activate?user=");
text.append(account.getUser());
text.append("&passwd=");
text.append(account.getPasswd());
text.append("\">这里</a>👈激活注册账户。请在24小时内激活您刚刚注册的账户否则账户将会被回收,若无法点击请用浏览器查看本邮件📧。如非本人操作,请忽略此邮件!<br/><img src='../../../images/markdown_img/2020/20200905111242.gif' style='width:200px'/></fieldset></form></span>");
message.setContent(text.toString(),"text/html;charset=utf-8");//html文本,可设置样式

//5、发送邮件
ts.sendMessage(message, message.getAllRecipients());
//6、关闭连接
ts.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

由于注册账户模块中涉及到账户类型的操作。故对类CheckLogin进行相应的修改,在验证账户的用户名与密码之前先检查用户的账户类型,如果不是普通账户或管理员账户则不进行检查。返回给前端的数据中多了个type参数,可以根据type判断验证失败的原因是不是账户已经注销或者新注册未激活,若不是上边两种情况则是用户输入的用户名、密码不正确。

CheckLogin的修改(在第54行):

1
2
3
4
5
6
7
8
9
10
//新增:添加判断账户状态
Account databaseAccount = new Table_account().findByName(account.getUser());
type = databaseAccount.getType();
if (type.equals("admin") || type.equals("common")){//deprecated,废弃(注销状态)。admin,管理员。common,普通用户,deprecated_new,新用户待激活
//当前账户,是admin账户或common账户才可以进行登陆操作
status = new Table_account().check_passwd(account);//检查密码是否正确
}

//新增一个返回值类型
json.put("type",type);

激活账户

ActivateAccount,激活账户,更改账户类型为普通用户-common。请求地址为“/activate”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.qsdbl.util;
import com.qsdbl.connect.Account;
import com.qsdbl.connect.Table_account;
import com.qsdbl.login.CheckLogin;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.beans.PropertyVetoException;
import java.io.IOException;
import java.io.PrintWriter;

//激活账户,更改账户类型为普通用户-common
public class ActivateAccount extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//查看客户端 发过来的 参数(字段)是否合法
if (!CheckLogin.checkParameter(new String[]{"user","passwd"},req)){//调用类CheckLogin中的方法
//false,表示缺少某个字段
return;//退出程序
}
String user = req.getParameter("user");
String passwd = req.getParameter("passwd");
if (user.length()>0 && passwd.length()>0){
//验证账号、密码
try {
Account account = new Account();//创建账户对象
account.setUser(user);
account.setPasswd(passwd);//类中默认的账户类型为common(普通用户),这里不需要添加type属性
Account database_account = new Table_account().findByName(user);
if (database_account.getUser().equals(user) && database_account.getPasswd().equals(passwd) && database_account.getType().equals("deprecated_new")){
//用户名、密码正确,且账户类型为deprecated_new注册待激活状态时,才允许更改账户类型
new Table_account().alterType(account);//修改账户类型 为 普通用户
}else {
//账户不是带激活状态deprecated_new,退出程序
resp.setContentType("text/html; charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.write("<span style=\"display:block;width:100vw;height: 100vh;line-height: 100vh; text-align: center;color: red;font-size: 1.55rem;\">该账户已过了激活期限!请联系网站客服或重新注册®️。</span>");
writer.flush();
writer.close();
return;//(提前)退出程序
}
} catch (PropertyVetoException e) {
e.printStackTrace();
}
}else{
return;//用户名、密码不合法,退出程序
}
//响应客户端,账户已激活成功!
resp.setContentType("text/html; charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.write("<span style=\"display:block;width:100vw;height: 100vh;line-height: 100vh; text-align: center;color: red;font-size: 1.55rem;\">账户已激活成功!可以正常使用了。</span>");
writer.flush();
writer.close();
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--  激活账户-->
<servlet>
<servlet-name>activate</servlet-name>
<servlet-class>com.qsdbl.util.ActivateAccount</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>activate</servlet-name>
<url-pattern>/activate</url-pattern>
</servlet-mapping>
<!-- 注册账户-->
<servlet>
<servlet-name>register</servlet-name>
<servlet-class>com.qsdbl.login.Register</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>register</servlet-name>
<url-pattern>/register</url-pattern>
</servlet-mapping>

测试

1
2
3
4
5
6
7
8
- - 同域 - -
访问部署在Tomcat中的web页面(验证模块部署在Tomcat的myinterface中):
http://qsdbl.site:8080/myinterface/LR.html
https://qsdbl.site:8443/myinterface/LR.html

- - 跨域 - -
访问部署在Apache中的web页面(验证模块部署在Tomcat的myinterface中):
https://www.qsdbl.site/private/myinterface/LR.html

访问部署在Apache中的web页面:

登陆账户:

注册账户:

邮件激活账户:

如果过了激活期限(24小时),再点击链接,则显示的是:

源码

原案例,只有普通的用户名、密码验证功能(源码只有打包后的war包):

https://cloud.189.cn/t/YfMVF36vEZz2 (访问码:kk0g)

改进后,加了验证码功能和RSA加密,响应速度变慢了许多(源码有IDEA项目文件和war包):

https://cloud.189.cn/t/6Bfmm2jmIrEj (访问码:vz4b)

注册+登陆,添加了注册模块:https://cloud.189.cn/t/7JRji2eIj2Q3 (访问码:x3lv)

若图片不能正常显示,请在浏览器中打开

欢迎关注我的其它发布渠道