主要内容:使用JWT令牌和过滤器实现登录校验
原理:jwt令牌是一串简单的字符串,定义了简答,自包含的格式,通信双方以json格式安全地传输信息。因为有数字签名的存在,这些信息是可靠的。jwt令牌通常包含头部,载荷和数字签名。其具体应用场景一般是登录验证,流程为浏览器发生登录请求,登录成功后,服务端生成jwt令牌并下发给浏览器,后续浏览器的每次请求都要携带jwt令牌,服务端统一拦截所有请求,并检查是否携带jwt令牌以及是否有效,若有效,则允许访问web资源,若非法,则返回错误响应信息。其中,判断jwt令牌是否有效的环节通常有过滤器来完成。过滤器是javaweb三大组件之一,可以拦截所有请求,并实现某些特殊的通用的功能,如登录校验。其流程是拦截请求,执行放行前逻辑,放行,然后访问web资源,返回过滤器,执行放行后逻辑,通常配合webfilter注解和servletconponentscan注解,实现对指定路径进行拦截和对servlet组件支持。另外,可以配置多个过滤器,组成过滤器链,实现更复杂的功能。
目录结构
FilterUtils
package com.example.sun.utils;
import com.alibaba.fastjson.JSONObject;
import com.example.sun.pojo.Result;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.io.IOException;
@WebFilter(urlPatterns = "/*")
@Slf4j
public class FilterUtils implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req=(HttpServletRequest) servletRequest;
HttpServletResponse res=(HttpServletResponse) servletResponse;
String url=req.getRequestURI().toString();
if(url.contains("login")){
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if(url.contains("add")){
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if(url.contains("edit")){
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if(url.contains("show")){
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String jwt=req.getHeader("token");
log.info(jwt);
if(!StringUtils.hasLength(jwt)){
Result error=Result.error("NotLoginjwt");
String notLogin= JSONObject.toJSONString(error);
res.getWriter().write(notLogin);
return;
}
try{
JwtUtils.parseJWT(jwt);
} catch(Exception e){
e.printStackTrace();
Result error=Result.error("NotLoginerror");
String notLogin= JSONObject.toJSONString(error);
res.getWriter().write(notLogin);
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
JwtUtils
package com.example.sun.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
private static String signKey="sunfan";
private static Long expire =43200000L;
/*生成JWT令牌*/
public static String generateJwt(Map<String,Object> claims){
String jwt= Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256,signKey)
.setExpiration(new Date(System.currentTimeMillis()+expire))
.compact();
return jwt;
}
/*解析JWT令牌*/
public static Claims parseJWT(String jwt) {
Claims claims=Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
poetcontroller
package com.example.sun.controller;
import com.example.sun.pojo.Poet;
import com.example.sun.pojo.Result;
import com.example.sun.pojo.User;
import com.example.sun.service.PoetserviceA;
import com.example.sun.pojo.Poetbean;
import com.example.sun.utils.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
public class poetcontroller {
@Autowired
private PoetserviceA poetserviceA;
@GetMapping("/findCondition/{page}/{pageSize}")
/*@GetMapping("/poet")*/
public Result select(@PathVariable Integer page,
@PathVariable Integer pageSize,
String name,String style) {
Poetbean poetbean=poetserviceA.page(page,pageSize,name,style);
return Result.success(poetbean);
}
@GetMapping("/poet/{page}/{pageSize}")
/*@GetMapping("/poet")*/
public Result select( @PathVariable Integer page,
@PathVariable Integer pageSize) {
Poetbean poetbean=poetserviceA.page(page,pageSize);
return Result.success(poetbean);
}
@RequestMapping("/delete")
public int deletePoet(Integer id){
return poetserviceA.delete(id);
}
@RequestMapping("/update")
public Result updatePoet(@RequestBody Poet poet){
boolean r = poetserviceA.update(poet);
if(r) {
// 成功 code==1
return Result.success();
} else {
// 失败 code==0
return Result.error("更新失败");
}
}
@RequestMapping("/findById/{id}")
public Result poetfindById(@PathVariable("id") Integer id) {
return Result.success(poetserviceA.poetfindById(id));
}
@RequestMapping("/insert")
public Result insert( @RequestBody Poet poet){
boolean result = poetserviceA.insert(poet);
if(result) {
// 成功 code==1
return Result.success();
} else {
// 失败 code==0
return Result.error("添加失败");
}
}
@PostMapping("/login")
public Result login(@RequestBody User user) {
//调用业务层:登录功能
User loginUser=poetserviceA.getByNameAndPassword(user);
//判断:登录用户是否存在
if(loginUser !=null ){
//自定义信息
Map<String , Object> claims = new HashMap<>();
claims.put("password",loginUser.getPassword());
claims.put("name",loginUser.getName());
//使用JWT工具类,生成身份令牌
String token = JwtUtils.generateJwt(claims);
System.out.println(token);
Map<String, String> response = new HashMap<>();
response.put("token", token);
return Result.success(token);
}
return Result.error("用户名或密码错误");
}
}
show.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>查询出所有并分页控件显示</title>
<link rel="stylesheet" href="js/element.css">
</head>
<body style="align:center">
<div id="app" style="width: 80%;align:center" >
<div >
<h1 align="center">诗人信息</h1>
<p align="center">
<el-form :inline="true" :model="formInline" class="demo-form-inline">
<el-form-item label="姓名">
<el-input v-model="formInline.name" placeholder="姓名"></el-input>
</el-form-item>
<el-form-item label="风格">
<el-input v-model="formInline.style" placeholder="风格"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="onSubmit" >查询</el-button>
</el-form-item>
<el-form-item>
</el-form-item>
<el-form-item>
</el-form-item>
<el-form-item>
</el-form-item>
<el-form-item>
</el-form-item>
<el-form-item>
</el-form-item>
<el-form-item>
</el-form-item>
<el-form-item>
<el-button type="danger" icon="el-icon-switch-button" @click="logout">退出</el-button>
</el-form-item>
</el-form>
</p>
<p align="right">
<el-form :inline="true" :model="formInline" class="demo-form-inline">
<el-form-item>
<el-button type="primary" icon="el-icon-plus" @click="insert">添加</el-button>
</el-form-item>
</el-form>
</p>
<el-table
:data="tableData.filter(data => !search || data.name.toLowerCase().includes(search.toLowerCase()))"
style="width: 100%;align:center;font-size: 20px">
<el-table-column
label="id"
prop="id">
</el-table-column>
<el-table-column
label="姓名"
prop="name">
</el-table-column>
<el-table-column
label="性别"
prop="gender">
</el-table-column>
<el-table-column
label="朝代"
prop="dynasty">
</el-table-column>
<el-table-column
label="头衔"
prop="title">
</el-table-column>
<el-table-column
label="风格"
prop="style">
</el-table-column>
<el-table-column
align="right">
<template slot="header" slot-scope="scope">
<el-input
v-model="search"
size="mini"
placeholder="输入关键字搜索"/>
</template>
<template slot-scope="scope">
<el-button size="mini"
@click="handleEdit(scope.row.id)">编辑</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<p align="center">
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[3, 5, 10, 20]"
:page-size="pageSize"
:total="total">
</el-pagination>
</p>
</div>
<!-- <div v-else-if="tableData1.code==0" >-->
<!-- {{tableData1.code}}无权限访问-->
</div>
<!-- 引入组件库 -->
<script src="js/jquery.min.js"></script>
<script src="js/vue.js"></script>
<script src="js/element.js"></script>
<script src="js/axios-0.18.0.js"></script>
<script>
new Vue({
el:"#app",
data: {
search: '',
currentPage: 1,
pageSize: 3,
total: null,
token:localStorage.getItem("token"),
tableData: [],
tableData1:[],
tableData2:[],
formInline: {
name: '',
style: ''
}
},
methods: {
logout(){
window.open('http://localhost:8080/login.html')
},
insert(){
window.open('http://localhost:8080/add.html')
},
handleEdit(id) {
window.open('http://localhost:8080/edit.html?id='+id)
console.log(index, row);
},
handleDelete(id) {
var _thisd = this;
if (window.confirm("确定要删除该条数据吗???")) {
axios.post('/delete?id=' + id,{},{headers:{"token":this.token}})
.then(function (response) {
alert("删除成功")
_thisd.findAll();
})
.catch(function (error) {
console.log(error);
});
}
console.log(index, row);
},
handleSizeChange(val) {
this.pageSize = val;
this.findAll();
console.log(`每页 ${val} 条`);
},
handleCurrentChange(val) {
this.currentPage = val;
this.findAll();
console.log(`当前页: ${val}`);
},
onSubmit() {
var url = `/findCondition/${this.currentPage}/${this.pageSize}?name=${encodeURIComponent(this.formInline.name)}&style=${encodeURIComponent(this.formInline.style)}`
console.log(this.formInline.name);
console.log(this.formInline.style);
axios.get(url,{headers:{"token":this.token}}
)
.then(res =>{
this.tableData = res.data.data.rows;
this.total=res.data.data.total;
console.log(this.tableData);
console.log(this.total);
console.log(this.token);
})
.catch(error=>{
console.error(error);
})
},
findAll() {
/*var url1 = `/index1`
axios.get(url1)
.then(response => {
this.tableData1 = response.data;
console.log(this.tableData1);
})
.catch(error=>{
console.error(error);
})*/
var url = `/poet/${this.currentPage}/${this.pageSize}`
axios.get(url,{headers:{"token":this.token}})
.then(res => {
this.tableData = res.data.data.rows;
this.total = res.data.data.total;
console.log(this.tableData);
console.log(this.total);
})
.catch(error => {
console.error(error);
})
},
},
created(){
this.findAll();
}
})
</script>
</body>
</html>
add.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="./js/vue.js"></script>
<script src="./js/axios-0.18.0.js"></script>
</head>
<body>
<div id="app">
<table border="1">
<tr>
<td>id</td>
<td><input type="text" v-model="poet.id"> </td>
</tr>
<tr>
<td>姓名</td>
<td><input type="text" v-model="poet.name"> </td>
</tr>
<tr>
<td>性别</td>
<td><input type="text" v-model="poet.gender"></td>
</tr>
<tr>
<td>朝代</td>
<td><input type="text" v-model="poet.dynasty"></td>
</tr>
<tr>
<td>头衔</td>
<td><input type="text" v-model="poet.title"></td>
</tr>
<tr>
<td>风格</td>
<td><input type="text" v-model="poet.style"></td>
</tr>
<tr>
<td></td>
<td><input type="button" @click="addpoet" value="增加"> </td>
</tr>
</table>
</div>
</body>
<script>
new Vue({
el: '#app',
data: {
token:localStorage.getItem("token"),
poet: {
"id":"",
"name":"",
"gender":"",
"dynasty":"",
"title":"",
"style":""
} //详情
},
methods: {
addpoet() {
var url = 'insert'
axios.post(url,this.poet,{headers:{"token":this.token}})
.then(res => {
var baseResult = res.data
if(baseResult.code == 1) {
// 成功
location.href = 'show.html'
} else {
// 失败
alert(baseResult.message)
}
})
.catch(err => {
console.error(err);
})
}
},
})
</script>
</html>
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录后信息放在session中</title>
<!-- 引入组件库 -->
<script src="js/jquery.min.js"></script>
<script src="js/vue.js"></script>
<script src="js/element.js"></script>
<script src="js/axios-0.18.0.js"></script>
<link rel="stylesheet" href="js/element.css">
</head>
<body>
<div id="app" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">
<el-alert
v-if="showError"
title="账号或密码错误"
type="error"
center
show-icon
style="position: absolute; top: 50%; left: 50%; transform: translate(-42%, -400%);">
</el-alert>
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="姓名">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit" style="font-size: 20px; padding: 10px 20px; width: 200px;">登录</el-button>
</el-form-item>
</el-form>
</div>
<script>
new Vue({
el:"#app",
data:{
showError: false,
form: {
name: '',
password: ''
},
tableData: []
},
methods:{
onSubmit() {
var url = `/login`
console.log(this.form.name);
console.log(this.form.password);
axios.post(url, {
name: this.form.name,
password: this.form.password
})
.then(response => {
this.tableData = response.data;
console.log(this.tableData);
const token=response.data.data;
localStorage.setItem("token",token);
console.log(token);
if(this.tableData.data!=null) {
location.href = 'show.html'
}
if(response.data.code==0){
this.tableData=response.data;
this.showError=true;
setTimeout(() => {
this.showError = false; // 隐藏错误提示
}, 3000);
}
})
.catch(error=>{
console.error(error);
})
}
}
})
</script>
</body>
</html>
edit.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="./js/vue.js"></script>
<script src="./js/axios-0.18.0.js"></script>
</head>
<body>
<div id="app">
<table border="1">
<tr>
<td>编号</td>
<td><input type="text" v-model="this.id"> </td>
</tr>
<tr>
<td>姓名</td>
<td><input type="text" v-model="poet.name"> </td>
</tr>
<tr>
<td>性别</td>
<td>
<input type="text" v-model="poet.gender" >
</td>
</tr>
<tr>
<td>朝代</td>
<td><input type="text" v-model="poet.dynasty"> </td>
</tr>
<tr>
<td>头衔</td>
<td><input type="text" v-model="poet.title"> </td>
</tr>
<tr>
<td>风格</td>
<td><input type="text" v-model="poet.style"> </td>
</tr>
<tr>
<td></td>
<td><input type="button" @click="updatePoet" value="更新"> </td>
</tr>
</table>
</div>
</body>
<script>
new Vue({
el: '#app',
data: {
token:localStorage.getItem("token"),
id: '',
poet: {}, //详情
},
methods: {
selectById() {
//${this.id}
var url = `findById/${this.id}` //注意这里是反引号
//反引号(backticks,也称为模板字符串或模板字面量)是ES6(ECMAScript 2015)中引入的一种新字符串字面量功能,
// 它允许您在字符串中嵌入表达式。反引号用`(键盘上通常位于Tab键上方)来界定字符串的开始和结束。
axios.get(url,{headers:{"token":this.token}})
.then(response => {
var baseResult = response.data
if(baseResult.code == 1) {
this.poet = baseResult.data
}
})
.catch( error => {})
},
updatePoet() {
var url = 'update'
axios.put(url,this.poet,{headers:{"token":this.token}})
.then(res => {
var baseResult = res.data
if(baseResult.code == 1) {
// 成功
location.href ='show.html'
} else {
// 失败
alert(baseResult.message)
}
})
.catch(err => {
console.error(err);
})
},
},
created() {
// 获得参数id值
this.id = location.href.split("?id=")[1]
// 通过id查询详情
this.selectById()
},
})
</script>
</html>
效果展示
未登录时访问
登录后访问
说明
需要注意的时候,访问html页面,本身也是一种请求,不是所有的请求只在controller中体现。开始的时候没有把访问html页面作为一种请求,导致登录成功后跳转到show.html页面时,页面始终返回未登录信息,没有组件和数据展示。后续在FilterUtils通过编写以下代码解决:
尽管如此,项目仍存在问题,一方面是前端页面跳转的实现有待优化;另一方面后端过滤器对截取特定路径的控制有待简化。