校智谷----前后端分离的Web项目

image

from pixiv

README

老项目了,还是大二参加计算机设计大赛时写的Web项目。
最近课程javaweb要交大作业了,正好用这个项目。
由于项目年代久远,而且要答辩,所以正好复习下。

启动:

  • 开启redis
    image
  • 开启MySql
    image

前端

跨域问题

什么是跨域问题?如何解决?
我的解决方法: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
})

后端

基础知识回顾

image

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.ymlxxx.yaml

上述两个文件除了书写格式不一样,但是内容都可以参考官方文档
选择使用结构更加清晰的yaml配置文件

获取配置文件值

image
image

开发环境搭建

自从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‘

解决方法
image

第三方工具

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中进行参数校验:
image

对不符合参数校验规则的报错进行处理

  • 新建包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)
image

注意我们不能在有效载荷部分存放私密信息,因为这个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();
    }
}

image


然后就是注册并使用拦截器,在拦截器中调用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

image

以及如下可以发送JSON请求:
image

模拟表单图片上传

image

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;
}

image

阿里云第三方服务器保存图片

阿里云提供了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
image
当更改密码后,我们删除redis中保存的token,这样浏览器的token和redis的token对不上,那么就相当于强制要再登录一遍,从新获得token

同理退出功能也一样~

用了redis后记得需要开启redis服务:
image

在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);
posted @ 2024-05-06 22:58  次林梦叶  阅读(21)  评论(0编辑  收藏  举报