前后端分离的后台管理系统开发模板(带你从零开发一套自己的若依框架)上

前言:

目前,前后端分离开发已经成为当前web开发的主流。目前最流行的技术选型是前端vue3+后端的spring boot3,本次。就基于这两个市面上主流的框架来开发出一套基本的后台管理系统的模板,以便于我们今后的开发。

前端使用vue3+element-plus开发。后端使用spring boot3+spring security作为项目的支撑,使用MySQL8.0.30作数据存储,使用redis作缓存,使用minio作为项目的存储机构。

后台管理系统是比较注重权限的,本项目使用市面上最流行的RBAC模型。建立用户、角色、权限和它们两两之间的对映关系一共五张表,日志管理两张表,一张记录用户的行为、一张记录用户的操作。

搭建基础的环境:

后端:

创建一个spring boot项目,并导入一些基础的maven依赖:

 <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-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>4.3.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>



        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.21</version>
        </dependency>


        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.5.2</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.4</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.22</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.4.7</version>
        </dependency>


        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.6</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </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>
    </dependencies>

项目使用MybatisPlus进行数据库的操作,使用redis作为缓存。使用minio作为项目的存储机构。

MySQL是我本地的服务,redis和minio服务我放在了Linux服务器上。如果有对minio不熟悉的,可以看一下我之前写过的文章:

springboot整合minio(实现文件的上传和下载超详细入门)_minio下载文件-CSDN博客

根据数据库中的表,先创建出相应的controller、service、mapper和相应的实体类。我直接使用mybatisX插件进行相应数据的生成。一共有七张表,相应的SQL脚本,我会连同前后端的代码一起放在git。

至此。后端项目就暂时搭建出了一个基础的模板,我们接下来开始进行前端项目的部署;

前端:

挑选一个文件夹,运行

 npm create vue@latest

命令来创建一个基础的前端vue3项目,在创建项目时可以进行一些基础配置的选择。我在创建前端项目时,选择的编程语言是js,如果有选择ts的可能需要对数据的类型进行相应的指定。

在创建完前端项目之后,我们可能还需要引入一些相应的包。

npm install element-plus --save
npm install @element-plus/icons-vue
npm install sass
npm install pinia-plugin-persistedstate
npm install axios

在项目的终端运行命令来完成相应的依赖下载。下载完成之后。在package.json文件中相应的依赖如下:

至此,我们的前端vue3项目也已经搭建完成了,接下来,就可以开始我们前后端代码的编写了。

代码编写:

先进行前端代码的编写,一步步向后端靠拢,最终完成我们要实现的功能。

前端代码

在搭建环境时,我们引入了一些包的依赖,我们需要在main.js中进行依赖的声明和引用。


import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' 
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import App from '@/App.vue'
import router from '@/router'
// 1、pinia的持久化插件
import { createPersistedState } from 'pinia-plugin-persistedstate'
// element-plus的图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)
const pinia = createPinia()

//2、 接收createPersistedState函数
const piniaPersistedState = createPersistedState()

// 3、在pinia中引入持久化插件
pinia.use(piniaPersistedState)

// 全局引入图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
  }


app.use(pinia)
app.use(router)
app.use(ElementPlus, {
  locale: zhCn,
})
app.mount('#app')

删除vue3项目自带的一些vue组件,将整个vue项目恢复成一个纯净的项目。

首先,我们要做的页面是后台管理的登录页面。

在views目录下创建一个Login.vue 页面,这个页面中进行我们后台管理系统的登录操作。

在router路由中进行我们登录页面的配置,要求在运行项目时,首先跳转的就是登录页面(这也符合我们项目的预期,后台管理类的所有项目一定是要先登录,接下来才能进行功能的操作)

import { createRouter, createWebHistory } from 'vue-router'
import  useToeknStore from '@/stores/useToken'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
      // 系统用户登录
    {
      path: '/',
      name: 'login',
      component: () => import('@/views/Login.vue')
    }
  ]


})

// 前置守卫
// 全局拦截、除了登录页面,其他页面都必须授权(这里为pinia定义的token不为空)才能访问
router.beforeEach((to, from) => {
  const useToken=useToeknStore()
  if (to.name !== 'login' && !useToken.token) {

  //  alert("没有登录,自动跳转到登录页面")
    return { name: 'login' }
    
  }
  else{
    return true
  }
  }
  )




export default router

在这个路由文件中,不仅定义了登录页面,同时引入了一个路由前置守卫。这个守卫的功能是,如果没有登录,那么就只能访问登录页面。

(判断有没有登录的标识,就是pinia中token有没有值。如果登录成功,那么就会在pipin中部存入token的值,如果退出登录。那么前端也会删除token的值。借此,我们就可以判断出用户有没有登录了。我们知道,存入pipin中的值,其实是存储到了我们浏览器的localstore中,对于稍微懂点前端的人来说,都是很容易获取和改变的。在真实的项目中,肯定是不会使用这么简单的方法的。这个项目相当于我们个人开发的一个简单项目,所以就无所谓了。)

接下来,我们对axios进行一下封装,这样,每次发送请求到后端时就可以大大简化了。

创建util目录,在这个目录下新建一个request.js文件,在这个文件中封装我们axios。

import axios from "axios";
import  useTokenStore from '@/stores/useToken'
import { ElMessage } from 'element-plus';
// 先建一个api,系统
const api = axios.create({
    baseURL: "http://localhost:8888",
    timeout: 5000

});


// 发送请求前拦截
api.interceptors.request.use(
    config =>{
const useToken = useTokenStore();
// 系统用户的请求头
if(useToken.token){
    console.log("请求头toekn=====>", useToken.token);
    // 设置请求头
    // config.headers['token'] = useToken.token;
    config.headers.token = useToken.token;
}
        return config;

},
error =>{

    return Promise.reject(error);
}
)

// 响应前拦截
api.interceptors.response.use(
    response =>{
        console.log("响应数据====>", response);
if(response.data.code ==200){
return response.data;
}

// 响应失败
if(response.data.code !=200){
console.log("响应失败====>", response.data);

}

        return response.data;
},
error =>{
    return Promise.reject(error);
}
)

  
export  {api}

现在,我们就可以正式的编写我们后台管理系统的登录页面了。

(需要注意的是,我们的系统是一个后台管理类的系统,所以在首页不能让所有用户自行注册,在首页就写一个登录按钮和一个忘记密码的按钮。用户的添加需要有权限的用户在后台管理系统的功能区进行添加才行)

登录页面的内容如下:

<template>
  <!-- style="font-family:kaiti" -->
    <div class="background"  >

<!-- 注册表单,一个对话框 -->
<el-dialog v-model="isRegister"   title="用户注册"   width="30%"  draggable=true>
    <el-form label-width="120px" v-model="registerForm">
        <el-form-item label="用户名">
            <el-input type="text"   v-model="registerForm.username"   >
              <template #prefix>
                <el-icon><Avatar /></el-icon>
              </template>
              
            </el-input>
        </el-form-item>
        <el-form-item label="密码">
            <el-input  type="password" v-model="registerForm.password" >
              <template #prefix>
        <el-icon><Lock /></el-icon>
        </template>
            </el-input>
        </el-form-item>
        <el-form-item>
            <el-button type="primary" @click="registerAdd" >提交</el-button>
            <el-button @click="closeRegister">取消</el-button>
        </el-form-item>
    </el-form>
</el-dialog>

<!-- 登陆框 -->
<div class="login-box">
<el-form
    label-width="100px"
    :model="loginFrom"
    style="max-width: 460px"
    :rules="Loginrules"
    ref="ruleFormRef"
  >
 <div style=" text-align: center;
      font-weight: bold;">后台管理系统模板</div> 
    <el-form-item label="用户名"  prop="username">
      <el-input v-model="loginFrom.username"  clearable  >
        <template #prefix>
                <el-icon><Avatar /></el-icon>
              </template>
        </el-input>
    </el-form-item>
    <el-form-item label="密码" prop="password">

      <el-input v-model="loginFrom.password"   show-password   clearable  type="password" >
        <template #prefix>
        <el-icon><Lock /></el-icon>
        </template>
      </el-input>
    </el-form-item>

    <el-form-item label="验证码"  prop="codeValue">
      <el-input v-model="loginFrom.codeValue"  style="width: 100px;"  clearable  >
      </el-input>

      <img :src="codeImage" @click="getCode" style="transform: scale(0.9);"/>
    </el-form-item>

 <!-- <el-checkbox v-model="rememberMe.rememberMe"   >记住我</el-checkbox>  -->

 <!-- 跳转一个新页面 -->
 <el-link type="primary"  style="transform: translateX(330px)"  @click="resetPassword()">忘记密码</el-link>



 
<br>
<el-button type="success" @click="getLogin(ruleFormRef)" size="small"   class="my-button">登录</el-button>
    <!-- <el-button type="primary" @click="isRegister=true"   class="my-button">注册</el-button> -->
  </el-form>

  <!-- 按下enter键提交登录请求 -->
  <!-- <input @keyup.enter="getLogin(ruleFormRef)"> -->

</div>

    </div>
</template>

<script  setup>
import { ref,onMounted,reactive, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import useTokenStore  from '@/stores/useToken'
import {  getCodeApi ,loginApi,registerApi} from '@/api/SysLogin';

// <!-- 按下enter键提交登录请求 -->
    const keyDownHandler = (event) => {
        if (event.key === 'Enter') {
            // 执行你想要的操作
            console.log('Enter键被按下了');
            getLogin(ruleFormRef.value)
        }
    }
    onMounted(() => {
        window.addEventListener('keydown', keyDownHandler);
    });
    onUnmounted(() => {
        window.removeEventListener('keydown', keyDownHandler);
    });


// 重置密码
const resetPassword=()=>{

router.push({
  name:'resetPassword',
  query:{
  type:"sys"
}})

}





const ruleFormRef =  ref()

// const rememberMe=rememberMeStore()

const loginFrom=ref({
username:'',
password:'',
codeKey:'',
codeValue:''
// rememberMe:rememberMe.rememberMe
})

const Loginrules=reactive({

  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { min: 6, max: 12, message: '长度在 6 到 12 个字符', trigger: 'blur'}
  ],
  codeValue: [
    { required: true, message: '请输入验证码', trigger: 'blur' }
  ]

})

const registerForm=ref({
  username:'',
  password:''
})

const codeImage=ref('')

const isRegister=ref(false)

const tokenStore = useTokenStore();


const router = useRouter()


// 登录,提交阶段
const getLogin = async(formEl) => {
  if (!formEl)  return

  await formEl.validate(async (valid, fields) => {
  if (valid) {
      console.log('submit!')
      let data = await loginApi(loginFrom.value)
      console.log("几点几分上了飞机",data.code);
      if(data.code===200){
  ElMessage('登录成功')
  console.log(data.data);
  tokenStore.token=data.data
  router.replace({name:'layout'})

}else{

  ElMessage('登录失败')
  ElMessage('失败原因:'+data.message)

}
    } else {
      ElMessage('请输入完整信息')
      return;
    }
 
  })

  



}


const getCode=async()=>{
  
  let {data}=await getCodeApi()
  console.log(data);
  

  console.log("验证码的值为============>",data);
  loginFrom.value.codeKey=data.codeKey
  codeImage.value=data.codeValue
}

// 注册,提交阶段
const registerAdd=async()=>{

let data=await registerApi(registerForm.value)

if(data.code==200){
  ElMessage('注册成功')
  registerForm.value={
    username:'',
    password:''
  }
  isRegister.value=false
}else{

  ElMessage('注册失败')
  registerForm.value={
    username:'',
    password:''
  }
  isRegister.value=false
  }

}

const closeRegister=()=>{
  registerForm.value={
    username:'',
    password:''
  }
  isRegister.value=false
}



// 页面加载完成获取验证码

onMounted(()=>{

getCode()

})


</script>



<style lang="scss" scoped>
// 登录页面总样式
.background{
  background: url("@/assets/20.png");
  width:100%;
  height:100%;  /**宽高100%是为了图片铺满屏幕 */
  position: fixed;
  background-size: 100% 100%;
  font-family:kaiti
}

.login-box{
  border:1px solid #dccfcf;
  width: 350px;
  margin:180px auto;
  padding: 20px 50px 20px 50px;
  border-radius: 5px;
  -webkit-border-radius: 5px;
  -moz-border-radius: 5px;
  box-shadow: 0 0 25px #909399;
  background-color:rgba(255,255,255,0.7);//这里最后那个0.7就是为了防止背景表单颜色太浅
}


.my-button {
  margin-right: 100px;
  width: 400px;
  padding: 10px 20px;
  font-size: 16px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;




}


</style>

具体的展现效果如图:

挂载组件时,会发送一个请求验证码的接口到后端。后端我会使用hutoll工具包生成验证码。并将验证码放在redis中,并设置过期时间为5分钟。再通过base64编码的形式将验证码的图片传到前端直接显示。

在这个登录页面中,用户需要输入用户名、密码和验证码到后端。后端会先验证验证码的值如果正确,再验证用户名和密码。如果都正确,那么后端会返回登录成功的提示,并根据用户id生成一个token返回前端,前端收到token之后,会将这个token存入pinia中。接下来跳转到后台管理系统的功能页面

登录页面有一个“忘记密码”的按钮,我们通过这个按钮应该能实现用户密码的重置。

(密码重置通过用户邮箱发送验证码来进行验证,也可以使用短信验证码的形式进行验证,都是可以的)

重置密码页面:

<template>
    <div class="resetPassword">
<h1>密码重置页面</h1>
请输入邮箱:<input  v-model="emailNumber" class="inputEmail" type=email >  </input>
<el-button type="primary" @click="sendEmail">重置密码</el-button>

<br>
请输入验证码:<input type="text" v-model="code" class="inputEmail"/>

<el-button type="success" @click="sendCode">提交</el-button>

<div>
<!-- 修改密码表单,一个对话框 -->
<el-dialog v-model="isPassword"   title="用户修改密码"   width="30%"  draggable=true>
    <el-form label-width="120px" v-model="passwordFrom">
      
        <el-form-item label="密码">
            <el-input  type="password" v-model="passwordFrom.password" show-password   clearable   >
              <template #prefix>
        <el-icon><Lock /></el-icon>
        </template>
            </el-input>
        </el-form-item>
        <el-form-item label="确认密码">
            <el-input  type="password" v-model="passwordFrom.doPassword" show-password   clearable  >
              <template #prefix>
        <el-icon><Lock /></el-icon>
        </template>
            </el-input>
        </el-form-item>

        <el-form-item>
            <el-button type="primary" @click="sendPassword" >提交</el-button>
            <el-button @click="closeRegister">取消</el-button>
        </el-form-item>
    </el-form>
</el-dialog>
</div>
</div>

</template>

<script  setup>
import {resetPassword,sendSysCode,sendSysPassword} from "@/api/ResetPassword"
import { ref } from "vue";
import { useRouter } from 'vue-router';


let emailNumber = ref('')

let code = ref('')

let isPassword = ref(false)

let passwordFrom = ref({
    password:'',
    doPassword:'',
    email:""
})

const router = useRouter()

const sendEmail = async() => {
console.log(emailNumber.value);
  const data= await resetPassword(emailNumber.value)
  
console.log(data);

}


const sendCode = async() => {

  const data= await sendSysCode(code.value,emailNumber.value)
  
if(data.code==200){
    alert("验证码正确,请修改密码")
    isPassword.value=true
}else{
    alert("验证码错误,请重新输入")
}


console.log(data);

}

const sendPassword = async() => {
    if(passwordFrom.value.password==passwordFrom.value.doPassword){
        passwordFrom.value.email=emailNumber.value
        const data= await sendSysPassword(passwordFrom.value)
        console.log(data);
            if(data.code==200){
                alert("密码修改成功")
                isPassword.value=false
                router.replace({name:"login"})

            }else{
                alert("密码修改失败====>"+data.message)

                isPassword.value=false
            }
    }else{
        alert("两次密码不一致,请重新输入")
    }


}


const closeRegister = () => {
    isPassword.value=false
}

</script>

<style lang="scss" scoped>
.inputEmail{
    width: 300px;
    height: 40px;
}


 .resetPassword{

    max-width: 400px;
  margin: 0 auto;
  padding: 20px;
  background-color: #f0f2f5;
  border-radius: 8px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  text-align: center;
 }

</style>

在路由中添加相应的路径,并在路由守卫中加入重置页面。因为这个页面是不需要登录就能访问的:

import { createRouter, createWebHistory } from 'vue-router'
import  useToeknStore from '@/stores/useToken'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
      // 系统用户登录
    {
      path: '/',
      name: 'login',
      component: () => import('@/views/Login.vue')
    },

     // 重置密码页面
    {
      path: '/resetPassword',
      name: 'resetPassword',
      component: () => import('@/views/ResetPassword.vue')
    },
    // 通配符路由,匹配所有无法识别的路径
{
  path: '/:pathMatch(.*)*',
  component: () => import('@/error/NotFount.vue')
}
 


  ]


})

// 前置守卫
// 全局拦截、除了登录页面,其他页面都必须授权(这里为pinia定义的token不为空)才能访问
router.beforeEach((to, from) => {
  const useToken=useToeknStore()
  if (to.name !== 'login' && to.name!=='resetPassword'    && !useToken.token) {

  //  alert("没有登录,自动跳转到登录页面")
    return { name: 'login' }
    
  }
  else{
    return true
  }
  }
  )

export default router

再写一个错误路由统一处理的页面,并加入到路由中。

接下来就可以编写后端的代码来实现登录功能了;

后端代码:

由于我们后端使用了spring security作为安全框架。所以在controller层编写登录逻辑之前,我们还需要在后端做一些security的处理。

在yml配置文件中进行一些信息的配置:

spring:
  data:
    redis:
      host: 192.168.231.110
      port: 6379
      password: 123456
      database: 0
      timeout: 1000
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/zhangqiao-admin?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
    username: root
    password: 123456
  mail:
    host: smtp.qq.com
    username: 2996809820@qq.com
    password: jbtjqbhxeaerdfdi
    default-encoding: UTF-8
  servlet:
    multipart:
      max-file-size:  10MB   # 单个文件上传的最大上限
      max-request-size:  100MB  # 整个请求体的最大上限
mybatis-plus:
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0
      logic-delete-field: isDeleted
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

logging:
  file:
    path: D:/logs/zhangqiao-admin/spring-admin

# PageHelper 分页插件配置
pagehelper:
  helper-dialect: mysql
  reasonable: true
  support-methods-arguments: true
  params: count=countsql
minio:
  url: http://192.168.231.110:9001
  username: admin
  password: admin123456
  bucketName: zhangqiao-admin

exclude:
  syspaths:
    - /sys/getCaptcha
    - /sys/user/login
    - /sys/resetPassword
    - /sys/sendSysCode
    - /sys/sendSysPassword
#    - /sys/user/addUser

jwt:
#  expiration: 3600000L
  secret: zhangqiao


创建一个Security的配置类。来编写spring security的一些配置。



@Configuration
@EnableWebSecurity
public class MyServiceConfig {
 
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;


    @Resource
    private ExcludeSysPath excludeSysPath;
 
    /*
    * security的过滤器链
    * */

    @Resource
  private   CustomAccessDeniedHandler customAccessDeniedHandler;

    @Resource
    private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;


@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)throws Exception {
http.csrf(AbstractHttpConfigurer::disable);

http.authorizeHttpRequests((auth) ->
    auth
            .requestMatchers(excludeSysPath.getSyspaths().toArray(new String[0]))
            .permitAll()
            .anyRequest().authenticated()

);

http.exceptionHandling(e -> e.accessDeniedHandler(customAccessDeniedHandler)
        .authenticationEntryPoint(customAuthenticationEntryPoint)

);


http.cors(cors->{
    cors.configurationSource(corsConfigurationSource());
        });
//自定义过滤器放在UsernamePasswordAuthenticationFilter过滤器之前
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
 
 
 
 
 
 
    return http.build();
}
 
@Autowired
private MyUserDetailServerImpl myUserDetailsService;
 
 
 
/*
* 验证管理器
* */
    @Bean
    public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder){
        DaoAuthenticationProvider provider=new DaoAuthenticationProvider();
//将编写的UserDetailsService注入进来
        provider.setUserDetailsService(myUserDetailsService);
//将使用的密码编译器加入进来
        provider.setPasswordEncoder(passwordEncoder);
//将provider放置到AuthenticationManager 中
        ProviderManager providerManager=new ProviderManager(provider);
        return providerManager;
    }
 
 
//跨域配置
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
 
    /*
    * 密码加密器*/
@Bean
    public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();

}
 
}

这个配置类中编写了密码加密器、跨域相关、验证管理、定义了一个有关排除路径的bean,注入进配置文件。配置了未登录和权限不足时后端返回的一些具体信息,添加了一个JWT的拦截器,放在了security的登录拦截器之前。我们知道security的实现就是基于拦截器链的形式,在登录拦截器之前加入JWT的token拦截器,这样就能实现我们已经登录过的用户再访问其他资源时能正常访问。并且在 JWT拦截器中实现token的刷新;

定义一个MyTUserDetail,实现UserDetails接口,来当security的用户类:

@Data
public class MyTUserDetail implements Serializable, UserDetails {
 
    private static final long serialVersionUID = 1L;
 
    private User users;

    //    角色
    private List<String> roles;

    //    权限
    private  List<String> permissions;

    @JsonIgnore  //json忽略
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        List<GrantedAuthority> list =  new ArrayList<>();

//        如果角色不用空,则将角色添加到list中
        if (!ObjectUtils.isEmpty(roles)){
            roles.forEach(role->list.add(new SimpleGrantedAuthority(role)));
        }

//                如果权限不用空,则将权限添加到list中
        if (!ObjectUtils.isEmpty(permissions)){
            permissions.forEach(permission->list.add(new SimpleGrantedAuthority(permission)));
        }
        return list;

    }





    
    @JsonIgnore
    @Override
    public String getPassword() {
        return this.getUsers().getPassword();
    }
    @JsonIgnore
    @Override
    public String getUsername() {
        return this.getUsers().getUsername();
    }
 
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return this.getUsers().getStatus()==0;
    }
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return this.getUsers().getStatus()==0;
    }
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return this.getUsers().getStatus()==0;
    }
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return this.getUsers().getStatus()==0;
    }
}

定义一个MyUserDetailServerImpl类,实现UserDetailsService接口,在这个类中实现用户名和密码的具体查询:(现在我还没有实现权限的查询,按理说在登录时就应该一块进行的,现在,我只进行用户的登录,查询权限等到之后再开发。)

@Service
@Slf4j
public class MyUserDetailServerImpl implements UserDetailsService {
    @Autowired
    UserMapper userService;


@Autowired
private RedisTemplate<String,String> redisTemplate;



    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.selectOne(new LambdaQueryWrapper<User>().
                eq(StringUtils.hasText(username), User::getUsername, username));
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
 
        MyTUserDetail myTUserDetail=new MyTUserDetail();
        myTUserDetail.setUsers(user);

        return myTUserDetail;
    }
}

JWT的token拦截器,获取登录的用户信息和用户拥有的权限,放在SecurityContextHolder。在后面要用到用户信息时可以直接在SecurityContextHolder中得到登陆的用户信息。并同时在JWT拦截器中进行过期时间的刷新。

@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private RedisTemplate<String,String> redisTemplate;
 
    @Autowired
    private JwtUtil jwtUtil;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取请求头中的token
        String token = request.getHeader("token");
        System.out.println("前端的token信息=======>"+token);
        //如果token为空直接放行,由于用户信息没有存放在SecurityContextHolder.getContext()中所以后面的过滤器依旧认证失败符合要求
        if(!StringUtils.hasText(token)){
            filterChain.doFilter(request,response);
            return;
        }

//        解析Jwt中的用户id
        Integer userId = jwtUtil.getUsernameFromToken(token);
        //从redis中获取用户信息
        String redisUser = redisTemplate.opsForValue().get("UserLogin:"+ userId);
        if(!StringUtils.hasText(redisUser)){
            filterChain.doFilter(request,response);
            return;
        }

//        刷新token
        String newToken = jwtUtil.refreshToken(token);
        redisTemplate.opsForValue().
                set("UserLogin:"+userId,
                        redisUser,60, TimeUnit.MINUTES);


//        将redis中的用户信息转换成MyTUserDetail对象
        MyTUserDetail myTUserDetail= JSON.parseObject(redisUser, MyTUserDetail.class);
        log.info("Jwt过滤器中MyUserDetail的值============>"+myTUserDetail.toString());

        //将用户信息存放在SecurityContextHolder.getContext(),后面的过滤器就可以获得用户信息了。这表明当前这个用户是登录过的,后续的拦截器就不用再拦截了
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(myTUserDetail,null,myTUserDetail.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        filterChain.doFilter(request,response);
    }

}

现在,我们就可以在controller中实现用户的登录功能了。

用户登录controller:
 

  @PostMapping("/login")
    public Result<String> login(@RequestBody LoginDto loginDto){
        String token = userService.login(loginDto);
        return Result.successData(token);
    }

相应的service实现:

 

@Autowired
private RedisTemplate<String,String> redisTemplate;


    @Autowired
    AuthenticationManager authenticationManager;





@Override
    public String login(LoginDto loginDto) {
//        先检验验证码
        String codeRedis = redisTemplate.opsForValue().get(loginDto.getCodeKey());
        if (codeRedis==null){
            throw new ResultException(555,"验证码不存在");
        }
        if (!codeRedis.equals(loginDto.getCodeValue().toLowerCase())) {
            throw new ResultException(555, "验证码错误");
        }
//        验证码正确,删除redis中的验证码
        redisTemplate.delete(loginDto.getCodeKey());
        log.info("用户登录");
//        用户登录

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDto.getUsername(),loginDto.getPassword());

        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if(authenticate==null){
            throw new  ResultException(400,"用户名或密码错误");
        }
//        获取返回的用户信息
        Object principal = authenticate.getPrincipal();

        MyTUserDetail myTUserDetail=(MyTUserDetail) principal;
        System.out.println(myTUserDetail);
//        使用Jwt生成token,并将用户的id传入
        String token = jwtUtil.generateToken(myTUserDetail.getUsers().getId());
        redisTemplate.opsForValue().
                set("UserLogin:"+ myTUserDetail.getUsers().getId(),
                        JSON.toJSONString(myTUserDetail),60, TimeUnit.MINUTES);
        return token;
    }

至此,我们就完成了用户登录的全过程。要注意我们需要放行的路径,验证码的生成路径,用户登录路径、重置密码的路径等都需要进行放行。

由于数据库中还没有数据,所以我先在test测试中生成一条数据,再在前端进行登录;

前端登录的结果如下:

至此,我们的登录功能就实行了。在下篇文章中,我会实现其他的功能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/757823.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

go Channel 原理 (一)

Channel 设计原理 不要通过共享内存的方式进行通信&#xff0c;而是应该通过通信的方式共享内存。 在主流编程语言中&#xff0c;多个线程传递数据的方式一般都是共享内存。 Go 可以使用共享内存加互斥锁进行通信&#xff0c;同时也提供了一种不同的并发模型&#xff0c;即通…

Postman设置请求间自动保存返回参数,方便后续请求调用,减少复制粘贴

postman中常常出现&#xff1a;有两个请求&#xff0c;一个请求首先获取验证码或者token&#xff0c;再由得到的验证码或token编写body发送另一个请求。如何设置两个请求间自动关联相关数据呢&#xff1f; 通过环境存储全局变量 现在有两个请求如下图&#xff0c;生成验证码是…

代理IP如何助力旅游信息聚合?

在数字化时代&#xff0c;旅游信息聚合对于提升服务质量、优化用户体验起着至关重要的作用。随着在线旅游预订的普及&#xff0c;旅游信息的采集、整合和呈现成为了一个复杂而关键的过程。在这个过程中&#xff0c;代理IP技术以其独特的优势&#xff0c;为旅游信息聚合提供了强…

服务器硬件以及RAID配置

目录 一、RAID磁盘阵列原理&#xff08;嘎嘎重要&#xff09; 1、RAID的概述 2、常用的RAID 2.1、RAID 0 2.2、RAID 1 2.3、RAID 5 2.5、RAID 10 3、阵列卡介绍 二、建立软件RAID磁盘阵列 1、添加硬盘 2、使用fdisk分区&#xff0c;类型为fd 3、mdata命令使用参数 …

CXL:拯救NVMe SSD缓存不足设计难题-2

LMB提出了基于CXL协议的内存扩展框架和内核模块。该方案利用CXL内存扩展器作为物理DRAM源&#xff0c;旨在提供一个统一的内存分配接口&#xff0c;使PCIe和CXL设备都能方便地访问扩展的内存资源。通过这个接口&#xff0c;NVMe驱动和CUDA的统一内存内核驱动可以直接高效地访问…

探索人工智能和LLM对未来就业的影响

近年来&#xff0c;人工智能&#xff08;AI&#xff09;迅猛发展&#xff0c;引发了人们的兴奋&#xff0c;同时也引发了人们对就业未来的担忧。大型语言模型&#xff08;LLM&#xff09;就是最新的例子。这些强大的人工智能子集经过大量文本数据的训练&#xff0c;以理解和生成…

【贡献法】2262. 字符串的总引力

本文涉及知识点 贡献法 LeetCode2262. 字符串的总引力 字符串的 引力 定义为&#xff1a;字符串中 不同 字符的数量。 例如&#xff0c;“abbca” 的引力为 3 &#xff0c;因为其中有 3 个不同字符 ‘a’、‘b’ 和 ‘c’ 。 给你一个字符串 s &#xff0c;返回 其所有子字符…

【Arduino】实验使用ESP32控制可编程继电器制作跑马灯(图文)

今天小飞鱼实验使用ESP控制继电器&#xff0c;为了更好的掌握继电器的使用方法这里实验做了一个跑马灯的效果。 这里用到的可编程继电器&#xff0c;起始原理并不复杂&#xff0c;同样需要ESP32控制针脚输出高电平或低电平给到继电器&#xff0c;继电器使用这个信号控制一个电…

Linux 网络:网卡 promiscuous 模式疑云

文章目录 1. 前言2. 问题场景3. 问题定位和分析4. 参考资料 1. 前言 限于作者能力水平&#xff0c;本文可能存在谬误&#xff0c;因此而给读者带来的损失&#xff0c;作者不做任何承诺。 2. 问题场景 调试 Marvell 88E6320 时&#xff0c;发现 eth0 出人意料的进入了 promis…

【吊打面试官系列-MyBatis面试题】MyBatis 与 Hibernate 有哪些不同?

大家好&#xff0c;我是锋哥。今天分享关于 【MyBatis 与 Hibernate 有哪些不同&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; MyBatis 与 Hibernate 有哪些不同&#xff1f; 1、Mybatis 和 hibernate 不同&#xff0c;它不完全是一个 ORM 框架&#xff0c;因…

grpc学习golang版( 四、多服务示例 )

系列文章目录 第一章 grpc基本概念与安装 第二章 grpc入门示例 第三章 proto文件数据类型 第四章 多服务示例 第五章 多proto文件示例 第六章 服务器流式传输 第七章 客户端流式传输 第八章 双向流示例 文章目录 一、前言二、定义proto文件三、编写server服务端四、编写Client客…

盘点全球Top10大云计算平台最热门技能证书

小李哥花了一年半时间终于考下全球10大云的77张认证&#xff0c;今天盘点下各个云的热门证书&#xff0c;希望能帮到非CS专业转IT和刚刚入行云计算的小伙伴。 排名取自23年Yahoo云计算市场份额排名报告&#xff0c;我会从云平台、证书价格、证书热门程度做推荐。 1️⃣亚马逊云…

MathType7.6永久破解激活码注册码 包含安装包下载

MathType是一款强大的数学公式编辑器&#xff0c;它能够帮助用户轻松编辑各种复杂的数学公式和符号。无论是学生、教师还是科研人员&#xff0c;MathType都能提供专业、精确的数学公式编辑服务。 在学习和工作中&#xff0c;我们常常会遇到需要编写数学公式的情况。然而&#x…

Python 算法交易实验74 QTV200第二步(改): 数据清洗并写入Mongo

说明 之前第二步是打算进入Clickhouse的&#xff0c;实测下来有一些bug 可以看到有一些分钟数据重复了。简单分析原因&#xff1a; 1 起异步任务时&#xff0c;还是会有两个任务重复的问题&#xff0c;这个在同步情况下是不会出现的2 数据库没有upsert模式。clickhouse是最近…

除了重塑千行百业,生成式AI还能改善运动健康

飞速发展的生成式AI与大模型技术&#xff0c;不但正在重塑千行百业&#xff0c;而且还能有效改善人们的运动健康。 生成式AI技术应用的挑战 随着生活品质的不断提升&#xff0c;人们对于健康问题也越来越重视。作为一家以“AI重塑健康与美”为使命的AI数字健康解决方案提供商&a…

langchain学习总结

大模型开发遇到的问题及langchain框架学习 背景&#xff1a; 1、微场景间跳转问题&#xff0c;无法实现微场景随意穿插 2、大模型幻读&#xff08;推荐不存在的产品、自己发挥&#xff09; 3、知识库检索&#xff0c;语义匹配效果较差&#xff0c;匹配出的结果和客户表述的…

解决指南:如何应对错误代码 0x80070643

在使用Windows操作系统过程中&#xff0c;用户可能会遭遇各种错误代码&#xff0c;其中错误 0x80070643是比较常见的一种。这个错误通常在安装更新或某些软件时发生&#xff0c;尤其是在微软的Windows Defender或其他Microsoft安全产品以及.NET Framework更新过程中更为常见。本…

动画重定向——当给一个人物模型用别人物的动画时,会遇到人物与动画不匹配问题,怎么解决呢?

每日一句&#xff1a;实践出真知&#xff0c;试错方确信 目录 最开始我想的原因&#xff01; 分析一下动画相关参数 Animator组件参数详解&#xff1a; 人物模型的导入设置参数&#xff1a; Skinned Mesh Renderer组件详解: Skinned Mesh Renderer工作原理 设置Skinned …

【吴恩达深度学习笔记系列】Logistic Regression 【理论】

Binary Classification: Logistic Regression: y ^ σ ( w T x b ) \hat{y}\sigma{(w^T xb)} y^​σ(wTxb) using sigmoid function σ 1 1 e − z \sigma \frac{1}{1e^{-z}} σ1e−z1​. 【torch.sigmoid(x)】 Sigmoid ( x ) 1 1 e − x \text{Sigmoid}(x)\frac{1}{…

nginx优势以及应用场景,编译安装和nginx

一. Nginx是什么&#xff1f; 1. Nginx概述 高性能、轻量级Web服务软件系统资源消耗低对HTTP并发连接的处理能力高单台物理服务器可支持30,000&#xff5e;50,000个并发请求Nginx&#xff08;发音同 “engine x”&#xff09;是一个高性能的反向代理和Web服务器软件&#xff0c…