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

前言:

目前,前后端分离开发已经成为当前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测试中生成一条数据,再在前端进行登录;

前端登录的结果如下:

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Web前端

网页开发学习内容:html css JavaScript 两个框架:VUE.js ElementUI UI->user interface 用户界面 html(HyperText Markup Language):超文本标记语言 文本:文字 字符 超文本:网页内容 标记:标签 标识 例如商品上的标签,介绍了商品的信息 html语言就是一种标记语言,提供…

AWS云中的VPC启用流日志保存S3(AWS中国云)

问题 需要在AWS中国云中对VPC启用流日志操作。 步骤 创建s3桶 这里设置一个s3桶名&#xff0c;创建即可。如果出现已存在具有相同名称的存储桶错误&#xff0c;就换个桶名再试一试吧。 启用vpc流日志 找到vpc流日志入口操作&#xff0c;如下图&#xff1a; 设置vpc流日志…

eBPF技术揭秘:DeepFlow如何引领故障排查,提升运维效率

DeepFlow 实战&#xff1a;eBPF 技术如何提升故障排查效率 目录 DeepFlow 实战&#xff1a;eBPF 技术如何提升故障排查效率 微服务架构系统中各个服务、组件及其相互关系的全景 零侵扰分布式追踪&#xff08;Distributed Tracing&#xff09;的架构和工作流程 关于零侵扰持…

任务5.1 初识Spark Streaming

实战概述&#xff1a;使用Spark Streaming进行词频统计 1. 项目背景与目标 背景: Spark Streaming是Apache Spark的流处理框架&#xff0c;用于构建可伸缩、高吞吐量的实时数据处理应用。目标: 实现一个实时词频统计系统&#xff0c;能够处理流式数据并统计文本中的单词出现频…

微机原理 复习

第一章导论 1.3 冯诺依曼体系结构 &#xff08;1&#xff09;以二进制形式表示指令和数据 &#xff08;2&#xff09;程序和数据事先放在存储器中&#xff08;预存储&#xff09; &#xff08;3&#xff09;由运算器、控制器、输入设备和输出设备五大部件组成 字长、主频…

Java8新特性stream的原理和使用

这是一种流式惰性计算&#xff0c;整体过程是&#xff1a; stream的使用也异常方便&#xff0c;可以对比如List、Set之类的对象进行流式计算&#xff0c;挑出最终想要的结果&#xff1a; List<Timestamp> laterTimes allRecords.stream().map(Record::getTime).filter…

【摄像头标定】双目摄像头标定及矫正-opencv(python)

双目摄像头标定及矫正 棋盘格标定板标定矫正 棋盘格标定板 本文使用棋盘格标定板&#xff0c;可以到这篇博客中下载&#xff1a;https://blog.csdn.net/qq_39330520/article/details/107864568 标定 要进行标定首先需要双目拍的棋盘格图片&#xff0c;20张左右&#xff0c;…

30 哈希的应用

位图 概念 题目 给40亿个不重复的无符号整数&#xff0c;没排过序。给一个无符号整数&#xff0c;如何判断一个数是否在这40亿个整数中 1.遍历&#xff0c;时间复杂度O(N) 2.二分查找&#xff0c;需要先排序&#xff0c;排序(N*logN)&#xff0c;二分查找&#xff0c;logN。…

装载问题(回溯法)

#include<iostream> using namespace std; int n;//货物的数量 int c;//轮船的总的载重量 int cw;//轮船当前的载重量 int r;//货物的总重量 int w[1000];//n个货物各自的重量 int x[1000];//当前最优解 int bestx[1000];//最优解 int bestw;//货物的最优载重量 void Bac…

[JS]对象

介绍 对象是一种无序的数据集合, 可以详细的描述某个事物 事物的特征在对象中用属性来表示, 事物的行为在对象中用方法来表示 使用 创建对象 let 对象名 {属性名&#xff1a;值&#xff0c;方法名&#xff1a;函数&#xff0c; } let 对象名 new Object(); 对象名.属性…

Typora failed to export as pdf. undefined

变换版本并没有用&#xff0c;调整图片大小没有用 我看到一个博客后尝试出方案 我的方法 解决&#xff1a;从上图中的A4&#xff0c;变为其他&#xff0c;然后变回A4 然后到处成功&#xff0c;Amazing&#xff01; 参考&#xff1a; Typora 导出PDF 报错 failed to export…

Rpc服务的提供方(Rpcprovider)的调用流程

首先&#xff0c;服务的提供方&#xff0c;会通过rpcprovider向rpc服务方注册rpc服务对象和服务方法&#xff0c; 那么&#xff0c;我们通过protobuf提供的抽象层的service和method&#xff0c;将服务对象和它所对应的服务方法记录在map表中&#xff0c; 当它启动以后&#xff…

WordPress Quiz Maker插件 SQL注入漏洞复现(CVE-2024-6028)

0x01 产品简介 WordPress Quiz Maker插件是一款功能强大的测验生成工具,旨在帮助用户轻松、快速地构建复杂的测验和考试。插件支持多种问题类型,包括单选框(MCQ)、复选框(MCQ)、下拉列表(MCQ)、文本、短文本、数字、日期等。还支持横幅(HTML)显示信息性消息、填空题…

LONGAGENT:优化大模型处理长文本

现有的大模型&#xff08;LLMs&#xff09;&#xff0c;尽管在语言理解和复杂推理任务上取得了显著进展&#xff0c;但在处理这些超长文本时却常常力不从心。它们在面对超过10万令牌的文本输入时&#xff0c;常常会出现性能严重下降的问题&#xff0c;这被称为“中间丢失”现象…

Docker基本使用和认识

目录 基本使用 镜像仓库 镜像操作 Docker 如何实现镜像 1) namespace 2) cgroup 3) LXC Docker常见的网络类型 bridge网络如何实现 基本使用 镜像仓库 镜像仓库登录 1)docker login 后面不指定IP地址&#xff0c;则默认登录到 docker hub 上 退出 2)docker logo…

互联网直播/点播技术与平台创新应用:视频推拉流EasyDSS案例分析

随着互联网技术的快速发展&#xff0c;直播/点播平台已成为信息传播和娱乐的重要载体。特别是在电视购物领域&#xff0c;互联网直播/点播平台与技术的应用&#xff0c;不仅为用户带来了全新的购物体验&#xff0c;也为商家提供了更广阔的营销渠道。传统媒体再一次切实感受到了…

嵌入式Linux系统编程 — 4.7 regcomp、regexec、regfree正则表达式函数

目录 1 为什么需要正则表达式 2 正则表达式简介 3 正则表达式规则 4 regcomp、regexec、regfree函数 4.1 函数介绍 4.2 URL格式案例 1 为什么需要正则表达式 在许多的应用程序当中&#xff0c; 有这样的应用场景&#xff1a; 给定一个字符串&#xff0c;检查该字符串是否…

【小学期】常用基于Swing的七个静态界面

示例1&#xff1a;基本的带按钮和标签的界面 import javax.swing.*; import java.awt.*;public class SimpleSwingApp1 {public static void main(String[] args) {JFrame frame new JFrame("Simple Swing App 1");frame.setDefaultCloseOperation(JFrame.EXIT_ON_C…

5个大气的wordpress付费主题

Sesko赛斯科wordpress外贸主题 适合用于重型机械设备公司建外贸官方网站的橙红色wordpress外贸主题。 https://www.jianzhanpress.com/?p5886 Polar钋啦wordpress外贸主题 制造业wordpress网站模板&#xff0c;适合生产制造企业官方网站使用的wordpress外贸主题。 https:/…

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

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