当前位置: 首页 > news >正文

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

前言:

目前,前后端分离开发已经成为当前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-formlabel-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)  returnawait 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.datarouter.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.codeKeycodeImage.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.valueconst data= await sendSysPassword(passwordFrom.value)console.log(data);if(data.code==200){alert("密码修改成功")isPassword.value=falserouter.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.110port: 6379password: 123456database: 0timeout: 1000datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/zhangqiao-admin?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTCusername: rootpassword: 123456mail:host: smtp.qq.comusername: 2996809820@qq.compassword: jbtjqbhxeaerdfdidefault-encoding: UTF-8servlet:multipart:max-file-size:  10MB   # 单个文件上传的最大上限max-request-size:  100MB  # 整个请求体的最大上限
mybatis-plus:global-config:db-config:id-type: autologic-delete-value: 1logic-not-delete-value: 0logic-delete-field: isDeletedconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpllogging:file:path: D:/logs/zhangqiao-admin/spring-admin# PageHelper 分页插件配置
pagehelper:helper-dialect: mysqlreasonable: truesupport-methods-arguments: trueparams: count=countsql
minio:url: http://192.168.231.110:9001username: adminpassword: admin123456bucketName: zhangqiao-adminexclude:syspaths:- /sys/getCaptcha- /sys/user/login- /sys/resetPassword- /sys/sendSysCode- /sys/sendSysPassword
#    - /sys/user/addUserjwt:
#  expiration: 3600000Lsecret: zhangqiao

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

@Configuration
@EnableWebSecurity
public class MyServiceConfig {@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Resourceprivate ExcludeSysPath excludeSysPath;/** security的过滤器链* */@Resourceprivate   CustomAccessDeniedHandler customAccessDeniedHandler;@Resourceprivate 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;/*
* 验证管理器
* */@Beanpublic AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder){DaoAuthenticationProvider provider=new DaoAuthenticationProvider();
//将编写的UserDetailsService注入进来provider.setUserDetailsService(myUserDetailsService);
//将使用的密码编译器加入进来provider.setPasswordEncoder(passwordEncoder);
//将provider放置到AuthenticationManager 中ProviderManager providerManager=new ProviderManager(provider);return providerManager;}//跨域配置@Beanpublic 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;}/** 密码加密器*/
@Beanpublic 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忽略@Overridepublic 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@Overridepublic String getPassword() {return this.getUsers().getPassword();}@JsonIgnore@Overridepublic String getUsername() {return this.getUsers().getUsername();}@JsonIgnore@Overridepublic boolean isAccountNonExpired() {return this.getUsers().getStatus()==0;}@JsonIgnore@Overridepublic boolean isAccountNonLocked() {return this.getUsers().getStatus()==0;}@JsonIgnore@Overridepublic boolean isCredentialsNonExpired() {return this.getUsers().getStatus()==0;}@JsonIgnore@Overridepublic boolean isEnabled() {return this.getUsers().getStatus()==0;}
}

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

@Service
@Slf4j
public class MyUserDetailServerImpl implements UserDetailsService {@AutowiredUserMapper userService;@Autowired
private RedisTemplate<String,String> redisTemplate;@Overridepublic 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 {@Autowiredprivate RedisTemplate<String,String> redisTemplate;@Autowiredprivate JwtUtil jwtUtil;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取请求头中的tokenString token = request.getHeader("token");System.out.println("前端的token信息=======>"+token);//如果token为空直接放行,由于用户信息没有存放在SecurityContextHolder.getContext()中所以后面的过滤器依旧认证失败符合要求if(!StringUtils.hasText(token)){filterChain.doFilter(request,response);return;}//        解析Jwt中的用户idInteger userId = jwtUtil.getUsernameFromToken(token);//从redis中获取用户信息String redisUser = redisTemplate.opsForValue().get("UserLogin:"+ userId);if(!StringUtils.hasText(redisUser)){filterChain.doFilter(request,response);return;}//        刷新tokenString 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;@AutowiredAuthenticationManager authenticationManager;@Overridepublic 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测试中生成一条数据,再在前端进行登录;

前端登录的结果如下:

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

相关文章:

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

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

【C++ | 委托构造函数】委托构造函数 详解 及 例子源码

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…...

iCloud邮件全攻略:设置与使用终极指南

标题&#xff1a;iCloud邮件全攻略&#xff1a;设置与使用终极指南 摘要 iCloud邮件是Apple提供的一项邮件服务&#xff0c;允许用户在所有Apple设备上访问自己的邮件。本文将详细介绍如何在各种设备和邮件客户端上设置和使用iCloud邮件账户&#xff0c;确保用户能够充分利用…...

【计算机毕业设计】基于微信小程序的电子购物系统的设计与实现【源码+lw+部署文档】

包含论文源码的压缩包较大&#xff0c;请私信或者加我的绿色小软件获取 免责声明&#xff1a;资料部分来源于合法的互联网渠道收集和整理&#xff0c;部分自己学习积累成果&#xff0c;供大家学习参考与交流。收取的费用仅用于收集和整理资料耗费时间的酬劳。 本人尊重原创作者…...

CSS实现动画

CSS实现动画主要有三种方式&#xff1a;transition&#xff0c;transform&#xff0c;和animation1。以下是一些详细的逻辑&#xff0c;实例和注意事项&#xff1a; Transition&#xff1a;transition可以为一个元素在不同状态之间切换的时候定义不同的过渡效果。例如&#xff…...

Python+Pytest+Allure+Yaml+Jenkins+GitLab接口自动化测试框架详解

PythonPytestAllureYaml接口自动化测试框架详解 编撰人&#xff1a;CesareCheung 更新时间&#xff1a;2024.06.20 一、技术栈 PythonPytestAllureYamlJenkinsGitLab 版本要求&#xff1a;Python3.7.0,Pytest7.4.4,Allure2.18.1,PyYaml6.0 二、环境配置 安装python3.7&…...

[OtterCTF 2018]Bit 4 Bit

我们已经发现这个恶意软件是一个勒索软件。查找攻击者的比特币地址。** 勒索软件总喜欢把勒索标志丢在显眼的地方&#xff0c;所以搜索桌面的记录 volatility.exe -f .\OtterCTF.vmem --profileWin7SP1x64 filescan | Select-String “Desktop” 0x000000007d660500 2 0 -W-r-…...

计算机视觉全系列实战教程 (十四):图像金字塔(高斯金字塔、拉普拉斯金字塔)

1.图像金字塔 (1)下采样 从G0 -> G1、G2、G3 step01&#xff1a;对图像Gi进行高斯核卷积操作&#xff08;高斯滤波&#xff09;step02&#xff1a;删除所有的偶数行和列 void cv::pyrDown(cv::Mat &imSrc, //输入图像cv::Mat &imDst, //下采样后的输出图像cv::Si…...

正确重写equals和hashcode方法

1. 重写的原因 如有个User对象如下&#xff1a; public class User {private String name;private Integer age; }如果不重写equals方法和hashcode方法&#xff0c;则&#xff1a; public static void main(String[] args) {User user1 new User("userA", 30);Us…...

数据质量管理-时效性管理

前情提要 根据GB/T 36344-2018《信息技术 数据质量评价指标》的标准文档&#xff0c;当前数据质量评价指标框架中包含6评价指标&#xff0c;在实际的数据治理过程中&#xff0c;存在一个关联性指标。7个指标中存在4个定性指标&#xff0c;3个定量指标&#xff1b; 定性指标&am…...

python 实例002 - 数据转换

题目&#xff1a; 有一组用例数据如下&#xff1a; cases [[case_id, case_title, url, data, excepted],[1, 用例1, www.baudi.com, 001, ok],[4, 用例4, www.baudi.com, 002, ok],[2, 用例2, www.baudi.com, 002, ok],[3, 用例3, www.baudi.com, 002, ok],[5, 用例5, www.ba…...

1.k8s:架构,组件,基础概念

目录 一、k8s了解 1.什么是k8s 2.为什么要k8s &#xff08;1&#xff09;部署方式演变 &#xff08;2&#xff09;k8s作用 &#xff08;3&#xff09;Mesos&#xff0c;Swarm&#xff0c;K8S三大平台对比 二、k8s架构、组件 1.k8s架构 2.k8s基础组件 3.k8s附加组件 …...

动态规划基础练习

我们需要先从数组较大的开始进行处理&#xff0c;每次考察上下左右的&#xff0c;比较当前存储的最大值和转移来的值&#xff0c;哪一个大一点 #define _CRT_SECURE_NO_WARNINGS #include<bits/stdc.h> using namespace std;int n, m; int a[105][105]; int addx[] { 0,…...

基于Java的地方废物回收机构管理系统

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;Java技术&#xff0c;MIS的总体思想&#xff0c;MySQL数据库 工具&#xff1a;Eclipse&#xff0c;…...

Leetcode 450:删除二叉搜索树中的节点

给定一个二叉搜索树的根节点 root 和一个值 key&#xff0c;删除二叉搜索树中的 key 对应的节点&#xff0c;并保证二叉搜索树的性质不变。返回二叉搜索树&#xff08;有可能被更新&#xff09;的根节点的引用。 一般来说&#xff0c;删除节点可分为两个步骤&#xff1a; 首先…...

Go 中使用map时注意的问题

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…...

english-works

前奏&#xff08;prelude&#xff09;&#xff1a; To build up our body, our school sports meeting was held on our play ground last Thursday. All the students in my class took an active part in sports meeting. It began with an opening ceremony on the play g…...

Kubernetes面试整理-如何利用PodSecurityPolicies来提高集群的安全性?

PodSecurityPolicy (PSP) 是 Kubernetes 中用于定义和控制 Pod 安全配置的策略。通过 PSP,可以设置对 Pod 的一些安全约束条件,从而提高集群的安全性。虽然 PSP 从 Kubernetes 1.21 开始已被弃用,并在 1.25 版本中移除,但在一些旧版 Kubernetes 集群中,PSP 仍然是一个重要…...

YOLO网络结构特点收录

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

人生最有力,最棒的十句话!

人生最有力&#xff0c;最棒的十句话 1、允许一切事发生&#xff0c;所有一切发生的事不是你能阻挡了的&#xff0c;你接受&#xff0c;他也发生&#xff0c;你不接受&#xff0c;他也发生&#xff0c;你还不如坦然面对接受现实。 2、你焦虑的时候千万不要躺着啥也不干&#xf…...

Spark 之 入门讲解详细版(1)

1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室&#xff08;Algorithms, Machines, and People Lab&#xff09;开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目&#xff0c;8个月后成为Apache顶级项目&#xff0c;速度之快足见过人之处&…...

Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件

今天呢&#xff0c;博主的学习进度也是步入了Java Mybatis 框架&#xff0c;目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学&#xff0c;希望能对大家有所帮助&#xff0c;也特别欢迎大家指点不足之处&#xff0c;小生很乐意接受正确的建议&…...

MySQL 8.0 OCP 英文题库解析(十三)

Oracle 为庆祝 MySQL 30 周年&#xff0c;截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始&#xff0c;将英文题库免费公布出来&#xff0c;并进行解析&#xff0c;帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

使用 SymPy 进行向量和矩阵的高级操作

在科学计算和工程领域&#xff0c;向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能&#xff0c;能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作&#xff0c;并通过具体…...

iOS性能调优实战:借助克魔(KeyMob)与常用工具深度洞察App瓶颈

在日常iOS开发过程中&#xff0c;性能问题往往是最令人头疼的一类Bug。尤其是在App上线前的压测阶段或是处理用户反馈的高发期&#xff0c;开发者往往需要面对卡顿、崩溃、能耗异常、日志混乱等一系列问题。这些问题表面上看似偶发&#xff0c;但背后往往隐藏着系统资源调度不当…...

QT3D学习笔记——圆台、圆锥

类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体&#xff08;对象或容器&#xff09;QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质&#xff08;定义颜色、反光等&#xff09;QFirstPersonC…...

逻辑回归暴力训练预测金融欺诈

简述 「使用逻辑回归暴力预测金融欺诈&#xff0c;并不断增加特征维度持续测试」的做法&#xff0c;体现了一种逐步建模与迭代验证的实验思路&#xff0c;在金融欺诈检测中非常有价值&#xff0c;本文作为一篇回顾性记录了早年间公司给某行做反欺诈预测用到的技术和思路。百度…...

从 GreenPlum 到镜舟数据库:杭银消费金融湖仓一体转型实践

作者&#xff1a;吴岐诗&#xff0c;杭银消费金融大数据应用开发工程师 本文整理自杭银消费金融大数据应用开发工程师在StarRocks Summit Asia 2024的分享 引言&#xff1a;融合数据湖与数仓的创新之路 在数字金融时代&#xff0c;数据已成为金融机构的核心竞争力。杭银消费金…...

怎么让Comfyui导出的图像不包含工作流信息,

为了数据安全&#xff0c;让Comfyui导出的图像不包含工作流信息&#xff0c;导出的图像就不会拖到comfyui中加载出来工作流。 ComfyUI的目录下node.py 直接移除 pnginfo&#xff08;推荐&#xff09;​​ 在 save_images 方法中&#xff0c;​​删除或注释掉所有与 metadata …...