一、在vue项目中安装axios
由于需要使用axios框架进行异步请求,所以需要在vue项目中安装axios框架。在官方下载速度较慢,所以选择更换镜像源(我使用的是华为云镜像)
在项目终端中输入npm config set registry http://mirrors.cloud.tencent.com/npm/
更换后通过在终端输入npm config get registry检查当前镜像源
二、前端向后端发送请求
用户在浏览器进行操作,而这些操作最开始到达的是前端。这时候需要前端向后端发出响应。
我们以一个vue项目的登录功能为例,当用户输入了账户密码并点击登录是,前端需要拿到账户密码的数据并向后端发送
<!-- 一个.vue文件是一个组件,可以理解为一个页面,但是和页面不同
内容都写在一个template标签中,
template标签必须有一个根标签
-->
<template>
<div class="login_container">
<!-- 登录盒子-->
<div class="login_box">
<!-- 头像盒子-->
<div class="img_box">
<img src="./assets/logo.png" />
</div>
<!-- 表单 -->
<div style="margin-top: 100px; padding-right: 50px;">
<el-form ref="form" label-width="80px">
<!-- 第一栏 -->
<el-form-item label="账号">
<el-input v-model="account"></el-input>
</el-form-item>
<!-- 第二栏 -->
<el-form-item label="密码" show-password>
<el-input v-model="password"></el-input>
</el-form-item>
<!-- 按钮 -->
<el-form-item>
<el-button type="primary" @click="save()">登录</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</template>
<script>
/* 导出组件,并为组件定义数据,函数,生命周期函数 */
export default{
data(){
return{
account:'admin',
password:'111'
}
},
methods:{
save(){
if(this.account.length==0){
this.$message({
message: '账号不得为空',
type: 'warning'
});
return;
}
if(this.password.length==0){
this.$message({
message: '密码不得为空',
type: 'warning'
});
return;
}
// 向后端交互"/login","account="+this.account+"&password="+this.password
this.$http.post("login","account="+this.account+"&password="+this.password).then((resp)=>{
/* 后端响应的结果 */
})
}
}
}
</script>
<style>
.login_container{
height: 100vh;
margin: 0px;
padding: 0px;
background-image: url(assets/bg.png);
}
.login_box{
width: 450px;
height: 350px;
background-color: #fff;
border-radius: 10px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
opacity: 0.95;
}
.img_box{
width: 130px;
height: 130px;
position: absolute;
left: 50%;
transform: translate(-50%,-50%);
background-color: #fff;
border-radius: 50%;
padding: 5px;
border: 1px solid #eee;
}
.img_box img{
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #eee;
}
</style>
三、后端请求的处理
后端处理主要分成三个步骤,简单来说就是"接化发"。
接是接收来自前端的请求,化是在后端进行处理,发是对前端进行响应。这三个步骤也是后端的基本作用
登录servlet
package com.wbc.dorm.web;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wbc.dorm.dao.LoginDao;
import com.wbc.dorm.model.Admin;
import com.wbc.dorm.model.Result;
import com.wbc.dorm.util.JWTUtil;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/*登录处理servlet*/
@WebServlet(urlPatterns = "/login",name = "login", loadOnStartup = 1)
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
/*登录请求的账号密码*/
String account = req.getParameter("account");
String password = req.getParameter("password");
//System.out.println(account);
//System.out.println(password);
Result result = null;
PrintWriter printWriter = resp.getWriter();
/*调用dao层查询账号密码是否正确*/
try {
LoginDao loginDao = new LoginDao();
Admin admin = loginDao.login(account, password);
//System.out.println(admin.toString());
if (admin!=null){
//将admin放入标准化响应模型
result = new Result(200, "登陆成功", admin);
}
else {
result = new Result(201, "账号或密码错误", null);
}
} catch (Exception e) {
e.printStackTrace();
result = new Result(500, "系统忙", null);
}
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writeValueAsString(result);
printWriter.print(jsonString);
}
}
管理员模型
package com.wbc.dorm.model;
public class Admin {
private int id;
private String account;
private String password;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "Admin{" +
"id=" + id +
", account='" + account + '\'' +
", password='" + password + '\'' +
'}';
}
}
与数据库交互的dao
package com.wbc.dorm.dao;
import com.wbc.dorm.model.Admin;
import com.wbc.dorm.model.Admin;
import java.sql.*;
public class LoginDao {
public Admin login(String username, String password) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
String url ="jdbc:mysql://127.0.0.1:3306/dormdb?serverTimezone=Asia/Shanghai";//定义连接sql所需的url
String users ="root";//用户名
String passwords ="Wbc11280";//密码
//建立连接
Connection connection = DriverManager.getConnection(url,users,passwords);//建立连接
//预编译
PreparedStatement preparedStatement =connection.prepareStatement("select id,account from admin where account = ? and password = ?");
try{
//传入数据
preparedStatement.setObject(1, username);
preparedStatement.setObject(2, password);
//查询操作
ResultSet resultSet = preparedStatement.executeQuery();//将查询结构封装到ResultSet类型的对象中 需要将数据封装到指定类型的对象中
Admin admin = null;
while (resultSet.next()) {
admin = new Admin();
admin.setId(resultSet.getInt("id"));
admin.setAccount(resultSet.getString("account"));
// System.out.println(admin.toString());
}
return admin;
//return null;
}
finally {
if(preparedStatement != null){
preparedStatement.close();
}
if(connection != null){
connection.close();
}
}
}
}
四、前端接收响应并做处理
当后端程序向前端响应后,前端需要接收响应并作出响应的处理
// 向后端交互"/login","account="+this.account+"&password="+this.password
this.$http.post("login","account="+this.account+"&password="+this.password).then((resp)=>{
/* 后端响应的结果 */
if(resp.data.code==200){
sessionStorage.setItem("account",resp.data.data.account);//存储到绘画空间,浏览器内部存储
this.$router.push("/main");
}
else if(resp.data.code==201){
this.$message({message:resp.data.message,type:'warning'})
}
else if(resp.data.code==500){
this.$message({message:resp.data.message,type:'warning'})
}
})
根据后端传来的标准化响应模型对象中的状态码,来做出对用户的响应,如登陆成功跳转到主页面,登陆失败根据状态码给出相应提示
说明:
sessionStorage.setItem(key, value)可以将浏览器中的数据存储到浏览器中的会话空间,只要窗口打开,数据就可以一直使用,直到界面关闭或者通过sessionStorage.clear()方法手动清楚。会话空间的数据可以通过sessionStorage.getItem(key)方法取出,从而在整个项目中共享数据。
五、路由导航守卫
当我们有了登录验证功能之后,肯定不希望用户通过路由导航(直接输入网址)的方式越过登录直接进入到主页面。如果在每次跳转之后都进行判断则相当的麻烦
<template>
<div>
<el-container>
<el-header style="text-align: right; font-size: 12px">
<div class="header-title">后台管理系统</div>
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown" >
<el-dropdown-item>主页</el-dropdown-item>
<el-dropdown-item>修改密码</el-dropdown-item>
<el-dropdown-item><span @click="logout()">安全退出</span></el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<span>{{account}}</span>
</el-header>
<el-container>
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu :default-openeds="['1', '3']" router>
<el-submenu index="1">
<template slot="title"><i class="el-icon-message"></i>操作菜单</template>
<el-menu-item-group>
<el-menu-item index="/majorlist">专业管理</el-menu-item>
<el-menu-item index="/studentlist">学生管理</el-menu-item>
<el-menu-item index="1-3">学院管理</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
export default{
data(){
return{
account:"",
}
},
methods:{
logout(){
this.$confirm('您确定要退出么?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$message({
type: 'success',
message: '退出成功成功!'
});
sessionStorage.clear();
this.$router.push("/login")
})
}
},
mounted(){
this.account=window.sessionStorage.getItem("account");//将account存入路由器内部
console.log(this.account);
if(account==null){
this.$router.push("/login")
}
}
}
</script>
<style>
.el-header {
background-color: #00a7fa;
color: #333;
line-height: 60px;
}
.header-title{
width: 300px;
float: left;
text-align: left;
font-size: 20px;
color: white;
}
.el-main{
background-color: aliceblue;
height: 100vh;
}
</style>
需要在每一次跳转中都进行一次判断。
而我们所使用的axios框架为我们提出了更加方便的解决方法------路由导航守卫。我们可以通过在路由中配置路由导航守卫,在每一次跳转时自动执行代码,进行判断是否已经登录
在/router/index.js文件中定义路由组件代码下方加入如下代码
//配置路由导航守卫
//每当进行一次组件路由时,就会触发导航守卫
rout.beforeEach((to,from,next)=>{
if(to.path=='/login'){//如果用户访问的登录页,直接放行
return next();//继续访问
}
else{
var account = window.sessionStorage.getItem("account");//获取路由器中存储的管理员信息
if(account==null){//如果为空说明没有登录
return next("/login");//跳转到登陆页面
}
else{//已经登陆
next();//继续访问
}
}
})
如此我们就快要省去每次跳转判断的代码
<template>
<div>
<el-container>
<el-header style="text-align: right; font-size: 12px">
<div class="header-title">后台管理系统</div>
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown" >
<el-dropdown-item>主页</el-dropdown-item>
<el-dropdown-item>修改密码</el-dropdown-item>
<el-dropdown-item><span @click="logout()">安全退出</span></el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<span>{{account}}</span>
</el-header>
<el-container>
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu :default-openeds="['1', '3']" router>
<el-submenu index="1">
<template slot="title"><i class="el-icon-message"></i>操作菜单</template>
<el-menu-item-group>
<el-menu-item index="/majorlist">专业管理</el-menu-item>
<el-menu-item index="/studentlist">学生管理</el-menu-item>
<el-menu-item index="1-3">学院管理</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
export default{
data(){
return{
account:"",
}
},
methods:{
logout(){
this.$confirm('您确定要退出么?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$message({
type: 'success',
message: '退出成功成功!'
});
sessionStorage.clear();
this.$router.push("/login")
})
}
},
mounted(){
this.account=window.sessionStorage.getItem("account");//将account存入路由器内部
console.log(this.account);
/* if(account==null){
this.$router.push("/login")
} */
}
}
</script>
<style>
.el-header {
background-color: #00a7fa;
color: #333;
line-height: 60px;
}
.header-title{
width: 300px;
float: left;
text-align: left;
font-size: 20px;
color: white;
}
.el-main{
background-color: aliceblue;
height: 100vh;
}
</style>
六、web前后端之间的会话跟踪
当我们在登陆成功后需要进行其他操作时,前端会像后端发送http请求。但http请求是无状态的,因此后端不知道是谁在进行操作。对会话进行跟踪 就是为了解决这样的问题。
会话跟踪是Web程序中常用的技术,用来跟踪用户的整个会话过程。 给客户端们颁发一个通行证,每人一个,无论谁访问都必须携带自己通行证。 这样服务器就能从通行证上确认客户身份了。
我们大致可以将会话跟踪描述为下图的过程
当我们在
具体实现
(1)在登陆成功后,在后端为用户生成一个token字符串
token令牌,可以理解为身份证号,是该用户唯一标识字符串,通过这个字符串进行前后端验证。
我们可以通过jwt组件为管理员生成token令牌
说明:
token令牌分为三部分:声明、载荷、签证
声明包含着生成类型和加密算法的基本信息,载荷包含着用户信息。二者通过base64转码生成,此过程可逆,不包含加密,因此不建议在载荷中加入用户关键信息。
签证结合前两部分以及密钥,加密生成,故密钥十分重要
我们通过maven添加依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>
并在服务器中注册(web.xml)
<!--验证token过滤器-->
<filter>
<filter-name>tokenfilter</filter-name>
<filter-class>com.wbc.dorm.filter.AdminTokenFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>tokenfilter</filter-name>
<!--请求地址中有api地址的Servlet进入过滤器-->
<url-pattern>/api/*</url-pattern>
</filter-mapping>
由于登录界面不需要生成token,所以通过在其他配置信息的地址前添加/api来区分
将jwt的util稍作修改直接拿来用
package com.wbc.dorm.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.wbc.dorm.model.Admin;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT工具类
*/
public class JWTUtil {
/**
* 根据用户id,账号生成token
* @param admin
* @return
*/
public static String getToken(Admin admin) {
String token = "";
try {
//过期时间 为1970.1.1 0:0:0 至 过期时间 当前的毫秒值 + 有效时间
Date expireDate = new Date(new Date().getTime() + 10000*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)//设置token有效时间
.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);
}
}
添加Admin内的成员变量adminToken,并增加相应的get、set方法。
package com.wbc.dorm.model;
public class Admin {
private int id;
private String account;
private String password;
private String adminToken;
public String getAdminToken() {
return adminToken;
}
public void setAdminToken(String adminToken) {
this.adminToken = adminToken;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "Admin{" +
"id=" + id +
", account='" + account + '\'' +
", password='" + password + '\'' +
'}';
}
}
并在登录servlet中添加相应代码以生成token
package com.wbc.dorm.web;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wbc.dorm.dao.LoginDao;
import com.wbc.dorm.model.Admin;
import com.wbc.dorm.model.Result;
import com.wbc.dorm.util.JWTUtil;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/*登录处理servlet*/
@WebServlet(urlPatterns = "/login",name = "login", loadOnStartup = 1)
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
/*登录请求的账号密码*/
String account = req.getParameter("account");
String password = req.getParameter("password");
//System.out.println(account);
//System.out.println(password);
Result result = null;
PrintWriter printWriter = resp.getWriter();
/*调用dao层查询账号密码是否正确*/
try {
LoginDao loginDao = new LoginDao();
Admin admin = loginDao.login(account, password);
//System.out.println(admin.toString());
if (admin!=null){
//登陆成功后,为admin生成一个token字符串
//使用jwt组件为管理员生成公私密钥
String adminToken = JWTUtil.getToken(admin);
//将adminToken放入admin
admin.setAdminToken(adminToken);
//将admin放入标准化响应模型
result = new Result(200, "登陆成功", admin);
}
else {
result = new Result(201, "账号或密码错误", null);
}
} catch (Exception e) {
e.printStackTrace();
result = new Result(500, "系统忙", null);
}
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writeValueAsString(result);
printWriter.print(jsonString);
}
}
(2)在浏览器中存储token,每次发送请求时将身份码一同发出
在登录时,接收后端相应处,添加接收身份码的代码,并将其添加到会话空间
sessionStorage.setItem("adminToken",resp.data.data.adminToken);
在我们操作发送请求时,如果在发送请求时,通过字符串拼接将身份码一同发于后端,实在过于麻烦,幸好axios为我们提供了请求拦截的功能
在main.js中添加如下代码,可以将身份码添加到请求头中,直接发送
//axios 请求拦截
axios.interceptors.request.use(config =>{
//为请求头对象,添加Token验证的token字段
config.headers.adminToken = window.sessionStorage.getItem('adminToken');
return config;
});
(3)在后端创建一个过滤器,用于检测token的正确性,正确则退出过滤器继续响应,错误则直接向前端响应并给出状态码和提示信息
package com.wbc.dorm.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wbc.dorm.model.Result;
import com.wbc.dorm.util.JWTUtil;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;
public class AdminTokenFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//父类向子类转换(强制类型转换)
HttpServletRequest request = (HttpServletRequest) servletRequest;
String adminToken = request.getHeader("adminToken");
//验证token
Boolean res= JWTUtil.verify(adminToken);
System.out.println(res);
//处理验证结果
if(res){
filterChain.doFilter(request, servletResponse);//离开过滤器继续向下执行
}
else{
//向前端进行响应
Result result = new Result(401,"token验证失败,请重新登录",null);
PrintWriter printWriter = new PrintWriter(servletResponse.getWriter());
printWriter.write(new ObjectMapper().writeValueAsString(result));
}
}
}
在前端中main.js中添加响应拦截器(类似于javaEE中的过滤器),用于拦截后端的身份码错误响应
// 添加响应拦截器
axios.interceptors.response.use((resp) =>{
//正常响应拦截
if(resp.data.code==500){
ElementUI.Message({message:resp.data.message,type:"error"});
return;
}
if(resp.data.code==401){
ElementUI.Message({message:resp.data.message,type:"error"});
sessionStorage.clear();
router.replace("/login");
}
return resp;
});