springboot+vue 搭建管理平台常用知识点汇总

参考资料:📖【青哥带小白做毕设2024】所有教程资料汇总

1. css布局相关

参考资料:📖CSS-布局-flex

2. JS

变量赋值

  • b = a?.name:a 是 undefined 或者 null,b 不报错
  • b = a ?? c:a 是 undefined 或者 null,则赋值 c 给 b
  • b = a || c:a 是 undefined 或者 null,则赋值 c 给 b

数组操作

  • 新增元素:push()
  • 删除元素:splice()、pop()、shift()
  • 截取数组:slice()
  • 合并数组:concat()
  • 字符串变数组:split()
  • 数组变字符串:join(),默认使用 , 逗号分割
  • 获取元素序号:indexOf()
  • filter():筛选元素
    • let newArr = users.filter(v => v.name !== ‘李四’ && v.name !== ‘王二’) // 删除数组的指定元素
  • find():查找
  • map():转换
    • [1,2,3].map(v => v *2)
  • forEach():遍历
  • reduce():合并
    • 语法:arr.reduce(function(prev, cur, index, arr){...}, init);
    • prev:累计器累计回调的返回值,表示上一次调用回调时的返回值,或者初始值 init
    • cur:表示当前正在处理的数组元素
    • index:表示当前正在处理的数组元素的索引
    • arr:表示原数组
    • init:初始值
    • arr = [1,2,3]; let sum = arr.reduce((pre, cur) => pre + cur)

获取数组中每个字符出现的个数

let names =['a', 'b', 'c', 'a', 'b']
let res = names.reduce((all, cur) => {
  if (cur in all) {
    all[cur]++;
  } else {
    all[cur] = 1;
  }
  return all
}, {})

console.log("res=" , res) // {a: 2, b: 2, c: 1}

3. Vue 脚手架搭建

npm 配置淘宝镜像:

npm config set registry http://registry.npm.taobao.org/

安装vue/cli

npm install -g @vue/cli
vue --version

创建项目

vue create vue

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

配置文件 vue.config.js

配置启动端口、title等,修改后需要重启生效。

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    port: 7000
  },
  chainWebpack: config => {
    config.plugin('html')
      .tap(args => {
        args[0].title = "管理平台";
        return args;
      })
  }
})

App.vue

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('../views/HomeView.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'

import '@/assets/css/global.css'

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

HomeView.vue

<template>
  <div>主页</div>
</template>

<script>
export default {
  name: "HomeView",
};
</script>

assets/css/global.css

* {
    box-sizing: border-box;
}

body {
    color: #333;
    font-size: 14px;
    margin: 0;
    padding: 0;
}

在这里插入图片描述

4. ElementUI

4.1 引入ElementUI

npm 安装

npm i element-ui -S

在 main.js 里引入 ElementUI

import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI, { size: 'small' });

4.2 首页

在这里插入图片描述

4.2.1 整体框架

<el-container>
  <el-aside>Aside</el-aside>
  <el-container>
    <el-header>Header</el-header>
    <el-main>Main</el-main>
  </el-container>
</el-container>

4.2.2 Aside-logo

<div style="height: 60px; line-height: 60px; font-size: 20px; display: flex; align-items: center; justify-content: center">
  <img src="@/assets/logo1.png" style="width: 30px;" alt="">
  <span class="logo-title" v-show="!isCollapse">Honey2024</span>
</div>

4.2.3 Aside-菜单

<el-menu
  :collapse="isCollapse"
  :collapse-transition="false"
  router
  background-color="#001529" text-color="rgba(255, 255, 255, 0.65)" active-text-color="#fff"
  style="border: none"
  :default-active="$route.path"
>
  <el-menu-item index="/">
    <i class="el-icon-house"></i> <span slot="title">系统首页</span>
  </el-menu-item>
  <el-submenu index="2">
    <template slot="title">
      <i class="el-icon-menu"></i> <span>信息管理</span>
    </template>
    <el-menu-item index="/user">用户信息</el-menu-item>
    <el-menu-item index="/admin">管理员信息</el-menu-item>
  </el-submenu>
</el-menu>

4.2.4 Header-左侧

<i :class="collapseIcon" @click="handleCollapse" style="font-size: 26px"></i>
    <el-breadcrumb separator="/" style="margin-left: 20px">
    <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
    <el-breadcrumb-item :to="{ path: '/' }">课程管理</el-breadcrumb-item>
</el-breadcrumb>

4.2.5 Header-右侧

<div style="flex: 1; display: flex; justify-content: flex-end; align-items: center">
  <!-- 全屏按钮 -->
  <i class="el-icon-full-screen" @click="handleFull" style="font-size: 25px"></i>
  <!-- 下拉框 -->
  <el-dropdown placement="bottom">
    <div style="display: flex; align-items: center; cursor: pointer">
      <img src="@/assets/logo1.png" alt="" style="width: 40px; height: 40px; margin: 0 5px">
      <span>管理员</span>
    </div>
    <el-dropdown-menu slot="dropdown">
      <el-dropdown-item>个人信息</el-dropdown-item>
      <el-dropdown-item>修改密码</el-dropdown-item>
      <el-dropdown-item>退出登录</el-dropdown-item>
    </el-dropdown-menu>
  </el-dropdown>
</div>

4.2.6 iconfont 自定义图标

iconfont-阿里巴巴矢量图标库

搜索需要的图标,添加到自己的项目
在这里插入图片描述

设置图标前缀el-icon-Font Familyelement-icons
在这里插入图片描述

点击 “下载至本地”
在这里插入图片描述

赋值4个文件到项目
在这里插入图片描述

在 main.js 里引入 iconfont

import '@/assets/css/iconfont/iconfont.css'

4.2.7 完整代码

<template>
  <div>
    <el-container>
      <!--  侧边栏  -->
      <el-aside
        :width="asideWidth"
        style="min-height: 100vh; background-color: #001529"
      >
        <!-- logo+项目名称 -->
        <div style="height: 60px; color: white; display: flex; align-items: center; justify-content: center">
          <img src="@/assets/logo1.png" alt="" style="width: 40px; height: 40px">
          <span class="logo-title" v-show="!isCollapse">honey2024</span>
        </div>

        <!-- 侧边菜单栏 -->
        <el-menu
          :collapse="isCollapse"
          :collapse-transition="false"
          router
          background-color="#001529" text-color="rgba(255, 255, 255, 0.65)" active-text-color="#fff"
          style="border: none"
          :default-active="$route.path"
        >
          <el-menu-item index="/">
            <i class="el-icon-house"></i> <span slot="title">系统首页</span>
          </el-menu-item>
          <el-submenu index="2">
            <template slot="title">
              <i class="el-icon-menu"></i> <span>信息管理</span>
            </template>
            <el-menu-item index="/user">用户信息</el-menu-item>
            <el-menu-item index="/admin">管理员信息</el-menu-item>
          </el-submenu>
        </el-menu>
      </el-aside>

      <el-container>
        <!--  头部区域  -->
        <el-header>
          <!-- 展开折叠按钮 -->
          <i :class="collapseIcon" style="font-size: 26px" @click="handleCollapse"></i>
          <!-- 面包屑 -->
          <el-breadcrumb
            separator-class="el-icon-arrow-right"
            style="margin-left: 20px"
          >
            <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
            <el-breadcrumb-item :to="{ path: '/user' }">用户管理</el-breadcrumb-item>
          </el-breadcrumb>

          <!-- 头像下拉框 -->
          <div style="flex: 1; display: flex; justify-content: flex-end; align-items: center">
            <!-- 全屏按钮 -->
            <i class="el-icon-full-screen" @click="handleFull" style="font-size: 25px"></i>
            <!-- 下拉框 -->
            <el-dropdown placement="bottom">
              <div style="display: flex; align-items: center; cursor: pointer">
                <img src="@/assets/logo1.png" alt="" style="width: 40px; height: 40px; margin: 0 5px">
                <span>管理员</span>
              </div>
              <el-dropdown-menu slot="dropdown">
                <el-dropdown-item>个人信息</el-dropdown-item>
                <el-dropdown-item>修改密码</el-dropdown-item>
                <el-dropdown-item>退出登录</el-dropdown-item>
              </el-dropdown-menu>
            </el-dropdown>
          </div>
        </el-header>

        <!--  主体区域  -->
        <el-main>
          <div style="box-shadow: 0 0 10px rgba(0,0,0,.1); padding: 10px 20px; border-radius: 5px; margin-bottom: 10px">
            早安,骚年,祝你开心每一天!
          </div>
          <el-card style="width: 500px">
            <div slot="header" class="clearfix">
              <span>2024项目管理平台</span>
            </div>
            <div>
              2024项目管理平台正式开始了
              <div style="margin-top: 20px">
                <div style="margin: 10px 0"><strong>主题色</strong></div>
                <el-button type="primary">按钮</el-button>
                <el-button type="success">按钮</el-button>
                <el-button type="warning">按钮</el-button>
                <el-button type="danger">按钮</el-button>
                <el-button type="info">按钮</el-button>
              </div>
            </div>
          </el-card>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script>
export default {
  name: "HomeView",
  data() {
    return {
      isCollapse: false, // 不收缩
      asideWidth: "200px",
      collapseIcon: "el-icon-s-fold"
    };
  },
  methods: {
    handleFull() {
      document.documentElement.requestFullscreen();
    },
    handleCollapse() {
      this.isCollapse = !this.isCollapse;
      this.asideWidth = this.isCollapse ? "64px" : "200px";
      this.collapseIcon = this.isCollapse ? "el-icon-s-unfold" : "el-icon-s-fold";
    }
  }
};
</script>

<style>
.el-menu--inline {
  background-color: #000c17 !important;
}
.el-menu--inline .el-menu-item {
  background-color: #000c17 !important;
  padding-left: 49px !important;
}
.el-menu-item:hover,
.el-submenu__title:hover {
  color: #fff !important;
}
.el-submenu__title:hover i {
  color: #fff !important;
}
.el-menu-item:hover i {
  color: #fff !important;
}
.el-menu-item.is-active {
  background-color: #1890ff !important;
  border-radius: 5px !important;
  width: calc(100% - 8px);
  margin-left: 4px;
}
.el-menu-item.is-active i,
.el-menu-item.is-active .el-tooltip {
  margin-left: -4px;
}
.el-menu-item {
  height: 40px !important;
  line-height: 40px !important;
}
.el-submenu__title {
  height: 40px !important;
  line-height: 40px !important;
}
.el-submenu .el-menu-item {
  min-width: 0 !important;
}
.el-menu--inline .el-menu-item.is-active {
  padding-left: 45px !important;
}
/*.el-submenu__icon-arrow {*/
/*  margin-top: -5px;*/
/*}*/

.el-aside {
  transition: width 0.3s;
  box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
}
.logo-title {
  margin-left: 5px;
  font-size: 20px;
  transition: all 0.3s; /* 0.3s */
}
.el-header {
  box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
  display: flex;
  align-items: center;
}
</style>

在这里插入图片描述

4.3 封装前后端交互工具 axios

4.3.1 安装 axios

npm i axios -S

4.3.2 /src/utils/目录下建立一个request.js

import axios from 'axios'

// 创建一个新的 axios 对象
const request = axios.create({
    baseURL: 'http://localhost:9090',  // 请求后端地址
    timeout: 30000    // 超时时间
})

// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token,对请求参数统一加密
request.interceptors.request.use(config => {
    config.headers['Content-Type'] = 'application/json;charset=utf-8';
    // let user = localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : null
    // config.headers['token'] = 'token'  // 设置请求头

    return config;
}, error => {
    console.error('request error: ' + error); // for debug
    return Promise.reject(error);
});

// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
    response => {
        let res = response.data;

        // 兼容服务端返回的字符串数据
        if (typeof res === 'string') {
            res = res ? JSON.parse(res) : res
        }
        return res;
    },
    error => {
        console.error('response error: ' + error) // for debug
        return Promise.reject(error)
    }
)

export default request

4.3.3 main.js 全局声明

import request from '@/utils/request'

Vue.prototype.$request = request;

4.3.4 跨域访问

前端调用:

this.$request.get("selectAll").then((res) => {
   this.tableData = res.data;
});

this.$request.get("selectByPage", {
   params: { pageNum: 0, pageSize: 10, username: "gai", name: "盖" }
}).then((res) => {
   this.tableData = res.data;
});

⚠️ 报错信息:
在这里插入图片描述
在这里插入图片描述

这就是常见的 跨域访问 问题。关于 跨域访问 参见 📖 关于跨域和端口问题 。本例报错原因是前端地址是 http://localhost:7000/ ,访问后端地址 http://localhost:9090/,端口不一致,导致 跨域访问 报错。

📌 解决方法:
UserController 上加个注解 @CrossOrigin

@CrossOrigin
@RestController
public class UserController {
}

在这里插入图片描述

响应头可见,Access-Control-Allow-Origin: *,后端默认接收所以地址的请求。

4.4 登录、注册

4.4.1 整体设计

在这里插入图片描述

4.4.2 验证码组件

conponents/ValidCode.vue

<template>
  <div class="ValidCode disabled-select" style="width: 100%; height: 100%" @click="refreshCode">
    <span v-for="(item, index) in codeList" :key="index" :style="getStyle(item)">{{item.code}}</span>
  </div>
</template>

<script>
export default {
  name: 'ValidCode',
  data () {
    return {
      length: 4,
      codeList: []
    }
  },
  mounted () {
    this.createdCode()
  },
  methods: {
    refreshCode () {
      this.createdCode()
    },
    createdCode () {
      let len = this.length,
          codeList = [],
          chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz0123456789',
          charsLen = chars.length
      // 生成
      for (let i = 0; i < len; i++) {
        let rgb = [Math.round(Math.random() * 220), Math.round(Math.random() * 240), Math.round(Math.random() * 200)]
        codeList.push({
          code: chars.charAt(Math.floor(Math.random() * charsLen)),
          color: `rgb(${rgb})`,
          padding: `${[Math.floor(Math.random() * 10)]}px`,
          transform: `rotate(${Math.floor(Math.random() * 90) - Math.floor(Math.random() * 90)}deg)`
        })
      }
      // 指向
      this.codeList = codeList
      // 将当前数据派发出去
      this.$emit('update:value', codeList.map(item => item.code).join(''))
    },
    getStyle (data) {
      return `color: ${data.color}; font-size: ${data.fontSize}; padding: ${data.padding}; transform: ${data.transform}`
    }
  }
}
</script>

<style>
.ValidCode{
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
}
.ValidCode span {
  display: inline-block;
  font-size: 18px;
}
</style>

通过 this.$emit('update:value', codeList.map(item => item.code).join('')) 对外暴露方法 update:value,将生成验证码暴露出去。

4.4.3 登录 Login.vue

<template>
  <div style="height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #0f9876">
    <div style="display: flex; background-color: white; width: 50%; border-radius: 5px; overflow: hidden">
      <!-- 左侧图片区域 -->
      <div style="flex: 1">
        <img src="@/assets/login.png" alt="" style="width: 100%">
      </div>
      <!-- 右侧表单区域 -->
      <div style="flex: 1; display: flex; align-items: center; justify-content: center">
        <el-form :model="user" style="width: 80%" :rules="rules" ref="loginRef">
          <div style="font-size: 20px; font-weight: bold; text-align: center; margin-bottom: 20px">欢迎登录后台管理系统</div>
          <el-form-item prop="username">
            <el-input prefix-icon="el-icon-user" size="medium" placeholder="请输入账号" v-model="user.username"></el-input>
          </el-form-item>
          <el-form-item prop="password">
            <el-input prefix-icon="el-icon-lock" size="medium" placeholder="请输入密码" v-model="user.password" show-password></el-input>
          </el-form-item>
          <el-form-item prop="code">
            <div style="display: flex">
              <el-input style="flex: 1" prefix-icon="el-icon-circle-check" size="medium" placeholder="请输入验证码"  v-model="user.code"></el-input>
              <div style="flex: 1; height: 36px">
                <!-- 验证码组件回调方法 -->
                <valid-code @update:value="getCode" />
              </div>
            </div>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" style="width: 100%" @click="login">登 录</el-button>
          </el-form-item>
          <div style="display: flex">
            <div style="flex: 1">还没有账号?请 <span style="color: #0f9876; cursor: pointer" @click="$router.push('/register')">注册</span></div>
            <div style="flex: 1; text-align: right"><span style="color: #0f9876; cursor: pointer">忘记密码</span></div>
          </div>
        </el-form>
      </div>
    </div>
  </div>
</template>

<script>
import ValidCode from "@/components/ValidCode.vue";

export default {
  name: "Login",
  // 引入验证码组件
  components: {
    ValidCode,
  },
  data() {
    // 自定义验证码校验
    const validateCode = (rule, value, callback) => {
      if (value === "") {
        callback(new Error("请输入验证码"));
      } else if (value.toLowerCase() !== this.componentCode) {
        callback(new Error("验证码错误"));
      } else {
        callback();
      }
    };

    return {
      componentCode: "", // 验证码组件传递过来的 code
      user: {
        username: "",
        password: "",
        code: "", // 表单里用户输入的验证码 code
      },
      rules: {
        username: [{ required: true, message: "请输入账号", trigger: "blur" }],
        password: [{ required: true, message: "请输入密码", trigger: "blur" }],
        code: [{ validator: validateCode, trigger: "blur" }],
      },
    };
  },
  methods: {
    getCode(val) {
      this.componentCode = val.toLowerCase();
    },
    login() {
      this.$refs["loginRef"].validate((valid) => {
        if (valid) {
          // 验证通过
          this.$request.post("/login", this.user).then((res) => {
            if (res.code === "200") {
              // 登录成功,跳转到首页
              this.$router.push("/");
              this.$message.success("登录成功");
              localStorage.setItem("honey-user", JSON.stringify(res.data)); // 存储用户数据
            } else {
              this.$message.error(res.msg);
            }
          });
        }
      });
    }
  }
};
</script>

@update:value="getCode" 接收验证码组件传递参数。
用户名、密码、验证码验证通过后,this.$router.push("/") 跳转到首页,localStorage.setItem("honey-user", JSON.stringify(res.data)); 存储用户数据到本地存储,用于后续网页访问读取 token

4.4.4 注册 Register.vue

<template>
  <div style="height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #669fef">
    <div style="display: flex; background-color: white; width: 50%; border-radius: 5px; overflow: hidden">
      <div style="flex: 1">
        <img src="@/assets/register.png" alt="" style="width: 100%">
      </div>
      <div style="flex: 1; display: flex; align-items: center; justify-content: center">
        <el-form :model="user" style="width: 80%" :rules="rules" ref="registerRef">
          <div style="font-size: 20px; font-weight: bold; text-align: center; margin-bottom: 20px">欢迎注册后台管理系统</div>
          <el-form-item prop="username">
            <el-input prefix-icon="el-icon-user" size="medium" placeholder="请输入账号" v-model="user.username"></el-input>
          </el-form-item>
          <el-form-item prop="password">
            <el-input prefix-icon="el-icon-lock" size="medium" show-password placeholder="请输入密码" v-model="user.password"></el-input>
          </el-form-item>
          <el-form-item prop="confirmPass">
            <el-input prefix-icon="el-icon-lock" size="medium" show-password placeholder="请确认密码" v-model="user.confirmPass"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="info" style="width: 100%" @click="register">注 册</el-button>
          </el-form-item>
          <div style="display: flex">
            <div style="flex: 1">已经有账号了?请 <span style="color: #6e77f2; cursor: pointer" @click="$router.push('/login')">登录</span></div>
          </div>
        </el-form>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "Register",
  data() {
    // 验证码校验
    const validatePassword = (rule, confirmPass, callback) => {
      if (confirmPass === '') {
        callback(new Error('请确认密码'))
      } else if (confirmPass !== this.user.password) {
        callback(new Error('两次输入的密码不一致'))
      } else {
        callback()
      }
    }
    return {
      user: {
        username: '',
        password: '',
        confirmPass: ''
      },
      rules: {
        username: [
          { required: true, message: '请输入账号', trigger: 'blur' },
        ],
        password: [
          { required: true, message: '请输入密码', trigger: 'blur' },
        ],
        confirmPass: [
          { validator: validatePassword, trigger: 'blur' }
        ],
      }
    }
  },
  methods: {
    register() {
      this.$refs['registerRef'].validate((valid) => {
        if (valid) {
          // 验证通过
          this.$request.post('/register', this.user).then(res => {
            if (res.code === '200') {
              this.$router.push('/login')
              this.$message.success('注册成功')
            } else {
              this.$message.error(res.msg)
            }
          })
        }
      })
    }
  }
}
</script>

验证通过后,this.$router.push('/login') 跳转到登录页。

4.4.5 router/index.js 配置路由

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('../views/HomeView.vue')
  },
  {
    path: '/register',
    name: 'register',
    component: () => import('../views/Register.vue')
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('../views/Login.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

loginregister 组件加入 router

4.4.6 SpringBoot 解决跨域问题 CorsConfig

访问地址 http://localhost:7000/login
在这里插入图片描述

输入用户名、密码、验证码后,访问后端地址 http://localhost:9090/login跨域访问报错:
在这里插入图片描述

上一节给出解决方案Controller 类加个注解 @CrossOrigin,但每次新增 Controller 类都需要手工添加注解,比较麻烦。SpringBoot 提供过滤器 CorsFilter 统一处理 跨域访问 问题。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {

    // 当前跨域请求最大有效时长。这里默认1天
    private static final long MAX_AGE = 24 * 60 * 60;

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
        corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
        corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
        corsConfiguration.setMaxAge(MAX_AGE);
        source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置
        return new CorsFilter(source);
    }
}

4.4.7 后端接口

WebController

@RestController
public class WebController {

    @Resource
    UserService userService;

    @PostMapping("/login")
    public Result login(@RequestBody User user) {
        System.out.println(user);
        if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
            return Result.error("数据输入不合法");
        }
        user = userService.login(user);
        return Result.success(user);
    }

    @PostMapping("/register")
    public Result register(@RequestBody User user) {
        if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
            return Result.error("数据输入不合法");
        }
        if (user.getUsername().length() > 10 || user.getPassword().length() > 20) {
            return Result.error("数据输入不合法");
        }
        user = userService.register(user);
        return Result.success(user);
    }
}

UserServiceImpl

@Override
public User login(User user) {
    // 根据用户名查询数据库的用户信息
    User dbUser = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, user.getUsername()));
    if (dbUser == null) {
        // 抛出一个自定义的异常
        throw new ServiceException("用户名或密码错误");
    }
    if (!user.getPassword().equals(dbUser.getPassword())) {
        throw new ServiceException("用户名或密码错误");
    }
    // 生成token
    String token = TokenUtils.createToken(dbUser.getId(), dbUser.getPassword());
    dbUser.setToken(token);
    return dbUser;
}

@Override
public User register(User user) {
    User dbUser = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, user.getUsername()));
    if (dbUser != null) {
        // 抛出一个自定义的异常
        throw new ServiceException("用户名已存在");
    }
    user.setName(user.getUsername());
    userMapper.insert(user);
    return user;
}

自定义异常 ServiceException

@Getter
public class ServiceException extends RuntimeException {

    private final String code;

    public ServiceException(String msg) {
        super(msg);
        this.code = "500";
    }

    public ServiceException(String code, String msg) {
        super(msg);
        this.code = code;
    }

}

GlobalException

@ControllerAdvice
public class GlobalException {

    @ExceptionHandler(ServiceException.class)
    @ResponseBody
    public Result serviceException(ServiceException e) {
        return Result.error(e.getCode(), e.getMessage());
    }

}

4.5 SpringBoot集成JWT token实现权限验证

4.5.1 pom.xml添加JWT依赖

<!-- JWT -->
<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>4.3.0</version>
</dependency>

4.5.2 工具类 TokenUtils

@Component
public class TokenUtils {

    private static UserMapper staticUserMapper;

    @Resource
    UserMapper userMapper;

    @PostConstruct
    public void setUserService() {
        staticUserMapper = userMapper;
    }

    /**
     * 生成token
     *
     * @return
     */
    public static String createToken(String userId, String sign) {
        return JWT.create().withAudience(userId) // 将 user id 保存到 token 里面,作为载荷
                .withExpiresAt(DateUtil.offsetHour(new Date(), 2)) // 2小时后token过期
                .sign(Algorithm.HMAC256(sign)); // 以 password 作为 token 的密钥
    }

    /**
     * 获取当前登录的用户信息
     *
     * @return user对象
     */
    public static User getCurrentUser() {
        try {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String token = request.getHeader("token");
            if (StrUtil.isNotBlank(token)) {
                String userId = JWT.decode(token).getAudience().get(0);
                return staticUserMapper.selectById(Integer.valueOf(userId));
            }
        } catch (Exception e) {
            return null;
        }
        return null;
    }
}

4.5.3 login() 方法增加 token 返回

@Override
public User login(User user) {
    // 根据用户名查询数据库的用户信息
    User dbUser = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, user.getUsername()));
    if (dbUser == null) {
        // 抛出一个自定义的异常
        throw new ServiceException("用户名或密码错误");
    }
    if (!user.getPassword().equals(dbUser.getPassword())) {
        throw new ServiceException("用户名或密码错误");
    }
    // 生成token
    String token = TokenUtils.createToken(String.valueOf(dbUser.getId()), dbUser.getPassword());
    dbUser.setToken(token);
    return dbUser;
}

Login.vue 将返回 token 存储本地

login() {
this.$refs["loginRef"].validate((valid) => {
  if (valid) {
    // 验证通过
    this.$request.post("/login", this.user).then((res) => {
      if (res.code === "200") {
        // 登录成功,跳转到首页
        this.$router.push("/");
        this.$message.success("登录成功");
        localStorage.setItem("honey-user", JSON.stringify(res.data.token)); // 存储 token 到本地
      } else {
        this.$message.error(res.msg);
      }
    });
  }
});

在这里插入图片描述
登录成功后,本地存储数据:

在这里插入图片描述

📌前端接口在每次请求后端数据的时候,都会在请求头带上这个 token 作为验证信息。

📅 request.js:
请求拦截器:对请求头增加 token

request.interceptors.request.use(config => {
  config.headers['Content-Type'] = 'application/json;charset=utf-8';
  // 设置请求头,增加token
  let token = JSON.parse(localStorage.getItem("honey-user") || '{}')
  config.headers['token'] = token
  
  return config;
}

响应拦截器:判断权限不足,重定向登录页面

request.interceptors.response.use(
  response => {
    let res = response.data;

    // 兼容服务端返回的字符串数据
    if (typeof res === 'string') {
      res = res ? JSON.parse(res) : res
    }

    // 拦截权限不足的请求,重定向登录页面,防止直接输入网址访问
    if (res.code === '401') {
      router.push('/login')
    }
    return res;
  }
)

如果不登录直接访问 http://localhost:7000/ ,后台接口返回错误码 401,会被响应拦截器拦截,重定向到登录页面。登录完成后,本地存储 token,后续访问后端请求从本地存储获取到 token,才能正常访问。

4.5.4 自定义注解 AuthAccess

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthAccess {
}

该注解用于标注权限放行的方法。

@AuthAccess
@PostMapping("/register")
public Result register(@RequestBody User user) {
    if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
        return Result.error("数据输入不合法");
    }
    if (user.getUsername().length() > 10 || user.getPassword().length() > 20) {
        return Result.error("数据输入不合法");
    }
    user = userService.register(user);
    return Result.success(user);
}

WebController 的方法 register() 标注注解 @AuthAccess,结合下面拦截器 JwtInterceptor 对该注解的处理,register() 方法将被放行。

4.5.5 自定义拦截器 JwtInterceptor

public class JwtInterceptor implements HandlerInterceptor {

    @Resource
    private UserMapper userMapper;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("token");
        if (StringUtils.isBlank(token)) {
            token = request.getParameter("token");
        }

        // 对标注 AuthAccess 注解的方法进行放行
        if (handler instanceof HandlerMethod) {
            AuthAccess annotation = ((HandlerMethod) handler).getMethodAnnotation(AuthAccess.class);
            if (annotation != null) {
                return true;
            }
        }

        // 判断前端上送 token,执行认证
        if (StringUtils.isBlank(token)) {
            throw new ServiceException("401", "请登录");
        }
        // 获取 token 中的 user id
        String userId;
        try {
            userId = JWT.decode(token).getAudience().get(0);
        } catch (JWTDecodeException j) {
            throw new ServiceException("401", "请登录");
        }
        // 根据token中的userid查询数据库
        User user = userMapper.selectById(userId);
        if (user == null) {
            throw new ServiceException("401", "请登录");
        }
        // 用户密码加签验证 token
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
        try {
            jwtVerifier.verify(token); // 验证token
        } catch (JWTVerificationException e) {
            throw new ServiceException("401", "请登录");
        }
        return true;
    }
}

📌 请求头获取 token 字段值,进行JWT 认证判断是否为登录成功生成的 token ,进而进行权限认证。

💦if (handler instanceof HandlerMethod) 的含义是什么?

1.springmvc 启动时候,扫描所有 controller 类,解析所有映射方法,将每个映射方法封装一个对象 HandlerMethod ,该类包含所有请求映射方法信息(映射路径 / 方法名 / 参数 / 注解 / 返回值),上例中 AuthAccess annotation = ((HandlerMethod) handler).getMethodAnnotation(AuthAccess.class),就是获取请求方法是否标注 AuthAccess 注解。
2.springmvc 针对这些请求映射方法信息封装对象类,使用类似 map 的数据结构进行统一管理 Map<String, HandlerMethod> map
3.页面发起请求时(/users/currentUser),进入拦截器之后,springmvc 自动解析请求路径,得到 url(/users/currentUser),获取url之后,进而获取 /users/currentUser 路径对应的映射方法 HandlerMethod 实例
4.调用拦截器 preHandle 方法并将请求对象、响应对象、映射方法对象 handler 一起传入。
📖 登录拦截器原理

1.在Spring MVC中,拦截器的preHandlepostHandleafterCompletion方法的第三个参数是一个 Object 类型的 handler 参数。这个 handler 参数实际上就是处理当前请求的处理器。
2.在Spring MVC中,处理器不一定是 HandlerMethod 类型的。例如,当请求的URL对应的是一个静态资源时,处理器可能是ResourceHttpRequestHandler类型的。
3.因此,如果你的拦截器的代码只适用于 HandlerMethod 类型的处理器,你需要在代码中加入 if (handler instanceof HandlerMethod)这样的判断,以确保代码不会在处理其他类型的处理器时出错。
4.在Spring MVC中,HandlerMethod 是一个特殊的处理器类型,它用于处理由 @RequestMapping 注解(或其变体,如@GetMapping、@PostMapping等)标注的方法。
📖 Springmvc拦截器的时候要加判断 handler instanceof HandlerMethod

4.5.6 配置拦截器 InterceptorConfig

@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor())
                .addPathPatterns("/**")         // 1. 设置拦截路径
                .excludePathPatterns("/login"); // 2. 设置放行路径
        super.addInterceptors(registry);
    }

    @Bean
    public JwtInterceptor jwtInterceptor() {
        return new JwtInterceptor();
    }
}

.addPathPatterns("/**"):对所用请求地址进行拦截
.excludePathPatterns("/login"):设置放行路径,此处对 /login 进行放行,不进行拦截处理,即不校验 token。如果想对整个路径放行,可以设置 /login/**,即对 /login 下所有路径放行。

  • .excludePathPatterns(url) 和 注解 @AuthAccess 结合使用,可以灵活设置放行方法。

4.6 单文件、多文件上传和下载

4.6.1 文件上传、下载 Java 代码

@RestController
@RequestMapping("/file")
public class FileController {

    @Value("${ip:localhost}")
    String ip;

    @Value("${server.port}")
    String port;

    private static final String ROOT_PATH =  System.getProperty("user.dir") + File.separator + "files";

    @PostMapping("/upload")
    public Result upload(MultipartFile file) throws IOException {
        // 文件的原始名称
        String originalFilename = file.getOriginalFilename();
        // 获取文件名称、后缀名
        String mainName = FileUtil.mainName(originalFilename);
        String extName = FileUtil.extName(originalFilename);
        // 如果当前文件的父级目录不存在,就创建
        if (!FileUtil.exist(ROOT_PATH)) {
            FileUtil.mkdir(ROOT_PATH);
        }
        // 如果当前上传的文件已经存在了,那么重命名一个文件
        if (FileUtil.exist(ROOT_PATH + File.separator + originalFilename)) {
            originalFilename = System.currentTimeMillis() + "_" + mainName + "." + extName;
        }
        File saveFile = new File(ROOT_PATH + File.separator + originalFilename);
        // 存储文件到本地的磁盘里面去
        file.transferTo(saveFile);
        String url = "http://" + ip + ":" + port + "/file/download/" + originalFilename;
        // 返回文件的链接,这个链接就是文件的下载地址,这个下载地址就是我的后台提供出来的
        return Result.success(url);
    }

    @AuthAccess
    @GetMapping("/download/{fileName}")
    public void download(@PathVariable String fileName, HttpServletResponse response) throws IOException {
        // 附件下载
        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
        // 预览
        // response.addHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(fileName, "UTF-8"));
        String filePath = ROOT_PATH  + File.separator + fileName;
        if (!FileUtil.exist(filePath)) {
            return;
        }
        byte[] bytes = FileUtil.readBytes(filePath);
        ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(bytes);
        outputStream.flush();
        outputStream.close();
    }
}

在这里插入图片描述

在这里插入图片描述

📅 响应头 Content-Dispositionattachment;filename=,文件以附件形式下载;

在这里插入图片描述

📅Content-Dispositioninline;filename=,图片和 pdf 可以预览,其他文件类型还是以附件形式下载。

在这里插入图片描述

4.6 个人信息修改、修改密码、重置密码

📚 ElementUI官网
📖 Element-UI自学实践

5. 相关学习网站

详尽的搭建过程可以参考:
📚使用ElementPlus页面布局搭建
📚[bilibili]VUE项目,VUE项目实战,vue后台管理系统,前端面试,前端面试项目

关于跨域和端口问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会叫的狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值