權限模塊開發流程
- 前端login頁面開發
- 后端SpringSecurity配置
- 后端login接口開發
- 前端頁面框架搭建
- 前端路由守衛,狀態管理開發
- 前后端完成認證流程
開發Login頁面
- 創建Login頁面
- 創建router,可以跳轉到Login頁面
Login頁面
使用element-plus開發
認證功能流程
前端
- 集成axios,并對axios進行封裝
- 配置路由守衛
- 對請求和響應的攔截
- 登錄接口的調用
- 登錄成功之后的頁面跳轉
- 跳轉到首頁
- 頁面就是由左側導航、頭部導航、頁面主體
后端
- 配置SpringScurity
- 寫登錄接口
- 返回用戶的token、使用jwt生成token
- 返回用戶的權限信息【路由頁面】
配置Redis
引入依賴
<!--引入redis-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>3.1.5</version>
</dependency>
配置連接
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://rm-2ze9013333m0paw8ywo.mysql.rds.aliyuncs.com:3306/daocao?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: puhe_shitianpassword: Shitian@9527data:redis:# 地址host: localhost# 數據庫索引database: 2# 密碼password: $2a$10$4NR/# 連接超時時間timeout: 10slettuce:pool:# 連接池中的最小空閑連接min-idle: 0# 連接池中的最大空閑連接max-idle: 8# 連接池的最大數據庫連接數max-active: 8# #連接池最大阻塞等待時間(使用負值表示沒有限制)max-wait: -1ms
配置Redis數據序列化
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>{public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;public FastJson2JsonRedisSerializer(Class<T> clazz){super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException{if (t == null){return new byte[0];}return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException{if (bytes == null || bytes.length <= 0){return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);}
}
package com.daocao.support.config.redis;import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.StringRedisSerializer;/*** @author 石添* @date 2023/11/15 9:31*/
@Configuration
@EnableCaching
public class RedisConfig {@Bean@SuppressWarnings(value = { "unchecked", "rawtypes" })public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);// 使用StringRedisSerializer來序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}@Beanpublic DefaultRedisScript<Long> limitScript() {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(limitScriptText());redisScript.setResultType(Long.class);return redisScript;}/*** 限流腳本*/private String limitScriptText() {return "local key = KEYS[1]\n" +"local count = tonumber(ARGV[1])\n" +"local time = tonumber(ARGV[2])\n" +"local current = redis.call('get', key);\n" +"if current and tonumber(current) > count then\n" +" return tonumber(current);\n" +"end\n" +"current = redis.call('incr', key)\n" +"if tonumber(current) == 1 then\n" +" redis.call('expire', key, time)\n" +"end\n" +"return tonumber(current);";}
}
工具類
package com.daocao.common.redis;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;import java.util.*;
import java.util.concurrent.TimeUnit;/*** @author ShiTian* @date 2022/8/1 17:19*/
@Component
public class RedisCache {@Autowiredpublic RedisTemplate redisTemplate;/*** 緩存基本的對象,Integer、String、實體類等** @param key 緩存的鍵值* @param value 緩存的值*/public <T> void setCacheObject(final String key, final T value){redisTemplate.opsForValue().set(key, value);}/*** 緩存基本的對象,Integer、String、實體類等** @param key 緩存的鍵值* @param value 緩存的值* @param timeout 時間* @param timeUnit 時間顆粒度*/public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit){redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/*** 設置有效時間** @param key Redis鍵* @param timeout 超時時間* @return true=設置成功;false=設置失敗*/public boolean expire(final String key, final long timeout){return expire(key, timeout, TimeUnit.SECONDS);}/*** 設置有效時間** @param key Redis鍵* @param timeout 超時時間* @param unit 時間單位* @return true=設置成功;false=設置失敗*/public boolean expire(final String key, final long timeout, final TimeUnit unit){return redisTemplate.expire(key, timeout, unit);}/*** 獲得緩存的基本對象。** @param key 緩存鍵值* @return 緩存鍵值對應的數據*/public <T> T getCacheObject(final String key){ValueOperations<String, T> operation = redisTemplate.opsForValue();return operation.get(key);}/*** 刪除單個對象** @param key*/public boolean deleteObject(final String key){return redisTemplate.delete(key);}/*** 刪除集合對象** @param collection 多個對象* @return*/public long deleteObject(final Collection collection){return redisTemplate.delete(collection);}/*** 緩存List數據** @param key 緩存的鍵值* @param dataList 待緩存的List數據* @return 緩存的對象*/public <T> long setCacheList(final String key, final List<T> dataList){Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 獲得緩存的list對象** @param key 緩存的鍵值* @return 緩存鍵值對應的數據*/public <T> List<T> getCacheList(final String key){return redisTemplate.opsForList().range(key, 0, -1);}/*** 緩存Set** @param key 緩存鍵值* @param dataSet 緩存的數據* @return 緩存數據的對象*/public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet){BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);Iterator<T> it = dataSet.iterator();while (it.hasNext()){setOperation.add(it.next());}return setOperation;}/*** 獲得緩存的set** @param key* @return*/public <T> Set<T> getCacheSet(final String key){return redisTemplate.opsForSet().members(key);}/*** 緩存Map** @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap){if (dataMap != null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 獲得緩存的Map** @param key* @return*/public <T> Map<String, T> getCacheMap(final String key){return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入數據** @param key Redis鍵* @param hKey Hash鍵* @param value 值*/public <T> void setCacheMapValue(final String key, final String hKey, final T value){redisTemplate.opsForHash().put(key, hKey, value);}/*** 獲取Hash中的數據** @param key Redis鍵* @param hKey Hash鍵* @return Hash中的對象*/public <T> T getCacheMapValue(final String key, final String hKey){HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 刪除Hash中的數據** @param key* @param hKey*/public void delCacheMapValue(final String key, final String hKey){HashOperations hashOperations = redisTemplate.opsForHash();hashOperations.delete(key, hKey);}/*** 獲取多個Hash中的數據** @param key Redis鍵* @param hKeys Hash鍵集合* @return Hash對象集合*/public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys){return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 獲得緩存的基本對象列表** @param pattern 字符串前綴* @return 對象列表*/public Collection<String> keys(final String pattern){return redisTemplate.keys(pattern);}
}
認證功能實現
認證功能主要由三部分組成:
- 根據用戶名和密碼登陸,獲取返回的token保存下來
- 攜帶token請求用戶的路由權限信息,跳轉到項目首頁
- 通過路由守衛,實現動態路由配置
登陸
登錄方法中獲取token,將token存儲到sessionStorage中【或者pinia】。在axios請求中,獲取token,將token傳到后端
請求路由信息
后端:創建過濾器,獲取request中的token,獲取token之后,需要刷新token
獲取路由數據,根據token獲取用戶信息,根據用戶id查詢對應的權限
存儲路由數據
使用pinia進行全局存儲【localstorage存儲,sessionstorage存儲】,前提保障項目中引入pinia
路由頁面
使用vue-router路由頁面,保障項目中引入vue-router
創建核心頁面
頁面其實單頁面,所有的路由都是跳轉到同一個組件下的,只不過在main容器中,替換不同的vue文件【頁面】就可以了
核心頁面由三部分組成
- 左側導航【Aside】
- 樹形結構的菜單,里邊包含了菜單名以及路由信息
- 頭部【Header】
- 主體【Main】
可以使用element-plus實現
動態路由
兩步走:
- 路由查詢出來之后,需要渲染動態路由結構
- 在路由頁面的時候,可以知道頁面跳轉的位置【通過路由守衛實現】
pinia持久化
安裝持久化插件
npm install pinia-plugin-persistedstate
main.js配置
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
store文件
export const xxxx = defineStore('xxx', {state: () => ({ ...... }),getter: {},actions: {},// 持久化persist: {enabled: true,storage: localStorage,key: 'useMenu',path: ['xxxx','xxxx']}
})
自定義icon
導入依賴
npm install fast-globnpm install vite-plugin-svg-icons
vite.config.js
// 引入path
import path from 'path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'export default defineConfig({plugins: [vue(),createSvgIconsPlugin({iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],symbolId: '[name]'})],resolve: {// 使用import導入文件時刻省略后綴extensions: ['.js', '.vue', '.json'],alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}}
})
編寫組件
在 components
下創建 SvgIcon\index.vue
<template><!-- svg:圖標外層容器節點,內部需要與use標簽結合使用 --><svg :style="{ width, height }"><!-- xlink:href執行用哪一個圖標,屬性值務必#icon-圖標名字 --><!-- use標簽fill屬性可以設置圖標的顏色 --><use :xlink:href="prefix + name" :fill="color"></use></svg>
</template><script setup>//接受父組件傳遞過來的參數
defineProps({//xlink:href屬性值前綴prefix: {type: String,default: "#",},//提供使用的圖標名字name: String,//接受父組件傳遞顏色color: {type: String,default: "",},//接受父組件傳遞過來的圖標的寬度width: {type: String,default: "16px",},//接受父組件傳遞過來的圖標的高度height: {type: String,default: "16px",},
})
</script><style lang="scss" scoped></style>
全局注冊,在main.js添加
import SvgIcon from '@/components/SvgIcon/index.vue';
import 'virtual:svg-icons-register';app.component('svg-icon', SvgIcon);
使用
<template #prefix>
<svg-iconv-if="form.icon"slot="prefix":name="form.icon" width="16px" height="25px"/>
</template>
使用elemet-plus的icon
頭部tabs開發
Main組件
Aside組件
配置省略后綴
在 vite.config.js文件中配置
export default defineConfig({......resolve: {extensions: ['.js','.vue','.json','.css'],alias: {'@': fileURLToPath(new URL('./src',import.meta.url))}}
})
安裝vue3 devtools插件
參考鏈接