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

news2024/7/4 4:57:31

前言:

目前,前后端分离开发已经成为当前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.coloradmin.cn/o/1877478.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

YOLO网络结构特点收录

YOLO网络结构特点收录 YOLO&#xff08;You Only Look Once&#xff09;网络结构随着版本迭代不断进化&#xff0c;以下是一些关键版本的网络结构特点概述&#xff1a; YOLOv1 输入&#xff1a;将图像调整至固定尺寸&#xff0c;如448x448像素。骨干网络&#xff1a;初期版本…

Leetcode3190. 使所有元素都可以被 3 整除的最少操作数

Every day a Leetcode 题目来源&#xff1a;3190. 使所有元素都可以被 3 整除的最少操作数 解法1&#xff1a;遍历 遍历数组&#xff0c;累加最少操作数&#xff0c;即 min(num % 3, 3 - num % 3)。 代码&#xff1a; /** lc appleetcode.cn id3190 langcpp** [3190] 使所…

ElementUI框架搭建及组件使用

前言: 当开始使用ElementUI框架来搭建网站或Web应用程序时&#xff0c;了解框架的基本结构和组件的使用是至关重要的。ElementUI是一个基于Vue.js的框架&#xff0c;提供了丰富的UI组件和工具&#xff0c;可以帮助开发人员快速构建现代化的用户界面。 在本文中&#xff0c;我…

电脑提示vcomp140.dll丢失的几种有效的解决方法,轻松搞定dll问题

在电脑使用过程中&#xff0c;我们可能会遇到一些错误提示&#xff0c;其中之一就是找不到vcomp140.dll。那么&#xff0c;究竟什么是vcomp140.dll呢&#xff1f;为什么会出现找不到vcomp140.dll的情况呢&#xff1f;本文将从vcomp140.dll的定义、常见原因、对电脑的影响以及解…

[鹏城杯 2022]babybit

发现一个压缩包提取出来提取出来两个压缩包里面是注册表使用MiTeC Windows Registry Recovery 恢复注册表 flag在ROOT\ControlSet001\Control\FVEStats里的OsvEncryptInit和OsvEncryptComplete中 NSSCTF{2022/6/13_15:17:39_2022/6/13_15:23:46}

Nuxtjs3教程

起步 官方文档 官方目录结构 安装 npx nuxi@latest init <project-name>后面跟着提示走就行 最后yarn run dev 启动项目访问localhost:3000即可 路由组件 app.vue为项目根组件 <nuxt-page />为路由显示入口 将app.vue更改内容如下 <template><d…

PostgreSQL 17 Beta 1 发布!

PostgreSQL 全球开发小组宣布&#xff0c;PostgreSQL 17 的第一个测试版本现已可供下载。此版本包含 PostgreSQL 17 正式发布时将提供的所有功能的预览&#xff0c;但测试期间版本的某些细节可能会发生变化。 #PG培训#PG考试#postgresql培训#postgresql考试#postgresql认证 您…

【Qt+opencv】编译、配置opencv

文章目录 前言下载opencv编译opencvmingw版本 总结 前言 OpenCV&#xff08;Open Source Computer Vision Library&#xff09;是一个开源的计算机视觉和机器学习软件库&#xff0c;它包含了超过2500个优化的算法。这些算法可以用来检测和识别面部&#xff0c;识别对象&#x…

动手学深度学习(Pytorch版)代码实践 -计算机视觉-47转置卷积

47转置卷积 import torch from torch import nn from d2l import torch as d2l# 输入矩阵X和卷积核矩阵K实现基本的转置卷积运算 def trans_conv(X, K):h, w K.shapeY torch.zeros((X.shape[0] h - 1, X.shape[1] w - 1))for i in range(X.shape[0]):for j in range(X.shap…

Python_Socket

Python Socket socket 是通讯中的一种方式&#xff0c;主要用来处理客户端与伺服器端之串连&#xff0c;只需要protocol、IP、Port三项目即可进行网路串连。 Python套件 import socketsocket 常用函式 socket.socket([family], [type] , [proto] ) family: 串接的类型可分为…

pdf怎么转换成jpg,本地转换还是在线转换?

PDF&#xff08;Portable Document Format&#xff09;和JPG&#xff08;Joint Photographic Experts Group&#xff09;这两种文件格式在我们的日常生活和工作中扮演着举足轻重的角色。PDF因其跨平台、保持原样性强的特点&#xff0c;被广泛应用于文件传输和存储&#xff1b;而…

快速修复mfc100u.dll丢失解决方案

相连文章&#xff1a;SecureCRT的安装破解 [详细过程2024] 有小伙伴向我反馈在打开SecureFX注册机之后显示【mfc100u.dll找不到】重装之后也没有用&#xff0c;这个是因为Microsoft Visual C的运行时组件和库出现了错误&#xff0c;直接选择重新安装就可以 出现这种情况的原因…

识图生成代码:通义千问vsGPt4o,有点小崩

今日对比一下通义千问和GPt4o&#xff0c;在通过识别图片然后去生成前端代码 在当今ai的时代&#xff0c;通过ai去生成页面的代码可以很大的提高我们的开发效率下面是我们要求的生成的图片截图&#xff0c;这是掘金的榜单 效果对比 首先我们使用通义千问&#xff0c;让他去帮我…

LabVIEW在机器人研究所中的应用

机器人研究所致力于机器人技术的研究与开发&#xff0c;涵盖工业机器人、服务机器人、医疗机器人等多个领域。研究所需要一个高效、灵活的实验控制和数据采集系统&#xff0c;以进行复杂的机器人实验&#xff0c;并对实验数据进行实时处理和分析。 项目需求 实时控制与监控&am…

Linux中使用网络文件系统NFS挂载远程目录,对远程文件的本地化操作

目录 一、NFS及其在linux系统中的挂载 1、NFS概述 2、NFS挂载及其作用 &#xff08;1&#xff09;资源共享 &#xff08;2&#xff09;简化数据管理 &#xff08;3&#xff09;提高数据可用性 &#xff08;4&#xff09;灵活性 &#xff08;5&#xff09;访问控制 &am…

ctfshow-web入门-命令执行(web56、web57、web58)

目录 1、web56 2、web57 3、web58 1、web56 命令执行&#xff0c;需要严格的过滤 新增过滤数字&#xff0c;只能采用上一题临时文件上传的方法&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><…

C语言学习记录(十二)——指针与数组及字符串

文章目录 前言一、指针和数组二、指针和二维数组**行指针(数组指针)** 三、 字符指针和字符串四、指针数组 前言 一个学习嵌入式的小白~ 有问题评论区或私信指出~ 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、指针和数组 在C语言中 &#xff0…

RHCE四---web服务器的高级优化方案

一、Web服务器&#xff08;2&#xff09; 基于https协议的静态网站 概念解释 HTTPS&#xff08;全称&#xff1a;Hyper Text Transfer Protocol over Secure Socket Layer 或 Hypertext TransferProtocol Secure&#xff0c;超文本传输安全协议&#xff09;&#xff0c;是以…

​Chrome插件:Postman Interceptor 调试的终极利器

今天给大家介绍一款非常实用的工具——Postman Interceptor。 这个工具可以捕捉任何网站的请求&#xff0c;并将其发送到Postman客户端。 对于经常和API打交道的程序员来说&#xff0c;Postman Interceptor真的是神器级别的存在。 下面就让我详细说说这个插件怎么用&#xf…

操作系统-文件的物理结构(文件分配方式)

文章目录 总览文件块和磁盘块连续分配顺序访问直接访问&#xff08;随机访问&#xff09;为什么连续分配同时支持这两种访问模式&#xff1f; 链接分配隐式链接显示链接小结索引分配链接方案多层索引混合索引小结 总结 总览 文件数据存放在外存中 文件块和磁盘块 文件内通过逻…