Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
配置攔截器(不攔截登錄、注冊請求),對所有請求進行攔截,驗證session中保存的登錄用戶是否存在,如果不存在,說明用戶已經下線,清空session,強制返回登錄頁面。
在SpringMVC配置文件中配置攔截器,對文件上傳、用戶登錄、注冊不作攔截。
<!-- 配置攔截器 -->
<mvc:interceptors>
<!-- 使用bean定義一個Interceptor,直接定義在mvc:interceptors根下面的Interceptor將攔截所有的請求 -->
<!-- session失效攔截器 -->
<mvc:interceptor>
<!-- 匹配的是url路徑, 如果不配置或/**,將攔截所有的Controller請求 -->
<mvc:mapping path="/**" />
<!-- 不需要攔截的請求地址 -->
<mvc:exclude-mapping path="/file/**.html" /><!-- 文件上傳請求 -->
<mvc:exclude-mapping path="/login.html" /><!-- 用戶登錄的請求 -->
<bean class="lxkj.zz07.interceptor.MyLoginInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
12345678910111213
通過實現HandleInterceptor接口,來定義用戶攔截器類,重寫preHandle方法,該方法將在Controller處理之前進行調用,SpringMVC中的Interceptor攔截器是鏈式的,可以同時存在 多個Interceptor,然后SpringMVC會根據聲明的前后順序一個接一個的執行,而且所有的Interceptor中的preHandle方法都會在 Controller方法調用之前調用。
SpringMVC的這種Interceptor鏈式結構也是可以進行中斷的,這種中斷方式是令preHandle的返 回值為false,當preHandle的返回值為false的時候整個請求就結束了。
Web應用的安全管理,主要包括兩個方面的內容,一個是用戶身份的認證,即用戶登錄的設計,二是用戶授權,即一個用戶在一個應用系統中能夠執行哪些操作的權限管理。權限管理的設計一般使用角色來管理,即給一個用戶賦予哪些角色,這個用戶就具有哪些權限。
Spring框架體系中,經典的安全體系框架是Security。關于系統的安全管理及各種設計,Spring Security已經大體上都實現了,只需要一些配置和引用就能夠正常使用。SpringBoot使用Security更加的簡單,因為SpringBoot本身的簡單配置使用加上Security的功能豐富全面,可用快速幫助我們構建完善的登陸認證服務。
關于Security,SpringBoot本身有spring-boot-starter-security依賴組件,Spring Cloud微服務全家桶中也有spring-cloud-starter-security依賴組件,并且spring-cloud-starter-security中也包含了spring-boot-starter-security,下面的學習中,會先使spring-boot-starter-security,然后再spring-cloud-starter-security學習安全管理的功能,從SpringBoot單體的登陸注冊和權限管理,到Spring Cloud微服務中構建認證和授權服務,都會一一接觸到。
關于版本的問題,我從SpringBoot1.3.x版的使用到2.1.x的使用,Security的配置也經歷了不小的變化,最準確的配置建議去官網文檔學習。下面的學習中,將使用2.1.5版本,官方文檔地址是: https://docs.spring.io/spring-boot/docs/2.1.5.RELEASE/reference/htmlsingle/ 。 Security的源碼非常復雜,因此我們后面再討論深層次的東西,現在來用實例進行入門學習。
先來看一個入門例子,springboot項目結構我們都很熟悉,先來看依賴:
依賴很簡單,除了一個web組件和thymeleaf視圖組件,就是一個security。接下來看一下啟動類:
可以看到啟動類沒有任何特殊的配置。至于配置文件,我們簡單的配置一下端口,其它不做任何配置:
這樣一個簡單的入門例子就完成了,現在來啟動項目,啟動日志很短,可以看到有一行特殊的日志:
這是我們加入了security組件的依賴之后,引入了security的默認配置,此時就有了一個簡單的登錄功能,打印出的一行是默認密碼的信息,這個密碼是現在沒有任何代碼和配置的狀態下每次啟動隨機生成的,security不僅會生成一個默認密碼,依賴組件中還有一個默認的登陸鏈接/login,還有一個默認的用戶名 user,而且在springboot2.1.x版本中,這個/login有一個非常不錯的默認登錄頁面,下面進行測試:
用戶名輸入user,密碼輸入日志中打印出的隨機密碼,登錄成功后,就會跳轉到默認地址,默認成功的地址就是登錄地址去掉/login,
現在沒有定義任何鏈接匹配這個地址,我們來定義一個簡單的頁面,在resource下面,新建一個templates文件夾,在templates下面新建一個主頁 home.html,內容如下:
然后定義一個controller跳轉到這個頁面:
這樣我們登陸成功后,就能自動跳轉到這個頁面:
這樣,一個最簡單的登錄流程就完成了,我們幾乎沒有做任何配置,只是引入了一個依賴而已。下面我們給security配置一個默認用戶名密碼,這樣就不用每次啟動都用隨機密碼,直接在springboot的默認配置文件中配置:
這樣等登陸就可以使用 admin/admin登陸了。
代碼地址 :https://gitee.com/blueses/spring-boot-security 01
權限控制,最常見的基本上有 2 種
這個兩種到底有什么不同呢?
我們通過下圖來分析一下
添加圖片注釋,不超過 140 字(可選)
ACL 是基于 用戶 -> 權限,直接為每個用戶分配權限 RBAC 基于 用戶 -> 角色 -> 權限,以角色為媒介,來為每個用戶分配權限 這樣做的好處是,某個權限過于敏感時,想要將每個用戶或者部分用戶的權限去掉,就不需要每個用戶的權限都操作一遍,只需要刪除對應角色的權限即可 那在實際的開發中 RBAC 是最常用的權限控制方案,就前端而言,RBAC 主要如何實現的呢? 主要就兩個部分
下面我們就來實現這兩個部分
頁面的訪問,我們都是需要配置路由表的,根據配置路由表的路徑來訪問頁面 那么,我們控制了路由表,不就能控制頁面的訪問了嗎? 實現思路
基本環境
創建項目
npm install -g @vue/cli
vue --version # @vue/cli 5.0.8
vue create vue-router-dome
添加圖片注釋,不超過 140 字(可選)
打開項目,npm run serve運行一下
添加圖片注釋,不超過 140 字(可選)
代碼初始化,刪除不必要的一些文件
添加圖片注釋,不超過 140 字(可選)
我們創建幾個新文件夾
添加圖片注釋,不超過 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 主要為了存放一些公共路由,沒有權限頁可以訪問,比如登錄頁、404頁面這些
因為是用 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
}
}
接下來編寫,創建路由、導出路由
import type { App } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
import remainingRouter from './modules/remaining'
// 創建路由實例
const router=createRouter({
history: createWebHashHistory(), // createWebHashHistory URL帶#,createWebHistory URL不帶#
strict: true,
routes: remainingRouter as RouteRecordRaw[],
scrollBehavior: ()=> ({ left: 0, top: 0 })
})
// 導出路由實例
export const setupRouter=(app: App<Element>)=> {
app.use(router)
}
export default router
在main.ts中導入下
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'
// 創建實例
const setupAll=async ()=> {
const app=createApp(App)
setupRouter(app)
app.mount('#app')
}
setupAll()
接下來寫下 Layout 架構
我們要實現的效果,是一個后臺管理頁面的側邊欄,點擊菜單右邊就能跳轉到對應路由所在頁面
添加圖片注釋,不超過 140 字(可選)
創建
AppMain.vue 右邊路由跳轉頁
Sidebar.vue 側邊欄
index.vue 作為 layout 架構的統一出口
添加圖片注釋,不超過 140 字(可選)
<!--
@description: AppMain
-->
<template>
<div>
<router-view v-slot="{ Component, route }">
<transition name="fade-transform" mode="out-in"> <!-- 設置過渡動畫 -->
<keep-alive>
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
上面是一種動態路由的固定寫法,需要與的路由配置進行對應 其中最主要的就是 <component :is="Component" :key="route.fullPath" /> 中的 key,這是為確定路由跳轉對應頁面的標識,沒這個就跳不了 有一個小知識點
//路徑:http://127.0.0.1:3000/user?id=1
console.log(route.path) // 輸出 /user
console.log(route.fullPath) // 輸出 /user?id=1
為了實現右邊側邊欄,需要引入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 組件庫樣式文件
// 創建實例
const setupAll=async ()=> {
const app=createApp(App)
app.use(ElementPlus)
app.mount('#app')
}
setupAll()
我們來編寫下 側邊欄
<!--
@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()
// 通過計算屬性,路由發生變化時更新路由信息
const routers=computed(()=> {
return filterRoutes(router.getRoutes()) // router.getRoutes() 用于獲取路由信息
})
</script>
統一導出 layout 架構,加一點小樣式
<!--
@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運行一下
添加圖片注釋,不超過 140 字(可選)
通常我們實現頁面權限管理,比較常見的方案是,有權限的路由信息由后端傳給前端,前端再根據路由信息進行渲染
我們先安裝下 pinia 模擬下后端傳過來的數據
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: "系統管理",
meta: undefined,
},
]
}),
getters: {},
actions: {},
});
好了,我們把模擬的路由數據,加到本地路由中
添加圖片注釋,不超過 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) // 動態添加可訪問路由表
})
next()
})
// 路由跳轉之后調用
router.afterEach((to)=> { })
添加圖片注釋,不超過 140 字(可選)
添加圖片注釋,不超過 140 字(可選)
報錯了,為什么呢?
對比路由表的數據,原來,組件模塊的數據與公共路由的數據不一致
添加圖片注釋,不超過 140 字(可選)
我們需要把模擬后端傳過來的數據處理一下
添加圖片注釋,不超過 140 字(可選)
// router.ts
import Layout from '@/layout/index.vue';
import type { RouteRecordRaw } from 'vue-router'
/* 處理從后端傳過來的路由數據 */
export const formatRoutes=(routes: any[])=> {
const formatedRoutes: RouteRecordRaw[]=[]
routes.forEach(route=> {
formatedRoutes.push(
{
...route,
component: Layout, // 主要是將這個 null -> 組件
children: route.children.map((child: any)=> {
return {
...child,
component: ()=> import(`@/views${child.component}`), // 根據 本地路徑配置頁面路徑
}
}),
}
)
})
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) // 動態添加可訪問路由表
})
next()
})
// 路由跳轉之后調用
router.afterEach((to)=> { })
main.ts引入一下
import './permission'
可以正常訪問了
添加圖片注釋,不超過 140 字(可選)
除了頁面權限,外我們還有按鈕權限
可以通過自定義指令來完成,permission.ts 中定義一下
添加圖片注釋,不超過 140 字(可選)
/* 按鈕權限 */
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('權限不存在')
}
})
}
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'
// 創建實例
const setupAll=async ()=> {
const app=createApp(App)
setupRouter(app)
setupAuth(app)
app.use(ElementPlus)
app.use(createPinia())
app.mount('#app')
}
setupAll()
還是在store那里加一下模擬數據
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: "系統管理",
meta: undefined,
},
],
permissions: [
// '*:*:*', // 所有權限
'system:user:create',
'system:user:update',
'system:user:delete',
]
}),
});
user.vue加入幾個按鈕,使用自定義指令
<!-- user.vue -->
<template>
<div>
<el-button type="primary" v-hasPermi="['system:user:create']">創建</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']">沒權限</el-button>
</div>
</template>
system:user:admin這個權限沒有配置,無法顯示
添加圖片注釋,不超過 140 字(可選)
加一下權限
添加圖片注釋,不超過 140 字(可選)
添加圖片注釋,不超過 140 字(可選)
用戶權限我們使用 v-hasPermi自定義指令,其原理是通過刪除當前元素,來實現隱藏
如果使用 Element Plus 的標簽頁呢
我們在 src/views/home.vue 寫一下基本樣式
<!--
@description: 主頁
-->
<template>
<div>
<el-tabs>
<el-tab-pane label="標簽一" name="first">標簽一</el-tab-pane>
<el-tab-pane label="標簽二" name="second">標簽二</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang='ts'>
</script>
添加圖片注釋,不超過 140 字(可選)
我們加下按鈕權限控制
<template>
<div>
<el-tabs v-model="activeName">
<el-tab-pane label="標簽一" v-hasPermi="['system:tabs:first']" name="first">標簽一</el-tab-pane>
<el-tab-pane label="標簽二" name="second">標簽二</el-tab-pane>
</el-tabs>
</div>
</template>
添加圖片注釋,不超過 140 字(可選)
因為這個權限我們沒有配置,標簽頁內容隱藏了,這沒問題
但是,標簽沒隱藏啊,通常要是標簽一沒權限,應該是標簽項、和標簽內容都隱藏才對
為什么會這樣呢?
我們在 hasPermi 自定義指令中,打印下獲取到的元素
添加圖片注釋,不超過 140 字(可選)
添加圖片注釋,不超過 140 字(可選)
id 為pane-first、pane-second元素對應位置在哪里,我們找一下 需要先把指令去掉,因為元素都被我們刪除的話,我們看不到具體DOM結構
添加圖片注釋,不超過 140 字(可選)
添加圖片注釋,不超過 140 字(可選)
添加圖片注釋,不超過 140 字(可選)
對比一下,明顯可以看出 hasPermi 自定義指令獲取到只是標簽內容的元素 那怎么辦? 解決辦法一:根據當前元素,一層層找到標簽項,然后刪除,這樣是可以。但是這樣太麻煩了,也只能用于標簽頁,那要是其他組件有這樣的問題咋辦 解決辦法二:我們寫一個函數判斷權限是否存在,再通過 v-if 進行隱藏
*請認真填寫需求信息,我們會在24小時內與您取得聯系。