引言
本博客的主要目的:收集个人写毕设所采用的网上的各种种好的课程、解决问题的办法,以及个人的思路和写毕设所学习到的知识。不说多用有用吧,希望能帮到快毕业又对毕业设计无从下手的患兄难弟......
列举下技术:
Spring Boot + Mybatis(自己喜欢写SQL,所以就没用MP)搭建后端框架,采用Spring Security做安全框架,结合JWT进行身份验证,区分角色和权限。
前端用Vue + Element UI搭建,Axios做网络请求...(前端会的不多)
前后端分离项目。
注意:本博客只提供个人的毕设设计思路、学习路径和遇到的问题以及如何解决,提供少量代码。
效果图
先声明这是份初稿,然后,我又重写了一份前、后端,请往下看。其次,由于我毕业答辩还没答呢,所以就不发所有的图了0.0,望体谅。
(毕业设计,也是帮学校写的项目,打马赛克别介意0.0)
正文
好的进入正题。
(1)写毕业设计的出发点
首先明白“巧妇难为无米之炊”,什么都不会是根本做不出来东西的。做毕业设计之前必须自己肚子里有墨水,不然给你个项目改都没法改。
工欲善其事必先利其器:
IDEA:IntelliJ IDEA – 领先的 Java 和 Kotlin IDE (jetbrains.com)
Another Redis Desktop Manager:Another Redis Desktop Manager | 更快、更好、更稳定的Redis桌面(GUI)管理客户端,兼容Windows、Mac、Linux,性能出众,轻松加载海量键值 (goanother.com)
MySQL:MySQL
NodeJS:Node.js (nodejs.org)
VSCode:Visual Studio Code - Code Editing. Redefined
Snipaste:Snipaste
(2)基本框架的搭建
看效果图就知道,我参考的是谁的课程。
这老师讲的非常好,我当时感觉找到宝藏一样,想着看完做出来就毕业。然后呢?然后呢?Spring Security的讲解给我搞蒙了,还有最后边的前端权限设定只说CV就行,我真服了,刚好前端不好,看不懂,看不懂意味着答辩没法说==寄。当然白嫖来的课程,老师讲的都已经非常不错了,打开了我后端权限控制的思路。
所以,说下个人的解决办法。我找了好久好久,看了B站所有的关于Spring Security的视频,尚硅谷、黑马、狂神...,给我整不自信了,听不懂(应该是我笨)。然后找到了”三更草堂“,我觉得讲的最好的Spring Security视频。
链接:SpringSecurity框架教程-Spring Security+JWT实现项目级前端分离认证授权-B站最通俗易懂的Spring Security课程_哔哩哔哩_bilibili
看完就解决Spring Security问题。
不过又引出新的问题:JWT。
由于之前写的是SSM项目,都是单体式项目,完全没接触过JWT,所以根部不知道这玩意是啥,就网上搜啊搜啊,(不多说那种枯燥和搜到的结果驴唇不对马嘴),有几篇比较好的分享出来:
JWT介绍:(6条消息) JWT详解_baobao555#的博客-CSDN博客_jwt
JWT工具类:(6条消息) java-jwt工具类_Tlimited的博客-CSDN博客
讲下上边链接里那个JWT工具类:那里边密钥是自动生成了,只用明白在Token有效期内,只要后端服务不关闭,它就一直有效就行。
弄完了这个,就可以继续看尚硅谷的视频了。
由于前端接触不深,所以那视频中的前端权限控制先放一下。
然后,再说说那视频中用到的前端框架:Vue Admin Template
写的是真的好啊,封装又封装,跟后端那个框架若依一样,全是大佬写的。
可是呢?笨笨的我看不懂。所以产生了重写前后端的想法。
(3)个人重写搭建后端基本框架
IDEA创SpringBoot项目,依赖我给大家粘一下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.29</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
应该就这几个包。
配置文件(注意改一下个人的密码、端口信息):
server:
port: 9090
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/workload_system?useUnicode=true&characterEncoding=utf-8
username: root
password:
redis:
host:
port: 6379
password:
lettuce:
pool:
# 最大连接
max-active: 8
# 最大空闲连接
max-idle: 8
# 最小空闲连接
min-idle: 0
# 连接等待时间
max-wait: 100ms
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mybatis:
type-aliases-package: com.hlw.ac.entity
mapper-locations: classpath*:mappers/**/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
file:
name: log
level:
com.hlw.ac: debug
这些做完基本上后台就搭好了。
相信看到这的兄弟已经看完了三更草堂的Spring Security。
接下来就很简单了,照着视频里面把对应的配置搬过来就行。
当然,我的配置跟他可能有点区别,后边给大家看一下。
先理一下思路:我们搭好了后台,做好了配置,现需要什么?
- Redis工具类、配置类,方便我们操作。
- JWT工具类(上边有链接,自行复制),JWT拦截器JwtAuthenticationTokenFilter。
- 后端返回值封装类ResultUtils。
- 自定义异常枚举类GlobalExceptionMsg、自定义全局异常类GlobalException、全局异常处理类GlobalExceptionHandler。
- 跨域请求配置类CorsConfig。
- Spring Security配置类SecurityConfig。
应该就这么多。少了我再补上。
好,咱一个一个来。
Redis工具类:这个直接网上搜,没啥技巧,CV战士嘛,自己写有点费时间。
推个好用的:
(6条消息) Redis常用工具类(详细、完整)_程序员coderQ的博客-CSDN博客_redis工具类
JWT:(6条消息) java-jwt工具类_Tlimited的博客-CSDN博客
JWT拦截器:
忘记看谁的课程了(应该是三更草堂的),讲一下思路:首先Spring Security就是一组拦截器,使用JWT后,先要进行Token认证,看看请求中有没有Token。
有:验证Token合法性,通过就往下走,不通过就抛异常,给前端发错误状态码、信息等等。
无:JWT拦截器放行该请求就行。
为啥放行?因为有Spring Security做兜底工作,它会看看这是不是咱Spring Security配置类中默认可以匿名访问的请求,是的话就不用Token也能放行,不是的话就拦截掉,说你没Token,给前端发消息,前端跳到登录页...
代码发的链接课程中有,这个就不发了。
后端返回值封装类以及异常:
推一个课程:
springboot如何优雅地统一异常处理和信息返回_哔哩哔哩_bilibili
看完就会了。
当然,状态码这种玩意,我还是发给大家,手写纯浪费时间。
首先了解下常用状态码:
/**
* 请求成功。
* */
OK(200, "操作成功。"),
/**
* 该请求已成功,并因此创建了一个新的资源。这通常是在 POST 请求,或是某些 PUT 请求之后返回的响应。
* PS:新增的时候用。
* */
CREATED(201, "操作成功。"),
/**
* 对于该请求没有的内容可发送,但头部字段可能有用。
* 用户代理可能会用此时请求头部信息来更新原来资源的头部缓存字段。
* PS:删除时候用。
* */
NO_CONTENT(204, "操作成功。"),
/**
* 由于被认为是客户端错误(例如,错误的请求语法、无效的请求消息帧或欺骗性的请求路由),
* 服务器无法或不会处理请求。
* */
BAD_REQUEST(400, "参数错误。"),
/**
* 客户端必须对自身进行身份验证才能获得请求的响应。
* PS:没登陆,Token失效、未通过认证...
* */
UNAUTHORIZED(401, "认证失败。"),
/**
* 客户端没有访问内容的权限;也就是说,它是未经授权的,因此服务器拒绝提供请求的资源。
* 与 401 Unauthorized 不同,服务器知道客户端的身份。
* PS:没资格访问内容。
* */
FORBIDDEN(403, "无权访问。"),
/**
* 该状态码表示服务器上无法找到请求的资源。除此之外,也可以在服务器端拒绝请求且不想说明理由时使用。
* */
NOT_FOUND(404, "资源丢失~"),
/**
* 服务器遇到了不知道如何处理的情况。
* */
INTERNAL_SERVER_ERROR(500, "服务器走神了~"),
/**
* 服务器不支持请求方法,因此无法处理。
* */
NOT_IMPLEMENTED(501, "请求出错。"),
/**
* 此错误响应表明服务器作为网关需要得到一个处理这个请求的响应,但是得到一个错误的响应。
* PS:服务器配置可能出错。
* */
BAD_GATEWAY(502, "服务器出错。"),
/**
* 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中
* */
SERVICE_UNAVAILABLE(503, "服务器出错。"),
;
(3条消息) HTTP状态码(完整版)_超级字节码的博客-CSDN博客
HTTP 响应状态码 - HTTP | MDN (mozilla.org)
其次,异常枚举类信息状态码:
SUCCESS(200, "操作成功。"),
CREATED_SUCCESS(201, "添加成功。"),
NO_CONTENT_SUCCESS(204, "删除成功。"),
EXIST_SUBMENU(201, "请先删除子菜单!"),
DATA_ERROR(400, "参数异常。"),
VERIFICATION_CODE_INVALID(400, "验证码已过期。"),
VERIFICATION_CODE_ERROR(400, "验证码错误。"),
VALID_DATA_ERROR(400, "参数校验异常。"),
USER_LOCK_ERROR(400, "该账号涉嫌违规,已被封禁,用户已被强制登出。"),
USER_PASSWORD_ERROR(400, "账号密码错误。"),
TOKEN_NOT_NULL_ERROR(401, "Token凭证不能为空,请重新登录获取。"),
TOKEN_LOSE_ERROR(401, "Token认证失败,请重新登录获取。"),
ACCOUNT_LOCK(401, "该账号被锁定,请联系系统管理员。"),
ACCOUNT_HAS_DELETED_ERROR(401, "该账号已被删除,请联系系统管理员。"),
TOKEN_PAST_DUE(401, "token失效,请刷新token。"),
NOT_PERMISSION(403, "没有权限访问该资源。"),
SYSTEM_ERROR(500, "系统异常。"),
行了。
跨域请求配置类:
这个没啥,我记得哪个课程里面有。
聊聊跨域,先说同源策略:
同源策略:就是我们的浏览器出于安全考虑,只允许与本域下的接口交互。不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源。
跨域:浏览器不能执行其他网站的脚本。
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(false)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
Spring Security配置类:
看三更草堂的。
(6条消息) 【JavaEE】SpringSecurity—— 三更草堂_Xiang.He的博客-CSDN博客_三更草堂spring security源码
行,我知道大家会说啥:这玩意为啥配置类里面的方法是过时的(高版本Spring Boot)?
ok,略有强迫症的我找到了解决方案。不客气。
SpringBoot2.7 WebSecurityConfigurerAdapter类过期如何配置 - 开发技术 - 亿速云 (yisu.com)
行了,跟着流程基本上能做出来。
(4)个人重写搭建前端基本框架
还是那个问题,我只有HTML,CSS,JS,JQ等基础。前端会的不多,所以再看开头那个尚硅谷视频的时候,卡住了壳。Vue Admin Template写的有点复杂(其实是我看不懂),所以打算自己用Vue+Element UI写一套前端框架。
还是老样子,先理思路。
- 先检查自己电脑上有环境不。
- 创建Vue脚手架。
- 二次封装Axios。
4.与后端交互。
我觉得大家前端都比我好,所以我的描述有所不当的话请不吝赐教。
环境:肯定要下载node js。这个没啥讲的,直接去官网就行了。
然后该安装啥安装啥。
创建Vue脚手架:
我知道的两种方式:
①win+r cmd,输入vue ui。
然后就弹出这个:
点Create:
点创建就行了。
②直接去官网。
官网这样说:
欧克,win + r cmd,然后cd desktop(防止创的项目你找不到在哪)。
选择版本或者自己手动配置。上下箭头控制,回车代表确定。
结束。
二次封装Axios:
做这个之前要先了解一下Vuex和路由。
推荐课程:
vuex:01.Vuex学习目标__哔哩哔哩_bilibili
路由的话,要学Vue:尚硅谷Vue2.0+Vue3.0全套教程丨vuejs从入门到精通_哔哩哔哩_bilibili
这个Vue课程特别好,但是赶时间的同学可以简单学点,快速学习路由那章,然后知道路由是啥,路由咋用就行。
最后,二次封装Axios:登录的前后端实现:token、Vue 导航守卫、axios 拦截器等_哔哩哔哩_bilibili
看完就会了。0.0
(5)遇到的问题
①关于Token到期,后端自定义异常处理类无法捕获问题。
先上代码:
登录处理时,部分service层登录逻辑代码:
try {
authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
authenticate = authenticationManager.authenticate(authenticationToken);
System.out.println("login:" + authenticate);
} catch (MalformedJwtException | SignatureException | ExpiredJwtException | UnsupportedJwtException e) {
SecurityContextHolder.getContext().setAuthentication(null);
}
用try catch处理Spring Security异常,所以异常可以被全局异常捕获类捕获,并且清空SecurityContextHolder里面的信息。
自定义异常处理类:
/**
* @ClassName: GlobalExceptionHandler
* @Description: 全局异常处理器
* @author: 阿长
* @date: 2022/12/3 15:12
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = {RuntimeException.class})
public <T> ResultUtils<T> exceptionHandler(RuntimeException e){
//这里先判断拦截到的Exception是不是我们自定义的异常类型
if(e instanceof GlobalException){
GlobalException appException = (GlobalException)e;
return ResultUtils.error(appException.getCode(), appException.getMsg());
}
//如果拦截的异常不是我们自定义的异常(例如:数据库主键冲突)
return ResultUtils.error(500, e.getMessage());
}
//认证异常处理器
@ExceptionHandler(AuthenticationException.class)
public <T> ResultUtils<T> error(AuthenticationException e) {
if (e instanceof LockedException) {
return ResultUtils.error(GlobalExceptionMsg.ACCOUNT_LOCK);
}
return ResultUtils.error(GlobalExceptionMsg.USER_PASSWORD_ERROR);
}
//授权异常处理器
@ExceptionHandler(AccessDeniedException.class)
public <T> ResultUtils<T> error(AccessDeniedException e) {
return ResultUtils.error(GlobalExceptionMsg.NOT_PERMISSION);
}
@ExceptionHandler(ExpiredJwtException.class)
public <T> ResultUtils<T> error(ExpiredJwtException e) {
return ResultUtils.error(GlobalExceptionMsg.TOKEN_LOSE_ERROR);
}
@ExceptionHandler(SignatureException.class)
public <T> ResultUtils<T> error(SignatureException e) {
return ResultUtils.error(GlobalExceptionMsg.TOKEN_LOSE_ERROR);
}
}
但是问题是:为什么JWT Token过期异常没法被全局捕获?
这个问题困扰了我好几天。
然后打了两天游戏,早上起来就解决了。所以说啊,该摆烂的时候就摆烂0.0
好了,说说解决方法:
首先解释下上面Spring Security异常为何能被全局捕获。
因为使用Try Catch在Service层捕获异常,所以此异常能被自定义异常处理器捕获。
换一句话说,因为拦截器在Controller层之前先发挥作用,所以拦截器捕获的异常没法被自定义异常捕获。但是上边代码使用Try Catch,将拦截器捕获的异常在Service层捕获,而Controller层又调用了Service层,所以解决这一问题。
(6条消息) 过滤器(Filter)和拦截器(Interceptor)的区别和执行顺序_一朵纯洁的小白花的博客-CSDN博客_xssfilter什么时候执行
了解了这一点,那么Token异常捕获就有了思路:还是利用Try Catch,将Token认证时的异常捕获。与Spring Security异常不同,Token异常必定在
JwtAuthenticationTokenFilter中处理,不通过的话直接抛异常了,肯定不经过Controller层,那怎么办呢?
答案是请求转发:咱把这个请求发给控制层,这样携带的异常信息不就被捕获了吗?
好,于是不想写了,上网搜,没办法,CV战士嘛。
找了一会,找到一篇跟我思路不谋而合的:
(6条消息) springBoot 在过滤器中如何捕获抛出的异常并自定义返回信息_愿你活成你喜欢的模样的博客-CSDN博客_springboot过滤器返回信息
真好,拿来改改,以下是我的代码:
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
filterChain.doFilter(request, response);
return;
}
try {
boolean expired = JWTUtils.isExpired(token);//验证token是否失效
boolean signed = JWTUtils.isSigned(token);//校验是不是jwt签名
boolean verify = JWTUtils.verify(token);//校验签名是否正确
} catch (ExpiredJwtException e) {
request.setAttribute("expiredJwtException", e);
request.getRequestDispatcher("/system/exception/expiredJwtException").forward(request, response);
} catch (SignatureException e) {
request.setAttribute("signatureException", e);
request.getRequestDispatcher("/system/exception/signatureException").forward(request, response);
}
....
}
所以,解决。
②:关于验证码生成思路。
这个玩意难是不难,但是挺怪0.0
讲一下我的思路:
首先这个生成验证码的请求必须在Spring Security配置类中设置匿名访问:都没登陆呢,哪来的Token给你?
然后是验证码如何一一对应问题:举例:用户A应该拿到A的验证码,B应该拿到B的验证码,校验的时候应该是A输入的的验证码和A登录页展示的验证码进行验证,B输入的的验证码和B登录页展示的验证码进行验证。
如何实现呢?
用Redis:UUID生成Key,Kaptcha生成验证码,保存到Redis中,设置一分钟有效期。来到登录页时,在登录页发请求获取验证码和Key值,填写完账号、密码和验证码后,将获得的Key一并发送给后端,调redis进行校验,用全局异常处理类捕获异常。
上代码:
/**
* @ClassName: KaptchaUtils
* @Description: 生成验证码图片
* @author: 阿长
* @date: 2022/12/9 17:27
*/
public class KaptchaUtils {
@Resource
private DefaultKaptcha producer;
@Resource
private RedisUtils redisUtils;
public ResultUtils getCode() throws IOException {
// 生成文字验证码
String code = producer.createText();
System.out.println("code:" + code);
// 生成图片验证码
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BufferedImage image = producer.createImage(code);
ImageIO.write(image, "jpg", outputStream);
// 生成captcha的token
Map<String, Object> map = new HashMap<>();
UUID codeKey = UUID.randomUUID();
System.out.println("codeKey:" + codeKey);
//保存验证码和对应的key
redisUtils.set("code:" + codeKey, code, TimeUtils.MINUTES);
map.put("codeKey", codeKey);
map.put("img", Base64.getEncoder().encodeToString(outputStream.toByteArray()));
return ResultUtils.success(map);
}
}
/**
* @ClassName: KaptchaConfig
* @Description: Kaptcha配置类
* @author: 阿长
* @date: 2022/12/4 12:44
*/
@Configuration
public class KaptchaConfig {
private final static String CODE_LENGTH = "4";
private final static String CODE_BORDER = "no";
private final static String CODE_WIDTH = "120";
private final static String CODE_HEIGHT = "40";
private final static String CODE_FONT_SIZE = "30";
private final static String CODE_FONT_COLOR = "black";
private final static String CODE_FONT_NAMES = "宋体,楷体,黑体";
@Bean
public DefaultKaptcha producer() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 设置边框
properties.setProperty("kaptcha.border", CODE_BORDER);
// 设置边框颜色
// properties.setProperty("kaptcha.border.color", "105,179,90");
// 设置字体颜色
properties.setProperty("kaptcha.textproducer.font.color", CODE_FONT_COLOR);
// 设置图片宽度
properties.setProperty("kaptcha.image.width", CODE_WIDTH);
// 设置图片高度
properties.setProperty("kaptcha.image.height", CODE_HEIGHT);
// 设置字体尺寸
properties.setProperty("kaptcha.textproducer.font.size", CODE_FONT_SIZE);
// 设置验证码长度
properties.setProperty("kaptcha.textproducer.char.length", CODE_LENGTH);
// 设置字体
properties.setProperty("kaptcha.textproducer.font.names", CODE_FONT_NAMES);
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
@Bean
public KaptchaUtils kaptchaUtils() {
return new KaptchaUtils();
}
}
③Token续期如何做?
【IT老齐025】无状态的JWT令牌如何实现续签功能?_哔哩哔哩_bilibili
这没啥好说了,老齐讲的太好了。
④关于前端
首先,我前端不太行,属于看得懂的程度,手写有点困难,所以之前提到的前端权限控制我就先撂着,但是我的思路是:登陆成功后把用户的权限信息放在VueX中。当然,这只是我的个人见解。
还是看视频吧:
从0开始带你手撸一套SpringBoot+Vue后台管理系统(2022年最新版)_哔哩哔哩_bilibili
青戈的视频,我觉得不错,当然还有好多...
总结
说什么呢?
这篇博客完全是记录自己写毕设的全过程,我是23年毕业的,这会毕设已经写的七七八八,当然还是漏洞百出,就当记录一下自己的学习路线,然后源码的话。。。不合适吧?我这会还没答辩呢,冒着论文查重的风险写的这博客,希望兄弟们见谅。
咱可以讨论讨论。
走完我说的那一套流程,其他的基本上只剩下填充自己的业务就行,我觉得多写代码是有好处的,当然对头发很不友好,然后就是用到的所有参考都附上了链接,在这个知识付费的时代,真的是非常感谢这些具有开源精神的老师。
最后,希望这篇博客能帮到你。
也希望你对我博客中的错误批评指正。