根据所学的知识, 写一个管理系统, 顺便总结一些知识点
准备:
前端用vue-cli的框架, 后端用jdbc连接数据库, 项目部署tomcat服务器来完成交互
●前端的vue-cli框架搭建可以看 [点击跳转] 的第二小结
●后端的tomcat在idea里的相关的配置与集成,可以看 [点击跳跃]
一、 前段搭建
在完成[点击跳转])Vue-cli 项目的搭建之后, 咱去组件官网直接 顺组件 (复制组件), 这里我用的是Element-ui官网的组件,官网点击右边: [点击完成跳转]
1. 登录组件
2. 管理页面
3.异步请求发送后端, 验证账号
首先使用xios来发送, 因为原生态的ajax代码繁琐
<1>首先要引入axios.js文件或者安装axios的脚手架
$ npm install axios
这里就引入xios.js的组件来使用axios发送请求
<2>下载完成后, 在node_moudules里导入到main.js的全局配置文件里面
<3>导入后就能直接用axios语法来发送请求
发送get请求
this.$http.get("http://localhost:8080/webBack_5.14/login?account=jwq&password=111").then(function(resp){
//业务逻辑代码
})
发送post请求
this.$http.post("http://localhost:8080/webBack_5.14/login",this.account).then((resp){
//业务逻辑代码
})
4.异步请求发送后端 数据转化json
由于json是一种轻量级的数据格式, 所以前后端传输数据时候, 经常用json格式来传输在前端可以写一个方法来转化数据格式:
请求里做参数转json格式,直接发送json的请求
5.防止通过浏览器地址恶意访问
如果仅仅是通过登录框验证密码还是不够安全, 还可以通过URL地址来直接进行访问, 就会跳过登录的页面
如下:
直接输入了网站URL的地址, 就直接访问进来了, 根本没有通过账户密码的验证所以要将一个验证信息放到浏览器里--->(sessionStorage)
sessionStorage
他是浏览器里可以存储数据的容器, 在关闭浏览器时就会清除
因此将用户信息存储到sessionStorage(会话储存) 里 , 在进入具体操作的网页网页时候可以先进行判断, 如果有会话存储里有账号, 那么就可以进行访问, 不然那就回到登录的页面, 这样验证就更加符合需求,
而且此时的sessionStorage还可以将登录人的信息, 账号放到页面上显示, 就好像京东这些购物软件, 将用户人的登录信息展示到 我的这一页面,就可以将操作人的信息放到sessionStorage里面, 随后在哪个页面想要获取渲染在前端时, 就可以随时获取, 非常方便
如下:
具体操作
在登录成功后, 将account的数据放入到sessionStorage里面,
在之后每一次请求处理访问登录的请求外的其他请求, 都要验证account
<1>放入account
<2>添加web会话跟踪技术
这是在router对象的那个index.js文件里配置
这样每一次请求的跳转就有了条件, 经过验证, 满足条件才会跳转, 让浏览器的安全性有了基础保障
提高安全性(session不安全), 使用token来验证
session缺点
- 浏览器里的数据时可以随时改的, 就会让一些不法分子走漏洞, 所以在前端验证并不是万全之策
- 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存
中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。
因此要引入另一种安全的高效的验证操作, token 验证方式
不了解token的可以看这里 点击跳转
具体操作
首先在登录成功之后, 要在 后端生成token, 再将token连随响应数据一并 发送给前端, 前端得到token之后, 在接下来的 每一次发送请求都会将token携带到请求里, 到 后端来验证token是否正确, 正确就继续执行操作, 不正确就要返回 token不对的异常信息
生成token:
首先要导入jar包
由JWT的方式生成token
想了解JWT的可以 点击去看看JWT
JWT的工具类
public class JWTUtil {
/**
* jwt 生成 token
*/
public static String token (Admin admin){
String token = "";
try {
//过期时间 为 1970.1.1 0:0:0 至 过期时间 当前的毫秒值 + 有效时间
Date expireDate = new Date(new Date().getTime() +3600*1000);
//秘钥及加密算法
Algorithm algorithm = Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE");
//设置头部信息
Map<String,Object> header = new HashMap<>();
header.put("typ","JWT");
header.put("alg","HS256");
//携带 id,账号信息,生成签名
token = JWT.create()
.withHeader(header)
.withClaim("id",admin.getId())
.withClaim("account",admin.getAccount())
.withExpiresAt(expireDate)
.sign(algorithm);
}catch (Exception e){
e.printStackTrace(); return null;
}
return token;
}
/**
* 验证 token 的方法
* @param token
* @return
*/
public static boolean verify(String token){
try {
//验签
Algorithm algorithm = Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE");
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) {//当传过来的 token 如果有问题,抛出异常
return false;
}
}
/**
* 获得 token 中 playload 部分数据,按需使用
* @param token
* @return
*/
public static DecodedJWT getTokenInfo(String token){
return JWT.require(Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE")).build().verify(token);
}
}
将token信息封装到admin对象里, 一并返回前端
前端将信息放入浏览器的会话存储里
请求发送时,后端验证token
先想想, 后端在接到前端请求时, 要验证token,可以写在一个过滤器里面, 除了登录请求外都要走这个过滤器, 那么问题来了, 如何使得这个过滤器除了登录请求外都走这个过滤器呢?
<1>过滤器的配置
我们可以通过给走这个过滤器的请求添加一层级的路径, 具体如下图
这样就可以区分经过验证token过滤器与不验证token过滤器的请求了, 不验证就直接写请求地址, 验证就加上一层级的/token
6.添加请求拦截器
由于我们的每一次请求都要将token信息发送给后端, 索性就放在响请求对象里的请求头的区域
那我们是不是每次请求都要设置请求头啊, 麻烦不麻烦
后端有没有将每次请求拦截下来的东西, 再设置请求头的内容, 你还别说还真有, 响应拦截器
这是
这是在每次请求时,将token放在请求的请求头区域里, 到后端浏览器调用
7. 响应拦截器
在前端处理后端响应回来的状态码时, 每一个组件对相同部分状态码总是同一个业务流程, 如: 500 状态码时候, 就会弹出服务器异常的信息, 那这样会繁琐很多, 使得代码冗余
在每次接收响应时, 可以根据状态码来做统一给用户做出响应结果, 可以将那些状态码放入响应拦截器里
8.Main组件添加子组件,来展示学生
- 首先定义组件
在Main组件点击学生管理, 就会到达studentList组件
List组件的生命周期函数mounted(页面加载完成调用), 写一个接收后端响应数据到Vue对象里的studentList的方法
- 注册组件
在router对象性里注册组件, 由于此时的组件是在Main组件里展示, 所以就要在Main组件下定义子组件
- 在Main组件(父组件)添加来展示不同组件
时间转化问题
由于后端从数据库从数据库接收到学生的注册信息, 会默认将数据库的Data类型转化long类型, 在渲染到前端, 就会是时间戳的格式, 如下图:
数据库里时间:
渲染到前端:
所以要格式化student类里oper_time(学生注册时间)的属性, 可以添加注解@JsonFornmat(),
这样在转json时候就会格式化, 响应到前端就会是 年月日 时分秒的格式
9.引入添加学生的功能
首先,在element-ui里引入功能的组件样式, 然后给组件添加事件, 事件就是打开一个类似于登录的表单, 点击保存之后, 向后端发送一个请求, 通过调取控制层,业务层,数据层将数据保存到数据库里
保存学生的表单,在选择专业时候, 专业不能是固定的, 不然以后更改专业还得改代码, 代码灵活度太低, 所以专业数据就要从数据库获取
点击调取openAddDiolog()方法, 将新增学生的弹窗设置可见(就展示出来)
导入组件
因为一个组件里写太多东西, 不方便改动, 那么就可以通过导入组件的方式将新增学生的内容写在另一个.vue组件里, 之后通过导入就可以了
Add组件:
dialogFormVisible:这是对话框的属性, 表示的是一个值, 这个值如果是true表示对话框可见, 如果false那就表示对话框是不可见, 可以通过调用之前的openAddDiolog()的方法来给他赋值在List组件里定义Add组件
点击新增就会调用openAddDiolog方法, 将新增学生弹窗改成可见
解决增加学生表单里的专业问题
在弹窗里的内容加载完成之后, 就调用生命周期函数, 将数据库里的专业信息展示到表单里
二、后端搭建
后端使用的是idea写代码工具, 集成了tomcat服务器
1. 项目构建
后端结构: 如下
2. 封装连接数据库的代码
由于要有许多的增删改查, 与数据库相关的操作, 所以就封装一下, 以后跟方便用
<1>首先先在src目录下新建一个. properties的文件
<2>新建一个工具类
工具类将加载驱动, 获取Connection连接对象, 释放资源一些操作封装成静态方法, 用的时候直接拿
就叫做JdbcUtil的类
public class JdbcUtil {
private static String dirver = null;
private static String url = null;
private static String username = null;
private static String password = null;
//获取数据库的外部文件
//这里放在静态代码块里, 在第一次new对象时候, 就需要加载驱动
static {
try {
//通过字节输入流来读取对应的properties文件
InputStream in = JdbcUtil.class.getClassLoader().getResourceAsStream("db.properties");
//再将读取到的文件放入到jdk里的properties对象里
Properties pop = new Properties();
//来获取外部的db.properties数据
pop.load(in);
//对应属性赋对象方法
dirver = pop.getProperty("driver");
url = pop.getProperty("url");
username = pop.getProperty("username");
password = pop.getProperty("password");
//加载驱动
Class.forName(dirver);
} catch (IOException | ClassNotFoundException e) {
System.out.println("java获取Connection对象错误");
e.printStackTrace();
}
}
//获取连接对象
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}
//释放连接对象的资源
public void relese(Connection con, Statement sta, ResultSet res){
if (res!=null){
try {
res.close();
} catch (SQLException throwables) {
System.out.println("sql接收数据集合ResultSet关闭异常");
throwables.printStackTrace();
}
}
if (sta!=null){
try {
sta.close();
} catch (SQLException throwables) {
System.out.println("sql接收数据集合ResultSet关闭异常");
throwables.printStackTrace();
}
}
if (con!=null){
try {
res.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
3. 登录判断账号
<1>与数据库交互的Dao层代码展示
_
LoginDao
public class LoginDao {
public Admin checkAccount(String account,String passWord) throws SQLException {
Connection connection = null;
PreparedStatement ps =null;
ResultSet resultSet = null;
Admin admin = null;
JdbcUtil jdbcUtil = new JdbcUtil();
//直接通过类调取方法来获取连接对象
connection = jdbcUtil.getConnection();
// ps = connection.prepareStatement("select account password from admin where account="+"\'"+account+"\'");
ps = connection.prepareStatement("select id, account from admin where account=? and password=?");
ps.setObject(1, account);
ps.setObject(2, passWord);
resultSet = ps.executeQuery();
//如果有结果, 说明有该账号
while(resultSet.next()){
admin = new Admin();
admin.setId(resultSet.getInt("id"));
admin.setAccount(resultSet.getString("account"));
break;
}
//释放连接数据库资源
jdbcUtil.relese(connection,ps,resultSet);
return admin;
}
}
这三个地方需要注意一下
<1>第一部分 在判断登录这个Dao里, 此方法不能仅仅判断账号密码是否正确, 还要获取返回的那个admin对象
在实际项目中, 难道后天不需要账户信息吗?
就拿淘宝例子来说把, 是不是当你买个东西之后,再次登录时候就给你推类似的一些东西, 这里就是后天已经记录了你买的东西, 所以登录时候就知道给你推相似的商品了<2>不能直接将用户的账号注入到SQL里查找
ps = connection.prepareStatement("select account password from admin where account="+"\'"+account+"\'");
这一行代码, 可能会让一些 偷奸耍滑 的人钻空子, 你想想, 直接获取到的账号信息放入sql语句查找,
如果有一个用户输入account: 1==1, 那你此处sql的where条件不就形同虚设了嘛, 随便进入管理系统, 那你就等着被炒鱿鱼把所以要用格式占位控制符来代替
ps = connection.prepareStatement("select id, account from admin where account=? and password=?");
ps.setObject(1, account);
ps.setObject(2, passWord);
这不就肥肠完美么
_
<2>业务处理层LoginService
public class LoginServlet extends HttpServlet {
@Override
public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException {
String account = req.getParameter("account");
String password = req.getParameter("password");
CommonRes commonRes = null;
try{
LoginDao loginDao = new LoginDao();
Admin admin = loginDao.checkAccount(account, password);
System.out.println(admin);
if (admin!=null){
//生成token
String token = JWTUtil.token(admin);
admin.setToken(token);
commonRes = new CommonRes(200,admin,"登录成功");
}else{
commonRes = new CommonRes(201,"账号或密码错误");
}
}catch(Exception e){
e.printStackTrace();
commonRes = new CommonRes(500,"服务器异常");
}
//将common结果转化成jason格式返回
resp.setContentType("text/html;charset=UTF-8");
Writer writer = resp.getWriter();
ObjectMapper mapper = new ObjectMapper();
String jsonstr = mapper.writeValueAsString(commonRes);
writer.write(jsonstr);
}
代码解读
由于前端用的是axios的请求发送框架, 所以数据格式在经过服务器内部的一系列处理之后, 就会变成json的键值对格式,
所以后端可以直接 用 req.getParameter("account");获取对应参数值
String account = req.getParameter("account");
String password = req.getParameter("password");
3. 解决前后端编码不一致的问题问题
在前端向后端发送带参数的请求, 后端获取时, 浏览器默认发送的是 ISO8859-1格式的编码 , 而这种编码格式是不能解析中文的, 所以就有乱码的出现
也就是你在request,getParameter("参数名")时候, 会是乱码
所以在获取参数之前,要对请求 设置编码request.setContetnType("text/html;charset=utf-8");
, 在请求头设置编码
同理, 后端服务器给前端做出响应的时候也应该设置响应编码, 否则会出现前端接收响应时, 响应的是乱码问题response.setContetnType("text/html;charset=utf-8")
由于每次接收请求与响应数据都要设置编码格式, 所以就可以将上面两行代码放到过滤器里面
5. 解决跨域问题
跨域是指从一个域名的网页去请求另一个域名的资源。比如从www.baidu.com 页面去请求 www.google.com 的资源。但是一般情况下不能这么做,它是由浏览器的同源策略造成
所以可以告诉浏览器可以访问, 可以在响应头里设置
我从后端来解决这个问题
由此我们可以写过一个过滤器来实现
public class CorsFilter implements Filter {
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
//允许携带Cookie时不能设置为* 否则前端报错
httpResponse.setHeader("Access-Control-Allow-Origin", httpRequest.getHeader("origin"));//允许所有请求跨域
httpResponse.setHeader("Access-Control-Allow-Methods", "*");//允许跨域的请求方法GET, POST, HEAD 等
httpResponse.setHeader("Access-Control-Allow-Headers", "*");//允许跨域的请求头
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");//是否携带cookie
filterChain.doFilter(servletRequest, servletResponse);
}
}
并且在web.xml里配置哪些业务经过 过滤器
<filter>
<filter-name>corsFilter</filter-name>
<filter-class>com.jwq.webback.filter.CorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>corsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
6. 后端对返回结果进行封装
由于每次对后端结果进行返回时候,只能一个一个返回, 所以此时可以封装一个返回的结果集
commonRes
public class CommonRes {
private int code;
private Object data;
private String message;
public CommonRes(int code, Object data, String message) {
this.code = code;
this.data = data;
this.message = message;
}
public CommonRes(int code, String message) {
this.code = code;
this.message = message;
}
//在转化json格式要通过反射机制用set, get方法来取值
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
代码解析
注意: 这里必须是有set,get方法, 因为在后面的json返回格式, 必须要通过反射机制转化json格式
7. 后端对返回结果进行转化json格式后进行响应
<1>首先要导入jar包
<2>在使用mapper对象里的writeValuesAsString()对响应结果进行json格式的处理
Writer writer = resp.getWriter();
ObjectMapper mapper = new ObjectMapper();
//commonRes这是上面说的返回结果集对象,放入mapper对象的
//方法里,转json格式
String jsonstr = mapper.writeValueAsString(commonRes);
writer.write(jsonstr);
<3>json响应结果查看
注意: 这里的message响应的是中文, 所以要设置响应头编码
可以设置一个过滤器, 将请求编码与响应编码一起设置了
public class EncodFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletRequest.setCharacterEncoding("utf-8");
HttpServletResponse response = (HttpServletResponse)servletResponse;
response.setContentType("text/html;charset=UTF-8");
//让进入到过滤器里的请求, 离开当前的过滤器, 继续向后执行, 后一个可能是过滤器,也可以是servlet
filterChain.doFilter(servletRequest,servletResponse);
}
}
8. List组件展示学生操作的处理代码(Contral层, service层, dao层)
由于学生相关的操作用的是一个service处理层, 在相同的get或者post请求下, 后端就要区分不同请求了,可以在前端发送请求时候给一个标记, mark
后端获取参数进行, 进行判断
_
展示学生的service业务层(暂时Contral与service层不分离)
/**
* 展示学生的方法
* @return
* @throws SQLException
*/
public List outStudent() throws SQLException {
StudentDao studentDao = new StudentDao();
List<Student> students = studentDao.outStudent();
return students;
}
dao层
/**
* 查询所有学生的方法
* @return
* @throws SQLException
*/
public List<Student> outStudent() throws SQLException {
List<Student> majors= new ArrayList<>();
JdbcUtil jdbcUtil = new JdbcUtil();
Connection connection = jdbcUtil.getConnection();
PreparedStatement ps = connection.prepareStatement("SELECT\n" +
"s.`num`,\n" +
"s.name,\n" +
"s.gender,\n" +
"s.address,\n" +
"s.phone,\n" +
"m.name mname,\n" +
"a.account aname,\n" +
"s.oper_time FROM student s\n" +
"LEFT JOIN major m\n" +
" ON s.majorid = m.id\n" +
"LEFT JOIN admin a\n" +
" ON s.adminid = a.id");
ResultSet resultSet = ps.executeQuery();
while(resultSet.next()){
Student major = new Student();
major.setNum(resultSet.getInt("num"));
major.setName(resultSet.getString("name"));
major.setGender(resultSet.getString("gender"));
major.setAddress(resultSet.getString("address"));
major.setPhone(resultSet.getString("phone"));
major.setMname(resultSet.getString("mname"));
major.setAname(resultSet.getString("aname"));
major.setOperTime(resultSet.getTimestamp("oper_time"));//年月日时分秒
majors.add(major);
}
//释放资源
jdbcUtil.relese(connection,ps,resultSet);
return majors;
}
9. 新增学生操作的处理代码(Contral层, service层, dao层)
10. 发送数据库的专业的代码
响应专业的业务层
/**
* 展示专业的方法
* @return
* @throws SQLException
*/
public List outMajor() throws SQLException {
StudentDao studentDao = new StudentDao();
List<Major> majors = studentDao.outMajor();
System.out.println(majors);
return majors;
}
dao层
/**
* 返回学生专业的方法
* @return
* @throws SQLException
*/
public List<Major> outMajor() throws SQLException {
List<Major> majors= new ArrayList<>();
JdbcUtil jdbcUtil = new JdbcUtil();
Connection connection = jdbcUtil.getConnection();
PreparedStatement ps = connection.prepareStatement("select id, name from major");
ResultSet resultSet = ps.executeQuery();
while(resultSet.next()){
Major major = new Major();
major.setNum(resultSet.getString("id"));
major.setName(resultSet.getString("name"));
majors.add(major);
}
//释放资源
jdbcUtil.relese(connection,ps,resultSet);
return majors;
}