基于springboot-vue个人博客开发与部署

项目截图:

基于springboot-vue个人博客开发与部署_第1张图片

基于springboot-vue个人博客开发与部署_第2张图片

基于springboot-vue个人博客开发与部署_第3张图片基于springboot-vue个人博客开发与部署_第4张图片

基于springboot-vue个人博客开发与部署_第5张图片

源码地址:GitHub - chenalian/Blog-SpringBoot-Vue: 基于vue和springboot的博客系统[这里是图片009]https://github.com/chenalian/Blog-SpringBoot-Vue

项目演示地址:

基于Vue-SpringBoot的个人博客网站_哔哩哔哩_bilibili基于vue-springboot搭建的个人网站项目源码地址:https://github.com/chenalian/Blog-SpringBoot-Vue项目详解地址:https://blog.csdn.net/qq_41593124/article/details/123067458[这里是图片010]https://www.bilibili.com/video/BV1zL4y1g7oh?spm_id_from=333.999.0.0

项目重难点

1,自定义返回数据类型R

后端传递给前端的数据统一规范化处理

/**
 * 自定义通用返回模板
 *   code:100为正常,非100为异常
 *   msg:文字描述信息
 *   data:携带的数据
 */
public class R extends HashMap {

    public R(int code) {
        this.put("code", code);
        this.put("time", System.currentTimeMillis());
    }

    public static R success() {
        return new R(100);
    }

    public static R success(String msg) {
        R r = success();
        r.put("msg", msg);
        return r;
    }

    public static R error() {
        return new R(200);
    }

    public static R error(String msg) {
        R r = error();
        r.put("msg", msg);
        return r;
    }

    public static R error(int code, String msg) {
        R r = new R(code);
        r.put("msg", msg);
        return r;
    }

    @SuppressWarnings("unchecked")
    public R setAttribute(String key, Object value) {
        String data = "data";
        if (!this.containsKey(data)) {
            this.put(data, new HashMap());
        }
        ((HashMap) this.get(data)).put(key, value);
        return this;
    }

}

2,异常处理

通过@RestControllerAdvice注解设置全局异常处理来捕获springmvc抛出的异常,在通@ExceptionHandler注解具体到某个异常类。

模板
 @RestControllerAdvice
 public class ExceptionHandle {
	 @ExceptionHandler(自定义异常类.class)
    public R aesException(自定义的异常 e) {
        String message = e.getMessage();
        return R.error(Constant.RESULT_CODE_BLOG_NOT_FOUND, message);
    }  
}

3,jackson的使用

jackson是json解析器,方便将对象转换成json,json转换成对象。com.fasterxml.jackson.databind.ObjectMapper实现对各种操作的封装。因为对数据的加密和解密一般是对json数据进行加密和解密,这就涉及到对象和json相互转换。

  // JSON字符串转Map
    @SuppressWarnings("unchecked")
    public static HashMap jsonToMap(String json) throws TypeConverterException {
        try {
            ObjectMapper mapper = new ObjectMapper();
            return mapper.readValue(json, HashMap.class);
        } catch (JsonProcessingException e) {
            throw new TypeConverterException("JsonToMap转换异常!");
        }
    }

    // Map转JSON字符串
    public static String mapToJson(Map data) throws TypeConverterException{
        ObjectMapper mapper = new ObjectMapper();
        String json = null;
        try {
            json = mapper.writeValueAsString(data);
        } catch (JsonProcessingException e) {
            throw new TypeConverterException("MapToJson转换异常!");
        }
        return json;
    }

4,AOP实现加密和解码

对后端返回的数据进行加密,对前端传过来的数据进行解密(get和post两种不同的方式,一种不带有请求体requestbody,一种带有请求体requestbody)

加密操作
@Aspect
@Order(2)
@Component
public class AESEncryptAspect {

    @Pointcut("execution(* site.hanzhe.controller..*.*(..))")// 对所有的controller方法返回的数据进行加密处理
    public void pointcut() {}

    @Around("pointcut()")
    @SuppressWarnings("unchecked")
    public Object decrypt(ProceedingJoinPoint point) throws Throwable {
        // 获取到控制器返回结果
        R r = (R) point.proceed(point.getArgs());
        // 如果包含结果集,将结果集进行加密后返回
        if (r.containsKey("data")) {
            String json = BlogUtil.mapToJson((HashMap)r.get("data"));
            String encrypt = BlogUtil.AESEncrypt(json);
            r.remove("data");
            r.setAttribute(Constant.SUCCESS_NAME, encrypt);
        }
        return r;
    }
}


解密操作
get和delete方法,不带请求体
package site.hanzhe.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import site.hanzhe.exception.AESException;
import site.hanzhe.exception.TypeConverterException;
import site.hanzhe.utils.BlogUtil;

import java.util.HashMap;

/**
 * Controller AES解密:GET、DELETE请求
 *    由于AESOtherDecryptAspect实现的RequestBodyAdvice只针对拥有请求体的HTTP请求生效
 *    而GET、DELETE请求不包含请求体,所以这里单独使用AOP进行解密
 */
@Aspect
@Order(1)
@Component
public class AESGetDeleteDecryptAspect {

    // 切入点:只有使用了@AESDecrypt注解的GET请求才会执行解密
    @Pointcut("@annotation(site.hanzhe.aspect.AESDecrypt) && " +
            "(@annotation(org.springframework.web.bind.annotation.GetMapping) || @annotation(org.springframework.web.bind.annotation.DeleteMapping)) && " +
            "execution(* site.hanzhe.controller..*.*(..))")
    public void pointcut() { }

    @Around("pointcut()")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取到请求的参数列表进行解密
        Object[] args = joinPoint.getArgs();
        this.decrypt(args);
        // 执行将解密的结果交给控制器进行处理,并返回处理结果
        return joinPoint.proceed(args);
    }

    // 解密方法
    @SuppressWarnings("unchecked")
    public void decrypt(Object[] args) throws AESException, TypeConverterException {
        // 获取请求参数并转换为字符串(密文)
        HashMap data = (HashMap) args[0];
        String encrypt = data.get("json").toString();
        // 将密文解密为JSON字符串
        String json = BlogUtil.AESDecrypt(encrypt);
        // 将JSON字符串转换为Map集合,并替换原本的参数
        args[0] = BlogUtil.jsonToMap(json);
    }
}



解密 put和post方法,带请求体
package site.hanzhe.aspect;

import lombok.SneakyThrows;
import org.apache.commons.io.IOUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import site.hanzhe.exception.AESException;
import site.hanzhe.utils.BlogUtil;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.HashMap;

/**
 * Controller AES解密:POST、PUT请求
 *    1. 使用@ControllerAdvice注解扫描Controller所在位置
 *    2. 实现RequestBodyAdvice接口来处理参数
 *
 *    经测试:RequestBodyAdvice执行优先级高于AOP
 */
@ControllerAdvice("site.hanzhe.controller")
public class AESPostPutDecryptAspect implements RequestBodyAdvice {

    // 判断当前Controller是否需要进行参数解密
    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class> aClass) {
        // 只有标识了AESDecrypt注解的控制器才需要解密
        return methodParameter.hasMethodAnnotation(AESDecrypt.class);
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class> aClass) throws IOException {
        return new HttpInputMessage() {
            @Override
            public InputStream getBody() throws IOException {
                String json = null;
                try {
                    InputStream body = httpInputMessage.getBody();
                    HashMap map = BlogUtil.jsonToMap(IOUtils.toString(body));
                    String encrypt = map.get("json").toString();
                    json = BlogUtil.AESDecrypt(encrypt);
                } catch (Exception e) {
                    // TODO 异常处理待考究
                }
                return IOUtils.toInputStream(json);
            }
            @Override
            public HttpHeaders getHeaders() {
                return httpInputMessage.getHeaders();
            }
        };
    }

    @Override
    public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class> aClass) {
        return o;
    }

    @Override
    public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class> aClass) {
        return o;
    }

}

5,项目路径与服务器路径映射配置

相当于为一个绝对路径起一个短一点的别名,便于前后端访问。

@Configuration
public class ResourceConfigAdapter extends WebMvcConfigurerAdapter {
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 添加图片映射(可以配置系统目录映射到项目目录)
        // 根据不同的系统设置不同的图片存储路径
        //获取文件的真实路径
        String os = System.getProperty("os.name");
        String winPath = System.getProperty("user.dir")+"\src\main\resources\static\static\blogImage\";
        //在Linux环境下的图片存放资源路径
        String linuxPath = "/usr/local/alian/images/";
        if (os.toLowerCase().startsWith("win")) {  //windows系统
            // 设置项目真实路径
            registry.addResourceHandler("/static/blogImage/**").addResourceLocations("file:"+winPath);
        }
        else{// linux系统
            registry.addResourceHandler("/images/**").
                    addResourceLocations("file:"+linuxPath);
        }
        //获取文件的真实路径
    }
}

6,访问请求设置

是否支持跨域,对请求头进行设置

package site.hanzhe.filters;

import org.springframework.core.annotation.Order;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Enumeration;

/**
 *  1. 这里切记不能用@Component或者其他注解添加到IOC容器,会造成二次过滤,
 *     要在配置类中使用@ServletComponentScan进行扫描
 *
 *  2. 过滤路径中的通配符使用单星号*即可,不可使用双星号**
 */
@Order(1)
@WebFilter("/api/*")
public class CorsFilter implements Filter {


    //对每次请求拦截两次
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        resp.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE");
        resp.setHeader("Access-Control-Allow-Headers", "Content-Type,XFILENAME,XFILECATEGORY,XFILESIZE,token");
        // 允许携带cookie
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        filterChain.doFilter(req, resp);
    }
}

7,对管理员方法进行验证token

/*
*
*
* 自定义token验证
* 根据成功验证了的用户名和密码生成token
* token存储在redis中,且返回给前端
* 前端每次传输时候携带token和redis中进行匹配
*
*
* */

@Order(1)
@WebFilter("/api/admin/*")
public class TokenFilter implements Filter {

    @Autowired
    private RedisTemplate redisTemplate;


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        String token = req.getHeader("token");
        // 在redis判断是否存在
        Set token1 = redisTemplate.opsForSet().members("token");
        boolean flag=token1.contains(token);
        if(token==null||!flag)
        {
            R r = R.error(Constant.RESULT_CODE_TOKEN_ERROR, "请先登录");
            this.refuse(resp, r);
            return;
        }
        // 刷新token生存时间
        redisTemplate.opsForSet().add("token",token,30, TimeUnit.MINUTES);
        // 判断
        chain.doFilter(req, resp);
    }

    public void refuse(HttpServletResponse resp, R r) {
        resp.setCharacterEncoding("UTF-8");
        try {
            String json = BlogUtil.mapToJson(r);
            resp.getWriter().write(json);
        } catch (IOException | TypeConverterException e) {
            e.printStackTrace();
        }
    }
}

下载并安装

  • 1,git clone 项目源码地址
  • 基于springboot-vue个人博客开发与部署_第6张图片
  • 基于springboot-vue个人博客开发与部署_第7张图片
  • 2,导入数据库文件
  • 基于springboot-vue个人博客开发与部署_第8张图片
  • 3,编辑application.yml文件
  • 基于springboot-vue个人博客开发与部署_第9张图片配置mysql和redis的ip和用户名密码
  • 基于springboot-vue个人博客开发与部署_第10张图片
  • 4,启动后端项目
  • 基于springboot-vue个人博客开发与部署_第11张图片
  • 5,命令行打开blog-Front
  • 基于springboot-vue个人博客开发与部署_第12张图片
  • npm install //安装项目依赖
  • 基于springboot-vue个人博客开发与部署_第13张图片
  • npm run dev //启动前端项目
  • 基于springboot-vue个人博客开发与部署_第14张图片
  • 打开浏览器,输入:http://localhost:8100/

Linux部署

  • 方法一:采用webpack打包的方式将vue项目打包放在springboot项目resource文件夹下面。
  • 1,先要修改blog-front项目下面的constant.js配置文件,启动前后端不分离的REQUEST_BASE_URL,然后将blog-front进行打包,打开命令行输入npm run build,将会生成dist文件夹
  • 基于springboot-vue个人博客开发与部署_第15张图片
  • 基于springboot-vue个人博客开发与部署_第16张图片
  • 基于springboot-vue个人博客开发与部署_第17张图片
  • ,2,将dist文件夹下面的文件进行复制到springboot项目resource文件夹下面的static文件下面。
  • 基于springboot-vue个人博客开发与部署_第18张图片
  • 3,打开Idea点击package打包,并生成Blog.jar
  • 基于springboot-vue个人博客开发与部署_第19张图片
  • 4,通过Xftp将jar包传入linux系统,
  • 基于springboot-vue个人博客开发与部署_第20张图片
  • LInux中要配置好和本地一样的mysql和redis
  • 5,再输入,java -jar blog.jar
  • 基于springboot-vue个人博客开发与部署_第21张图片
  • 打开本地的浏览器输入linux的IP+端口,就可以成功访问。
  • 如果需要让jar在linux后台运行,输入 nohup java -jar blog.jar就行;
  • 6,如何让博客系统随着linux服务器开机自启动?
  • 创建自定义脚本,为自定义脚本条件执行权限,在/etc/rc.d/rc.local文件中添加脚本路径,为rc.local添加执行权限。
  •   自定义脚本
      #!/bin/sh
      #alianblog自启动脚本
      source /etc/profile
      nohup java -jar /usr/local/alian/blog.jar > /usr/local/alian/sh/base.log 2>&1 &
      添加权限 chmod +x blogAutoStart.sh
      在rc.local添加自定义脚本路径/usr/local/alian/sh/blogAutoStart.sh
      为rc.local添加执行权限 chmod +x /etc/rc.d/rc.local
    

内网穿透

(让外网用户可以访问到自己本地Linux服务器上的网站)

操作步骤

  1. 工具下载网址,先下载好客户端工具。
  2. 注册信息,进行购买隧道,有两条免费的隧道。
  3. 基于springboot-vue个人博客开发与部署_第22张图片
  4. 基于springboot-vue个人博客开发与部署_第23张图片
  5. 采用命令行的方式输入 ./natapp -authtoken=你的token,就可以得到一个公网链接,然后将公网链接发送给小伙伴就能看到你的个人博客了。
  6. 基于springboot-vue个人博客开发与部署_第24张图片

项目总结

  1. 前后端数据传输采用ASE进行加密传输,前端通过修改发送请求头将明文进行加密,后端通过Aop对数据进行加密和解密;
  2. token令牌,通过username加密生成token,存入redis并设置生存时间;
  3. 图片上传存储位置,windows部署和linux部署均可以存储,需要配置路径映射;
  4. 跨域设置;

你可能感兴趣的:(前端,html,javascript,前端,vue.js)