配置攔截器(不攔截登錄、注冊(cè)請(qǐng)求),對(duì)所有請(qǐng)求進(jìn)行攔截,驗(yàn)證session中保存的登錄用戶是否存在,如果不存在,說明用戶已經(jīng)下線,清空session,強(qiáng)制返回登錄頁面。
在SpringMVC配置文件中配置攔截器,對(duì)文件上傳、用戶登錄、注冊(cè)不作攔截。
<!-- 配置攔截器 -->
<mvc:interceptors>
<!-- 使用bean定義一個(gè)Interceptor,直接定義在mvc:interceptors根下面的Interceptor將攔截所有的請(qǐng)求 -->
<!-- session失效攔截器 -->
<mvc:interceptor>
<!-- 匹配的是url路徑, 如果不配置或/**,將攔截所有的Controller請(qǐng)求 -->
<mvc:mapping path="/**" />
<!-- 不需要攔截的請(qǐng)求地址 -->
<mvc:exclude-mapping path="/file/**.html" /><!-- 文件上傳請(qǐng)求 -->
<mvc:exclude-mapping path="/login.html" /><!-- 用戶登錄的請(qǐng)求 -->
<bean class="lxkj.zz07.interceptor.MyLoginInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
12345678910111213
通過實(shí)現(xiàn)HandleInterceptor接口,來定義用戶攔截器類,重寫preHandle方法,該方法將在Controller處理之前進(jìn)行調(diào)用,SpringMVC中的Interceptor攔截器是鏈?zhǔn)降?,可以同時(shí)存在 多個(gè)Interceptor,然后SpringMVC會(huì)根據(jù)聲明的前后順序一個(gè)接一個(gè)的執(zhí)行,而且所有的Interceptor中的preHandle方法都會(huì)在 Controller方法調(diào)用之前調(diào)用。
SpringMVC的這種Interceptor鏈?zhǔn)浇Y(jié)構(gòu)也是可以進(jìn)行中斷的,這種中斷方式是令preHandle的返 回值為false,當(dāng)preHandle的返回值為false的時(shí)候整個(gè)請(qǐng)求就結(jié)束了。
Web應(yīng)用的安全管理,主要包括兩個(gè)方面的內(nèi)容,一個(gè)是用戶身份的認(rèn)證,即用戶登錄的設(shè)計(jì),二是用戶授權(quán),即一個(gè)用戶在一個(gè)應(yīng)用系統(tǒng)中能夠執(zhí)行哪些操作的權(quán)限管理。權(quán)限管理的設(shè)計(jì)一般使用角色來管理,即給一個(gè)用戶賦予哪些角色,這個(gè)用戶就具有哪些權(quán)限。
Spring框架體系中,經(jīng)典的安全體系框架是Security。關(guān)于系統(tǒng)的安全管理及各種設(shè)計(jì),Spring Security已經(jīng)大體上都實(shí)現(xiàn)了,只需要一些配置和引用就能夠正常使用。SpringBoot使用Security更加的簡單,因?yàn)镾pringBoot本身的簡單配置使用加上Security的功能豐富全面,可用快速幫助我們構(gòu)建完善的登陸認(rèn)證服務(wù)。
關(guān)于Security,SpringBoot本身有spring-boot-starter-security依賴組件,Spring Cloud微服務(wù)全家桶中也有spring-cloud-starter-security依賴組件,并且spring-cloud-starter-security中也包含了spring-boot-starter-security,下面的學(xué)習(xí)中,會(huì)先使spring-boot-starter-security,然后再spring-cloud-starter-security學(xué)習(xí)安全管理的功能,從SpringBoot單體的登陸注冊(cè)和權(quán)限管理,到Spring Cloud微服務(wù)中構(gòu)建認(rèn)證和授權(quán)服務(wù),都會(huì)一一接觸到。
關(guān)于版本的問題,我從SpringBoot1.3.x版的使用到2.1.x的使用,Security的配置也經(jīng)歷了不小的變化,最準(zhǔn)確的配置建議去官網(wǎng)文檔學(xué)習(xí)。下面的學(xué)習(xí)中,將使用2.1.5版本,官方文檔地址是: https://docs.spring.io/spring-boot/docs/2.1.5.RELEASE/reference/htmlsingle/ 。 Security的源碼非常復(fù)雜,因此我們后面再討論深層次的東西,現(xiàn)在來用實(shí)例進(jìn)行入門學(xué)習(xí)。
先來看一個(gè)入門例子,springboot項(xiàng)目結(jié)構(gòu)我們都很熟悉,先來看依賴:
依賴很簡單,除了一個(gè)web組件和thymeleaf視圖組件,就是一個(gè)security。接下來看一下啟動(dòng)類:
可以看到啟動(dòng)類沒有任何特殊的配置。至于配置文件,我們簡單的配置一下端口,其它不做任何配置:
這樣一個(gè)簡單的入門例子就完成了,現(xiàn)在來啟動(dòng)項(xiàng)目,啟動(dòng)日志很短,可以看到有一行特殊的日志:
這是我們加入了security組件的依賴之后,引入了security的默認(rèn)配置,此時(shí)就有了一個(gè)簡單的登錄功能,打印出的一行是默認(rèn)密碼的信息,這個(gè)密碼是現(xiàn)在沒有任何代碼和配置的狀態(tài)下每次啟動(dòng)隨機(jī)生成的,security不僅會(huì)生成一個(gè)默認(rèn)密碼,依賴組件中還有一個(gè)默認(rèn)的登陸鏈接/login,還有一個(gè)默認(rèn)的用戶名 user,而且在springboot2.1.x版本中,這個(gè)/login有一個(gè)非常不錯(cuò)的默認(rèn)登錄頁面,下面進(jìn)行測(cè)試:
用戶名輸入user,密碼輸入日志中打印出的隨機(jī)密碼,登錄成功后,就會(huì)跳轉(zhuǎn)到默認(rèn)地址,默認(rèn)成功的地址就是登錄地址去掉/login,
現(xiàn)在沒有定義任何鏈接匹配這個(gè)地址,我們來定義一個(gè)簡單的頁面,在resource下面,新建一個(gè)templates文件夾,在templates下面新建一個(gè)主頁 home.html,內(nèi)容如下:
然后定義一個(gè)controller跳轉(zhuǎn)到這個(gè)頁面:
這樣我們登陸成功后,就能自動(dòng)跳轉(zhuǎn)到這個(gè)頁面:
這樣,一個(gè)最簡單的登錄流程就完成了,我們幾乎沒有做任何配置,只是引入了一個(gè)依賴而已。下面我們給security配置一個(gè)默認(rèn)用戶名密碼,這樣就不用每次啟動(dòng)都用隨機(jī)密碼,直接在springboot的默認(rèn)配置文件中配置:
這樣等登陸就可以使用 admin/admin登陸了。
代碼地址 :https://gitee.com/blueses/spring-boot-security 01
權(quán)限控制,最常見的基本上有 2 種
這個(gè)兩種到底有什么不同呢?
我們通過下圖來分析一下
添加圖片注釋,不超過 140 字(可選)
ACL 是基于 用戶 -> 權(quán)限,直接為每個(gè)用戶分配權(quán)限 RBAC 基于 用戶 -> 角色 -> 權(quán)限,以角色為媒介,來為每個(gè)用戶分配權(quán)限 這樣做的好處是,某個(gè)權(quán)限過于敏感時(shí),想要將每個(gè)用戶或者部分用戶的權(quán)限去掉,就不需要每個(gè)用戶的權(quán)限都操作一遍,只需要?jiǎng)h除對(duì)應(yīng)角色的權(quán)限即可 那在實(shí)際的開發(fā)中 RBAC 是最常用的權(quán)限控制方案,就前端而言,RBAC 主要如何實(shí)現(xiàn)的呢? 主要就兩個(gè)部分
下面我們就來實(shí)現(xiàn)這兩個(gè)部分
頁面的訪問,我們都是需要配置路由表的,根據(jù)配置路由表的路徑來訪問頁面 那么,我們控制了路由表,不就能控制頁面的訪問了嗎? 實(shí)現(xiàn)思路
基本環(huán)境
創(chuàng)建項(xiàng)目
npm install -g @vue/cli
vue --version # @vue/cli 5.0.8
vue create vue-router-dome
添加圖片注釋,不超過 140 字(可選)
打開項(xiàng)目,npm run serve運(yùn)行一下
添加圖片注釋,不超過 140 字(可選)
代碼初始化,刪除不必要的一些文件
添加圖片注釋,不超過 140 字(可選)
我們創(chuàng)建幾個(gè)新文件夾
添加圖片注釋,不超過 140 字(可選)
寫下基本的頁面
添加圖片注釋,不超過 140 字(可選)
<!-- home.vue -->
<template>
<div>主頁</div>
</template>
<!-- menu.vue -->
<template>
<div>菜單管理</div>
</template>
<!-- user.vue -->
<template>
<div>用戶管理</div>
</template>
寫下路由配置
添加圖片注釋,不超過 140 字(可選)
// remaining.ts
import Layout from '@/layout/index.vue'
const remainingRouter: AppRouteRecordRaw[]=[
{
path: '/remaining',
component: Layout,
redirect: 'home',
children: [
{
path: '/remaining/home',
component: ()=> import('@/views/home.vue'),
name: '首頁',
meta: {},
}
],
name: '主頁管理',
meta: undefined
},
]
export default remainingRouter
remaining 主要為了存放一些公共路由,沒有權(quán)限頁可以訪問,比如登錄頁、404頁面這些
因?yàn)槭怯?typescript 編寫的,我們需要加一下聲明文件,定義下 remainingRouter 的類型
添加圖片注釋,不超過 140 字(可選)
// router.d.ts
import type { RouteRecordRaw } from 'vue-router'
import { defineComponent } from 'vue'
declare module 'vue-router' {
interface RouteMeta extends Record<string | number | symbol, unknown> {
hidden?: boolean
alwaysShow?: boolean
title?: string
icon?: string
noCache?: boolean
breadcrumb?: boolean
affix?: boolean
activeMenu?: string
noTagsView?: boolean
followAuth?: string
canTo?: boolean
}
}
type Component<T=any>=| ReturnType<typeof defineComponent>
| (()=> Promise<typeof import('*.vue')>)
| (()=> Promise<T>)
declare global {
interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
name: string
meta: RouteMeta
component?: Component | string
children?: AppRouteRecordRaw[]
props?: Recordable
fullPath?: string
keepAlive?: boolean
}
interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
icon: any
name: string
meta: RouteMeta
component: string
componentName?: string
path: string
redirect: string
children?: AppCustomRouteRecordRaw[]
keepAlive?: boolean
visible?: boolean
parentId?: number
alwaysShow?: boolean
}
}
接下來編寫,創(chuàng)建路由、導(dǎo)出路由
import type { App } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
import remainingRouter from './modules/remaining'
// 創(chuàng)建路由實(shí)例
const router=createRouter({
history: createWebHashHistory(), // createWebHashHistory URL帶#,createWebHistory URL不帶#
strict: true,
routes: remainingRouter as RouteRecordRaw[],
scrollBehavior: ()=> ({ left: 0, top: 0 })
})
// 導(dǎo)出路由實(shí)例
export const setupRouter=(app: App<Element>)=> {
app.use(router)
}
export default router
在main.ts中導(dǎo)入下
import { createApp } from 'vue'
import App from './App.vue'
import { setupRouter } from './router/index' // 路由
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 創(chuàng)建實(shí)例
const setupAll=async ()=> {
const app=createApp(App)
setupRouter(app)
app.mount('#app')
}
setupAll()
接下來寫下 Layout 架構(gòu)
我們要實(shí)現(xiàn)的效果,是一個(gè)后臺(tái)管理頁面的側(cè)邊欄,點(diǎn)擊菜單右邊就能跳轉(zhuǎn)到對(duì)應(yīng)路由所在頁面
添加圖片注釋,不超過 140 字(可選)
創(chuàng)建
AppMain.vue 右邊路由跳轉(zhuǎn)頁
Sidebar.vue 側(cè)邊欄
index.vue 作為 layout 架構(gòu)的統(tǒng)一出口
添加圖片注釋,不超過 140 字(可選)
<!--
@description: AppMain
-->
<template>
<div>
<router-view v-slot="{ Component, route }">
<transition name="fade-transform" mode="out-in"> <!-- 設(shè)置過渡動(dòng)畫 -->
<keep-alive>
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
上面是一種動(dòng)態(tài)路由的固定寫法,需要與的路由配置進(jìn)行對(duì)應(yīng) 其中最主要的就是 <component :is="Component" :key="route.fullPath" /> 中的 key,這是為確定路由跳轉(zhuǎn)對(duì)應(yīng)頁面的標(biāo)識(shí),沒這個(gè)就跳不了 有一個(gè)小知識(shí)點(diǎn)
//路徑:http://127.0.0.1:3000/user?id=1
console.log(route.path) // 輸出 /user
console.log(route.fullPath) // 輸出 /user?id=1
為了實(shí)現(xiàn)右邊側(cè)邊欄,需要引入element plus來快速搭建
pnpm install element-plus
main.ts改造一下,完整引入element-plus
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus' // element-plus 組件庫
import 'element-plus/dist/index.css' // element-plus 組件庫樣式文件
// 創(chuàng)建實(shí)例
const setupAll=async ()=> {
const app=createApp(App)
app.use(ElementPlus)
app.mount('#app')
}
setupAll()
我們來編寫下 側(cè)邊欄
<!--
@description: Sidebar
-->
<template>
<div>
<el-menu active-text-color="#ffd04b" background-color="#304156" default-active="2" text-color="#fff" router>
<el-sub-menu :index="item.path" v-for="item in routers">
<template #title>{{ item.name }}</template>
<el-menu-item :index="child.path" v-for="child in item.children">{{ child.name }}</el-menu-item>
</el-sub-menu>
</el-menu>
</div>
</template>
<script setup lang='ts'>
import { filterRoutes } from '@/utils/router';
import { computed } from 'vue';
import { useRouter } from 'vue-router';
const router=useRouter()
// 通過計(jì)算屬性,路由發(fā)生變化時(shí)更新路由信息
const routers=computed(()=> {
return filterRoutes(router.getRoutes()) // router.getRoutes() 用于獲取路由信息
})
</script>
統(tǒng)一導(dǎo)出 layout 架構(gòu),加一點(diǎn)小樣式
<!--
@description: layout index
-->
<template>
<div class="app-wrapper">
<Sidebar class="sidebar-container" />
<App-Main class="main-container" />
</div>
</template>
<script setup lang='ts'>
import { ref, reactive } from 'vue'
import Sidebar from './components/Sidebar.vue'
import AppMain from './components/AppMain.vue'
</script>
<style scoped>
.app-wrapper {
display: flex;
}
.sidebar-container {
width: 200px;
height: 100vh;
background-color: #304156;
color: #fff;
}
.main-container {
flex: 1;
height: 100vh;
background-color: #f0f2f5;
}
</style>
pnpm run serve運(yùn)行一下
添加圖片注釋,不超過 140 字(可選)
通常我們實(shí)現(xiàn)頁面權(quán)限管理,比較常見的方案是,有權(quán)限的路由信息由后端傳給前端,前端再根據(jù)路由信息進(jìn)行渲染
我們先安裝下 pinia 模擬下后端傳過來的數(shù)據(jù)
pnpm install pinia
添加圖片注釋,不超過 140 字(可選)
import { defineStore } from "pinia";
interface AuthStore {
// 菜單
menus: any[];
}
export const useAuthStore=defineStore("authState", {
state: (): AuthStore=> ({
menus: [
{
path: "/routing",
component: null,
redirect: "user",
children: [
{
path: "/routing/user",
component: "/user.vue",
name: "用戶管理",
meta: {},
},
{
path: "/routing/menu",
component: "/menu.vue",
name: "菜單管理",
meta: {},
}
],
name: "系統(tǒng)管理",
meta: undefined,
},
]
}),
getters: {},
actions: {},
});
好了,我們把模擬的路由數(shù)據(jù),加到本地路由中
添加圖片注釋,不超過 140 字(可選)
// permission.ts
import router from './router'
import type { RouteRecordRaw } from 'vue-router'
import { formatRoutes } from './utils/router'
import { useAuthStore } from '@/store';
import { App } from 'vue';
// 路由加載前
router.beforeEach(async (to, from, next)=> {
const { menus }=useAuthStore()
routerList.forEach((route)=> {
router.addRoute(menus as unknown as RouteRecordRaw) // 動(dòng)態(tài)添加可訪問路由表
})
next()
})
// 路由跳轉(zhuǎn)之后調(diào)用
router.afterEach((to)=> { })
添加圖片注釋,不超過 140 字(可選)
添加圖片注釋,不超過 140 字(可選)
報(bào)錯(cuò)了,為什么呢?
對(duì)比路由表的數(shù)據(jù),原來,組件模塊的數(shù)據(jù)與公共路由的數(shù)據(jù)不一致
添加圖片注釋,不超過 140 字(可選)
我們需要把模擬后端傳過來的數(shù)據(jù)處理一下
添加圖片注釋,不超過 140 字(可選)
// router.ts
import Layout from '@/layout/index.vue';
import type { RouteRecordRaw } from 'vue-router'
/* 處理從后端傳過來的路由數(shù)據(jù) */
export const formatRoutes=(routes: any[])=> {
const formatedRoutes: RouteRecordRaw[]=[]
routes.forEach(route=> {
formatedRoutes.push(
{
...route,
component: Layout, // 主要是將這個(gè) null -> 組件
children: route.children.map((child: any)=> {
return {
...child,
component: ()=> import(`@/views${child.component}`), // 根據(jù) 本地路徑配置頁面路徑
}
}),
}
)
})
return formatedRoutes;
}
再修改下permission.ts
import router from './router'
import type { RouteRecordRaw } from 'vue-router'
import { formatRoutes } from './utils/router'
import { useAuthStore } from '@/store';
import { App } from 'vue';
// 路由加載前
router.beforeEach(async (to, from, next)=> {
const { menus }=useAuthStore()
const routerList=menus
routerList.forEach((route)=> {
router.addRoute(route as unknown as RouteRecordRaw) // 動(dòng)態(tài)添加可訪問路由表
})
next()
})
// 路由跳轉(zhuǎn)之后調(diào)用
router.afterEach((to)=> { })
main.ts引入一下
import './permission'
可以正常訪問了
添加圖片注釋,不超過 140 字(可選)
除了頁面權(quán)限,外我們還有按鈕權(quán)限
可以通過自定義指令來完成,permission.ts 中定義一下
添加圖片注釋,不超過 140 字(可選)
/* 按鈕權(quán)限 */
export function hasPermi(app: App<Element>) {
app.directive('hasPermi', (el, binding)=> {
const { permissions }=useAuthStore()
const { value }=binding
const all_permission='*:*:*'
if (value && value instanceof Array && value.length > 0) {
const permissionFlag=value
const hasPermissions=permissions.some((permission: string)=> {
return all_permission===permission || permissionFlag.includes(permission)
})
if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error('權(quán)限不存在')
}
})
}
export const setupAuth=(app: App<Element>)=> {
hasPermi(app)
}
需要掛載到main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { setupRouter } from './router/index'
import ElementPlus from 'element-plus'
import { createPinia } from 'pinia'
import { setupAuth } from './permission'
import 'element-plus/dist/index.css'
import './permission'
// 創(chuàng)建實(shí)例
const setupAll=async ()=> {
const app=createApp(App)
setupRouter(app)
setupAuth(app)
app.use(ElementPlus)
app.use(createPinia())
app.mount('#app')
}
setupAll()
還是在store那里加一下模擬數(shù)據(jù)
export const useAuthStore=defineStore("authState", {
state: (): AuthStore=> ({
menus: [
{
path: "/routing",
component: null,
redirect: "user",
children: [
{
path: "/routing/user",
component: "/user.vue",
name: "用戶管理",
meta: {},
},
{
path: "/routing/menu",
component: "/menu.vue",
name: "菜單管理",
meta: {},
}
],
name: "系統(tǒng)管理",
meta: undefined,
},
],
permissions: [
// '*:*:*', // 所有權(quán)限
'system:user:create',
'system:user:update',
'system:user:delete',
]
}),
});
user.vue加入幾個(gè)按鈕,使用自定義指令
<!-- user.vue -->
<template>
<div>
<el-button type="primary" v-hasPermi="['system:user:create']">創(chuàng)建</el-button>
<el-button type="primary" v-hasPermi="['system:user:update']">更新</el-button>
<el-button type="primary" v-hasPermi="['system:user:delete']">刪除</el-button>
<el-button type="primary" v-hasPermi="['system:user:admin']">沒權(quán)限</el-button>
</div>
</template>
system:user:admin這個(gè)權(quán)限沒有配置,無法顯示
添加圖片注釋,不超過 140 字(可選)
加一下權(quán)限
添加圖片注釋,不超過 140 字(可選)
添加圖片注釋,不超過 140 字(可選)
用戶權(quán)限我們使用 v-hasPermi自定義指令,其原理是通過刪除當(dāng)前元素,來實(shí)現(xiàn)隱藏
如果使用 Element Plus 的標(biāo)簽頁呢
我們?cè)?src/views/home.vue 寫一下基本樣式
<!--
@description: 主頁
-->
<template>
<div>
<el-tabs>
<el-tab-pane label="標(biāo)簽一" name="first">標(biāo)簽一</el-tab-pane>
<el-tab-pane label="標(biāo)簽二" name="second">標(biāo)簽二</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang='ts'>
</script>
添加圖片注釋,不超過 140 字(可選)
我們加下按鈕權(quán)限控制
<template>
<div>
<el-tabs v-model="activeName">
<el-tab-pane label="標(biāo)簽一" v-hasPermi="['system:tabs:first']" name="first">標(biāo)簽一</el-tab-pane>
<el-tab-pane label="標(biāo)簽二" name="second">標(biāo)簽二</el-tab-pane>
</el-tabs>
</div>
</template>
添加圖片注釋,不超過 140 字(可選)
因?yàn)檫@個(gè)權(quán)限我們沒有配置,標(biāo)簽頁內(nèi)容隱藏了,這沒問題
但是,標(biāo)簽沒隱藏啊,通常要是標(biāo)簽一沒權(quán)限,應(yīng)該是標(biāo)簽項(xiàng)、和標(biāo)簽內(nèi)容都隱藏才對(duì)
為什么會(huì)這樣呢?
我們?cè)?hasPermi 自定義指令中,打印下獲取到的元素
添加圖片注釋,不超過 140 字(可選)
添加圖片注釋,不超過 140 字(可選)
id 為pane-first、pane-second元素對(duì)應(yīng)位置在哪里,我們找一下 需要先把指令去掉,因?yàn)樵囟急晃覀儎h除的話,我們看不到具體DOM結(jié)構(gòu)
添加圖片注釋,不超過 140 字(可選)
添加圖片注釋,不超過 140 字(可選)
添加圖片注釋,不超過 140 字(可選)
對(duì)比一下,明顯可以看出 hasPermi 自定義指令獲取到只是標(biāo)簽內(nèi)容的元素 那怎么辦? 解決辦法一:根據(jù)當(dāng)前元素,一層層找到標(biāo)簽項(xiàng),然后刪除,這樣是可以。但是這樣太麻煩了,也只能用于標(biāo)簽頁,那要是其他組件有這樣的問題咋辦 解決辦法二:我們寫一個(gè)函數(shù)判斷權(quán)限是否存在,再通過 v-if 進(jìn)行隱藏
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。