前言:
目前,前后端分离开发已经成为当前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测试中生成一条数据,再在前端进行登录;
前端登录的结果如下:
至此,我们的登录功能就实行了。在下篇文章中,我会实现其他的功能。