JDBC_03实现用户登录业务并解决SQL注入
1.需求描述
- 1.需求:
- 模拟用户登录功能。
- 2.业务描述:
- 程序运行的时候,提供一个输入的入口,可以让用户输入用户名和密码。
- 用户输入用户名和密码后,提交信息,java程序收集到用户信息。
- java程序连接数据库验证用户名和密码是否正确。
- 正确:显示登录成功。
- 错误:显示登录失败。
- 3.数据的准备:
- 在实际开发中,表的设计会使用专业的建模工具:PowerDesigner。
- 使用该工具来进行数据库表的设计。
2.准备数据
1.打开PowerDesigner,新建一个Model,然后选第二个Model types,在右边方框里选PhySical Data Model。
在下方Model name改为t_user,DBMS选自己装的版本。然后ok。
2.在新界面找到右上方里的Table图表,选择该图表后,找一片空白地方点击,就创建了一张表。
3.Name字段不是真正创建的表名,只是一个代称,下面的Code才是真正要创建的表名。
4.表结构设计如下:
5.点击确定即可保存,再次双击表打开,查看最后一栏,已自动生成建表sql语句。
6.ctrl+s保存建表项目数据;在建表sql语句界面页中点左上角的保存按钮保存sql文件。
7.在导出的sql文件中添加插入语句以及完善一些细节
drop table if exists t_user;
/*==============================================================*/
/* Table: t_user */
/*==============================================================*/
create table t_user
(
id bigint auto_increment,
loginName varchar(255),
loginPwd varchar(255),
realName varchar(255),
primary key (id)
);
insert into t_user(loginName,loginPwd,realName) values('zhangsan','123456','张三');
insert into t_user(loginName,loginPwd,realName) values('lisi','123456','李四');
commit;
select * from t_user;
8.将sql文件导入数据库
在开发时,sql文件可能很大,使用一些工具无法导入数据库中。此时可以打开dos窗口,进入数据库,使用“source + sql文件路径” 导入sql文件。
因为dos窗口支持的编码格式是gbk,所以中文显示会乱码,但不碍事儿,数据已经成功导入数据库中。
至此,前期的准备工作已完成,可以进行程序设计了。
3.设计程序
3.1整体程序设计思路
本程序分为两部分,分别为初始化登录界面部分,以及验证用户名和密码部分。
将两个部分写进两个方法里实现。
用户在初始化界面方法里输入用户名和密码,用户信息被放进一个Map对象并返回。
将返回的Map对象传进验证用户名和密码方法中,使用jdbc连接数据库,查询用户表信息,
将Map对象中的用户信息和用户表中的信息进行比对,如果匹配就返回true,如果不匹配就返回false。
package com.tsccg.jdbc;
import java.util.Map;
/**
* @Author: TSCCG
* @Date: 2021/07/27 17:01
*
*/
public class JdbcLogin {
/**
* 程序入口
*/
public static void main(String[] args) {
//初始化界面
Map<String,String> userInfo = initUi();
//验证用户信息是否正确
boolean loginResule = login(userInfo);
System.out.println(loginResule ? "登录成功" : "登录失败");
}
/**
* 初始化登录界面
* @return 返回一个Map对象,包含用户名及用户密码
*/
private static Map<String,String> initUi() {
return null;
}
/**
* 登录,验证用户名以及用户密码
* @param userInfo 用户信息
* @return 用户信息正确就返回true,不正确就返回false
*/
private static boolean login(Map<String, String> userInfo) {
return false;
}
}
3.2初始化登录界面
用Scanner接收用户键盘输入,然后将用户信息放进Map集合里,并返回。
/**
* 初始化登录界面
* @return 返回一个Map对象,包含用户名及用户密码
*/
private static Map<String,String> initUi() {
Scanner sc = new Scanner(System.in);
System.out.print("用户名:");
//nextLine()读一行
String username = sc.nextLine();
System.out.print("用户密码:");
String password = sc.nextLine();
//将用户信息放进Map集合
Map<String,String> userInfo = new HashMap<>();
userInfo.put("loginName",username);
userInfo.put("loginPwd",password);
return userInfo;
}
3.3验证用户名和密码
通过jdbc查询出有该用户名和密码的那条记录,如果匹配,那么查询结果就有数据。故只需调用next()方法即可判断是否匹配。
/**
* 登录,验证用户名以及用户密码
* @param userInfo 用户信息
* @return 用户信息正确就返回true,不正确就返回false
*/
private static boolean login(Map<String, String> userInfo) {
boolean isSuccess = false;
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
String url = "jdbc:mysql://localhost:3306/tsccg";
String user = "root";
String password = "123456";
conn = DriverManager.getConnection(url,user,password);
//3.获取数据库操作对象
stmt = conn.createStatement();
//4.执行SQL语句
//以用户名和密码为条件在用户表中查询该用户信息
String sql = "select * from t_user where loginName = '"+ userInfo.get("loginName") +"' and loginPwd = '"+ userInfo.get("loginPwd") +"'";
rs = stmt.executeQuery(sql);
//5.处理查询结果集,
//不需要遍历查询结果集,只要查询到数据就证明有该用户的信息
if (rs.next()) {
isSuccess = true;
}
} catch (SQLException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
//6.释放资源
//先关闭rs
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
//再关闭stmt
try {
if (stmt != null) {
stmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
//最后再关闭conn
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
return isSuccess;
}
3.4完整代码
package com.tsccg.jdbc;
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
/**
* @Author: TSCCG
* @Date: 2021/07/27 17:01
*
*/
public class JdbcLogin {
/**
* 程序入口
*/
public static void main(String[] args) {
//初始化界面
Map<String,String> userInfo = initUi();
//验证用户信息是否正确
boolean loginResule = login(userInfo);
System.out.println(loginResule ? "登录成功" : "登录失败");
}
/**
* 初始化登录界面
* @return 返回一个Map对象,包含用户名及用户密码
*/
private static Map<String,String> initUi() {
Scanner sc = new Scanner(System.in);
System.out.print("用户名:");
//nextLine()读一行
String username = sc.nextLine();
System.out.print("用户密码:");
String password = sc.nextLine();
//将用户信息放进Map集合
Map<String,String> userInfo = new HashMap<>();
userInfo.put("loginName",username);
userInfo.put("loginPwd",password);
return userInfo;
}
/**
* 登录,验证用户名以及用户密码
* @param userInfo 用户信息
* @return 用户信息正确就返回true,不正确就返回false
*/
private static boolean login(Map<String, String> userInfo) {
boolean isSuccess = false;
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
String url = "jdbc:mysql://localhost:3306/tsccg";
String user = "root";
String password = "123456";
conn = DriverManager.getConnection(url,user,password);
//3.获取数据库操作对象
stmt = conn.createStatement();
//4.执行SQL语句
String sql = "select * from t_user where loginName = '"+ userInfo.get("loginName") +"' and loginPwd = '"+ userInfo.get("loginPwd") +"'";
rs = stmt.executeQuery(sql);
//5.处理查询结果集
if (rs.next()) {
isSuccess = true;
}
} catch (SQLException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
//6.释放资源
//先关闭rs
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
//再关闭stmt
try {
if (stmt != null) {
stmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
//最后再关闭conn
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
return isSuccess;
}
}
运行:
用户名:zhangsan
用户密码:123456
登录成功
用户名:阿巴阿巴阿巴
用户密码:666
登录失败
4.演示SQL注入现象
上面编写的程序存在漏洞。
重新运行上面的程序,并输入如下用户名和密码进行登录:
用户名:abc
用户密码:abc'or'1'='1
登录成功
可见,虽然用户名和密码不正确,但仍然登录成功了。
这是什么原因呢?
我们debug运行一下:
如图所示,
原本的sql语句为:
String sql = "select * from t_user where loginName = '" + userInfo.get("loginName") +"' and loginPwd = '" + userInfo.get("loginPwd") +"'";
在输入
用户名:abc
用户密码:abc'or'1'='1
后,sql语句被改成了
select * from t_user where loginName = 'abc' and loginPwd = 'abc'or'1'='1'
利用or的优先级比and低,使得原先的and语句变为了or语句。
用户通过输入精心构造的字符串去更改SQL语句,这就叫SQL注入。
常见的SQL注入方法还有:
用户名:'or 1#
用户密码:123
登录成功
在输入’or 1#后,SQL语句将变为:
select * from t_user where loginName =''or 1#' and loginPwd = '123'
‘’or 1恒为true,#注释掉后面的内容,所以查询语句可以正确执行。
5.解决SQL注入
5.1导致SQL注入的根本原因
用户输入的信息中含有sql语句的关键字,并且这些关键字参与sql语句的编译过程,导致sql语句的原意被更改,从而达到sql注入。
所以,只要不让用户输入的内容参与编译,就可以解决SQL注入。
5.2解决SQL注入步骤
1.将Statement换成PreparedStatement
Connection conn = null;
//Statement stmt = null;
PreparedStatement ps = null;
ResultSet rs = null;
2.获取预编译的数据库操作对象
更改JDBC编程六步中的第3步:
//3.获取预编译的数据库操作对象
//sql语句的框子,其中?代表占位符,只能传入值。注意:不可用单引号把占位符括起来。
String sql = "select * from t_user where loginName = ? and loginPwd = ?";
//程序执行到此,会发送sql语句框子给DBMS,然后DBMS进行sql语句的预先编译。
//通过conn调用prepareStatement方法。注意:方法名prepare后面没有d
ps = conn.prepareStatement(sql);
//获取用户输入的用户名和用户密码
String loginName = userInfo.get("loginName");
String loginPwd = userInfo.get("loginPwd");
//给占位符?传值(第一个问号下标是1,第二个问号下标是2,JDBC中所有下标从1开始)
ps.setString(1,loginName);
ps.setString(2,loginPwd);
3.执行SQL语句
更改JDBC编程六步中的第4步:
//4.执行SQL语句
rs = ps.executeQuery();//不需要再次填入sql
至此,程序已更改完毕。
5.3.完整代码
package com.tsccg.jdbc.login;
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
/**
* @Author: TSCCG
* @Date: 2021/07/28 14:37
*/
public class JdbcLoginDemo02 {
/**
* 程序入口
*/
public static void main(String[] args) {
//初始化界面
Map<String,String> userInfo = initUi();
//验证用户信息是否正确
boolean loginResule = login(userInfo);
System.out.println(loginResule ? "登录成功" : "登录失败");
}
/**
* 初始化登录界面
* @return 返回一个Map对象,包含用户名及用户密码
*/
private static Map<String,String> initUi() {
Scanner sc = new Scanner(System.in);
System.out.print("用户名:");
//nextLine()读一行
String username = sc.nextLine();
System.out.print("用户密码:");
String password = sc.nextLine();
//将用户信息放进Map集合
Map<String,String> userInfo = new HashMap<>();
userInfo.put("loginName",username);
userInfo.put("loginPwd",password);
return userInfo;
}
/**
* 登录,验证用户名以及用户密码
* @param userInfo 用户信息
* @return 用户信息正确就返回true,不正确就返回false
*/
private static boolean login(Map<String, String> userInfo) {
boolean isSuccess = false;
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
String url = "jdbc:mysql://localhost:3306/tsccg";
String user = "root";
String password = "123456";
conn = DriverManager.getConnection(url,user,password);
//3.获取预编译的数据库操作对象
//sql语句的框子,其中一个?代表一个占位符,只能传入值。注意:不可用'?'把占位符括起来
String sql = "select * from t_user where loginName = ? and loginPwd = ?";
//程序执行到此,会发送sql语句框子给DBMS,然后DBMS进行sql语句的预先编译。
ps = conn.prepareStatement(sql);
//获取用户输入的用户名和用户密码
String loginName = userInfo.get("loginName");
String loginPwd = userInfo.get("loginPwd");
//给占位符?传值(第一个问号下标是1,第二个问号下标是2,JDBC中所有下标从1开始)
ps.setString(1,loginName);
ps.setString(2,loginPwd);
//4.执行SQL语句
// String sql = "select * from t_user where loginName = '" + userInfo.get("loginName") +"' and loginPwd = '" + userInfo.get("loginPwd") +"'";
//不需要再次填入sql
rs = ps.executeQuery();
//5.处理查询结果集
if (rs.next()) {
isSuccess = true;
}
} catch (SQLException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
//6.释放资源
//先关闭rs
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
//再关闭ps
try {
if (ps != null) {
ps.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
//最后再关闭conn
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
return isSuccess;
}
}
5.4测试
使用SQL注入的方式测试程序
用户名:abc
用户密码:abc'or'1'='1
登录失败