校智谷----前后端分离的Web项目
README
老项目了,还是大二参加计算机设计大赛时写的Web项目。
最近课程javaweb要交大作业了,正好用这个项目。
由于项目年代久远,而且要答辩,所以正好复习下。
启动:
- 开启redis
- 开启MySql
前端
跨域问题
什么是跨域问题?如何解决?
我的解决方法:vue.config.js解决跨域问题
具体而言:
//在vue.config.js下
module.exports = {
devServer: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:9090',//后端接口地址
changeOrigin: true,//是否允许跨越
pathRewrite: {
'^/api': ''//重写,
}
}
}
}
}
//在.env下
VUE_APP_SERVER_URL = '/api'
//在utils/request.js下
// 1.创建axios实例
const service = axios.create({
// 公共接口--url = base url + request url
baseURL: process.env.VUE_APP_SERVER_URL,
// 超时时间 单位是ms,这里设置了5s的超时时间
timeout: 5 * 1000
})
后端
基础知识回顾
-
bean注册
底层通过注解@Component
来识别bean对象
衍生的注解有@SpringBootApplication
,@Controller
,@Service
等 -
第三方bean对象注册
如果是maven管理项目:- 通过maven导入jar包到项目中
- 创建
com/campuscommunitybacked/config/CommonConfig
文件
在其中编写注册代码 - 使用@Bean,@import注解
springboot
注释与参数
当前端url的请求方式如:
/blog?pageNo={页码}&size={一页的大小}&tab={查询类型}
我们Controller中如何写注释和函数参数?
//我们可以使用@RequestParam解决这个问题:
public Result<List<BlogPost>> getBlogPostByCondition(@RequestParam Integer pageNo, @RequestParam Integer size, @RequestParam String tab){}
如果需要的内容都是我们分装的entity的属性,也可以使用:
//@RequestBody
public Result createBlogPost(@RequestBody BlogPost blogPost){}
当遇到/blog/{id}
,这个id是不确定的,需要根据参数变化
//@PathVariable
@GetMapping("/info/{id}")
public Result getUserInfo(@PathVariable("id") Integer userId){}
在真实前后端交互上
好吧,我上述是在postman进行测试的,具体到真实的前后端分类确实还不一样
我只知道,如果前端发送如这种json格式:
export function changeIntroduce(content) {
return request({
url: "user/info/introduce",
method: 'post',
data:{
introduce:content
}
})
}
那么我们后端一定要用@RequestBody
是最为合适的,@RequestParam
有时不管用
public Result updateIntroduce(@RequestBody Map<String, Object> params){
String introduce = (String) params.get("introduce");
Long id = UserHolder.getUser().getId();
return userInfoService.updateIntroduce(id, introduce);
}
配置文件
- properties配置文件
- yaml配置文件
写成xxx.yml
或xxx.yaml
上述两个文件除了书写格式不一样,但是内容都可以参考官方文档
选择使用结构更加清晰的yaml配置文件
获取配置文件值
开发环境搭建
自从2023后springboot不支持java 8了,而且我的IDEA也老了
更新开发环境:
- JDK 17
- IDEA 2024
- java 17
- maven 3.8.6
然后是一系列的包问题,maven又哪里not found了,Unresolved了...
下次我一定用docker来开发了...
报错
An incompatible version [1.2.33] of the Apache Tomcat Native library is installed, while Tomcat requires version [1.2.34]
具体来说,我将Tomcat文件下到了D:\IdeaIU\tomcat-native-1.2.34-openssl-1.1.1o-ocsp-win32-bin\bin\x64
然后我将其中的tcnative-1.dll
放到我的C:\Program Files\Java\jdk-17\bin
下
数据库搭建
整合Mybatis
//pom.xml
<!--mysql驱动依赖-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!--mybatis的起步依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
//application.yml
spring:
application:
name: campus-community-backed
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:aws://localhost:3306/campuscommunity
username: root
password: xxxxxxx
注解参数的使用
Mybatis注解方式传递多个参数的4种方式
然后我发现最好用的是Map的方法:
@Service
public class BlogServiceImpl implements BlogService {
@Autowired
private BlogMapper blogMapper;
@Override
public void createBlogPost(BlogPost blogPost, Integer userId) {
String tagsStr = TransformUtil.listToString(blogPost.getTags());
blogPost.setCreateTime(LocalDateTime.now());
blogPost.setUpdateTime(LocalDateTime.now());
Map<String, Object> map = new HashMap<>();
map.put("blogPost", blogPost);
map.put("userId", userId);
map.put("tags", tagsStr);
blogMapper.createBlogPost(map);
}
}
public interface BlogMapper {
@Insert("insert into blogpost(userId, title, images, content, tags, createTime, updateTime) " +
"values (#{userId}, #{blogPost.title}, #{blogPost.images}, #{blogPost.content} , " +
"#{tags}, #{blogPost.createTime}, #{blogPost.updateTime})")
void createBlogPost(Map<String, Object> map);
}
报错
Connection refused: connect
极有可能是数据库服务没有开
在windows中搜索service
,找到MySQL,然后运行
java.sql.SQLException: Incorrect string value: ‘\xF0\x9F\x92\x94‘
第三方工具
lombok:自动生成实体类get,set等方法
- 在
pom.xml
中添加lombok的依赖
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
- 在需要添加get,set等方法的实体类(entity)中添加注解
@Data
validation: 参数校验
- 在
pom.xml
中添加validation的依赖
<!--validation依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- 在Controller类写上注释
@Validated
以及需要校验函数参数的地方写上注释
@Validated
public class UserController {
...
@PostMapping("/login")
public Result<String> login(@Pattern(regexp = "^\\S{5,20}$") String account, @Pattern(regexp = "^\\S{5,20}$") String password){...}
}
或者可以直接在entity中进行参数校验:
对不符合参数校验规则的报错进行处理
- 新建包
exception
- 添加全局异常处理类:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result GlobalExceptionHandler(Exception e) {
e.printStackTrace();
return Result.error(400, !e.getMessage().isEmpty() ? e.getMessage() : "操作失败");
}
}
JWT登入认证
JWT(Json Web Token)
注意我们不能在有效载荷部分存放私密信息,因为这个Base64是公开的算法,任何人都可以加密/解密
// Test
public class JwtTest {
@Test
public void testGen(){ //生成JWT
Map<String, Object> claims = new HashMap<>();
claims.put("id", "1");
claims.put("account", "202126202206");
String token = JWT.create()
.withClaim("user", claims) //添加载荷
.withExpiresAt(new Date(System.currentTimeMillis() + 1000*60*60*12))
.sign(Algorithm.HMAC256("campusCommunityBacked"));
System.out.println(token);
}
@Test
public void testParse(){ //解析
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
"eyJ1c2VyIjp7ImlkIjoiMSIsImFjY291bnQiOiIyMDIxMjYyMDIyMDYifSwiZXhwIjoxNzE1Mjg4MTE1fQ." +
"IX5vVBuw-ZGeGrEDtymKtAht695LyyQt4pVzUK38zKA";
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("campusCommunityBacked")).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
Map<String, Claim> claims = decodedJWT.getClaims();
System.out.println(claims.get("user"));
//结果为:{"id":"1","account":"202126202206"}
}
}
// Utils
public class JwtUtil {
private static final String key = "campusCommunityBacked";
public static String genToken(Map<String, Object> claims){
return JWT.create()
.withClaim("claims", claims) //添加载荷
.withExpiresAt(new Date(System.currentTimeMillis() + 1000*60*60*12))
.sign(Algorithm.HMAC256(key));
}
public static Map<String, Object> parseToken (String token){
return JWT.require(Algorithm.HMAC256(key))
.build()
.verify(token)
.getClaim("claims")
.asMap();
}
}
然后就是注册并使用拦截器,在拦截器中调用JWT的生成和解析代码
- 在interceptors包下
//com/campuscommunitybacked/interceptors/LoginInterceptor.java
//登录拦截器,进行JWT令牌的校验
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
String token = request.getHeader("Authorization");
try{
Map<String, Object> claims = JwtUtil.parseToken(token);
return true;
}catch (Exception e){
response.setStatus(401);
return false;
}
}
}
- config 包下
//com/campuscommunitybacked/config/WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
//注册拦截器
public void addInterceptors(InterceptorRegistry registry) {
//登录和注册不拦截
registry.addInterceptor(loginInterceptor).
excludePathPatterns("/user/login", "/user/register", "/user/logout");
}
}
postman
自动携带请求头,以及用json格式请求
//我们在pre-request下如下脚本:
pm.request.addHeader("Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGFpbXMiOnsiaWQiOjEsImFjY291bnQiOiIyMDIxMjYyMDIyMDYifSwiZXhwIjoxNzE1MzgxNDI0fQ.aPd872L4oB-AAn5nLnxjX0fJZgy3P1ThCCnkvhb55XE")
//其中的后面一大串数值是JWT
以及如下可以发送JSON请求:
模拟表单图片上传
pageHelper实现分页
首先到pom.xml中注入依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
然后再到代码中使用:
//com/campuscommunitybacked/service/impl/BlogServiceImpl.java
@Override
public PageBean<BlogPost> getBlogPostByCondition(Integer pageNo, Integer size, String tab) {
PageBean<BlogPost> pageBean = new PageBean<>();
//1.创建pagebean对象,保存查询数据
String orderBy;
PageBean<BlogPost> pb = new PageBean<>();
if (tab.equals("last")){
orderBy = "create_time desc";
} else if (tab.equals("hot")){
orderBy = "comments desc";
} else {
return null;
}
//2.开启分页查询
PageHelper.startPage(pageNo, size, orderBy);
//3.调用mapper层代码
List<BlogPost> bp = blogMapper.getBlogPostByCondition();
Page<BlogPost> pbp = (Page<BlogPost>) bp;
pageBean.setItems(pbp.getResult());
pageBean.setTotal(pbp.getResult().size());
return pageBean;
}
//com/campuscommunitybacked/entity/PageBean.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean<T> {
private Integer total;
private List<T> items;
}
阿里云第三方服务器保存图片
阿里云提供了OSS
//分装成工具类:
package com.campuscommunitybacked.utils;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import java.io.InputStream;
public class AliOssUtil {
// Endpoint,其它Region请按实际情况填写。
private static final String endpoint = "xxx";
// 填写Bucket名称,例如examplebucket。
private static final String bucketName = "xxx";
private static final String accessKeyId = "xxx";
private static final String accessKeySecret = "xxx";
// 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
public static String uploadImage(String filePath, InputStream in) throws Exception {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
String url = "";
try {
ossClient.putObject(bucketName, filePath, in);
url = "https://" + bucketName + "." + endpoint.substring(endpoint.lastIndexOf("/") + 1) + "/" + filePath;
return url;
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return null;
}
}
//在Controller中调用:
@PostMapping("/upload/users")
public Result uploadUsersImage(MultipartFile file) throws Exception {
String originalFilename = file.getOriginalFilename();
assert originalFilename != null;
String filePath = "users/"+ UUID.randomUUID().toString() +
originalFilename.substring(originalFilename.lastIndexOf("."));
String url = AliOssUtil.uploadImage(filePath, file.getInputStream());
if (url != null)
return Result.success(200, url,null);
else
return Result.error(400, "图片上传失败");
}
@PostMapping("/upload/blogs")
public Result uploadBlogsImage(MultipartFile file) throws Exception {
String originalFilename = file.getOriginalFilename();
assert originalFilename != null;
String filePath = "blogs/"+ UUID.randomUUID().toString() +
originalFilename.substring(originalFilename.lastIndexOf("."));
String url = AliOssUtil.uploadImage(filePath, file.getInputStream());
if (url != null)
return Result.success(200, url,null);
else
return Result.error(400, "图片上传失败");
}
redis token验证与主动失效
解决的问题:
有如下一个场景,当用户更改密码后,用旧的token也能够继续登入,这是很危险的。
所以我们加入了redis
当更改密码后,我们删除redis中保存的token,这样浏览器的token和redis的token对不上,那么就相当于强制要再登录一遍,从新获得token
同理退出功能也一样~
用了redis后记得需要开启redis服务:
在pom.xml下导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在application.yml中进行配置:
spring:
data:
redis:
host: localhost
port: 6379
使用:
@Autowired
private StringRedisTemplate stringRedisTemplate;
//登入时
@PostMapping("/login")
public Result<String> login(@Pattern(regexp = "^\\S{5,20}$") String account, @Pattern(regexp = "^\\S{5,20}$") String password){
// 查询用户
User user = userService.findByAccount(account);
//判断密码是否正确
if(user == null){
return Result.error(400, "用户名错误");
}
if (Md5Util.getPwd(password).equals(user.getPassword())){
Map<String, Object> claims = new HashMap<>();
claims.put("id", user.getId());
claims.put("account", user.getAccount());
String token = JwtUtil.genToken(claims);
//将token保存到redis中
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.set(token, token, 12, TimeUnit.HOURS);
return Result.success(200, token, null);
} else {
return Result.error(400, "密码错误");
}
}
//注销时
@PostMapping("/logout")
public Result logout(@RequestHeader("Authorization") String token){
//删除redis中的token
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.getOperations().delete(token);
return Result.success(200);
}
优化
Threadlocal
作用:保存一些全局变量
解决的问题:如我可能需要多次解析JWT中得到id和account,但是我希望只解析一次,然后将得到的结果保存,下次需要取就行了
代码:
//将Threadlocal的get和set方法封装成工具类
//com/campuscommunitybacked/utils/ThreadLocalUtil.java
public class ThreadLocalUtil {
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();
public static <T> T get() {
return (T) THREAD_LOCAL.get();
}
public static void set(Object value) {
THREAD_LOCAL.set(value);
}
//防止内存泄漏
public static void remove() {
THREAD_LOCAL.remove();
}
}
//在拦截器代码中添加上报错JWT解析的结果
//com/campuscommunitybacked/interceptors/LoginInterceptor.java
ThreadLocalUtil.set(claims);
//当请求完成后,清楚保存到ThreadLocal的数据
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception {
ThreadLocalUtil.remove();
}
//然后再要用的时候拿出来即可
Map<String,Object> map = ThreadLocalUtil.get();
String account = (String) map.get("account");
User user = userService.findByAccount(account);