若依框架登录鉴权:1.获取token(过期在响应拦截器中实现),2.基于RBAC模型获取用户、角色和权限信息(在路由前置守卫),3.根据用户权限动态生成(从字符串->组件,根据permission添加动态路由信息)和添加路由addRoutes(在路由前置守卫)
若依框架(Ruoyi)后端的登录权限身份认证流程是一个复杂但高效的过程,它确保了系统的安全性和数据的保护。以下是一个典型的若依框架后端登录权限身份认证流程,基于多个来源的信息进行归纳和整理:
1.发起请求获取认证凭证(token)
现象:用户未登录或者token过期,刷新页面将重定向至登录页面
- 如果用户身份验证通过,系统会生成一个认证凭证(如JWT,即JSON Web Token)。
- 认证凭证的生成过程可能包括生成一个唯一的UUID作为token的一部分,并设置token的有效期(这个有效期可能是通过Redis等缓存系统来控制的,因为JWT本身不直接支持自动刷新有效期)。
- 生成的token会包含用户的基本信息和权限信息,以便后续进行权限控制和身份验证。
- 后端通过验证后返回一个包含用户信息的令牌(Token),前端将这个令牌保存起来(如存储在Cookie、LocalStorage或SessionStorage中),以后的请求都需要带上这个令牌进行验证。
//login.vue
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true;
if (this.loginForm.rememberMe) {
Cookies.set("username", this.loginForm.username, { expires: 30 });
Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });
} else {
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove('rememberMe');
}
this.$store.dispatch("Login", this.loginForm).then(() => {
//存储title至vuex
this.$store.commit("settings/SET_HEADTITLE",this.title)
this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
}).catch(() => {
this.loading = false;
if (this.captchaEnabled) {
this.getCode();
}
});
}
});
}
//store/modules/user.js
// 前端登录请求
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
//!!!key!!!
//获取并设置后端传过来的token
setToken(res.data.token)
commit('SET_TOKEN', res.data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
//system/SysLoginController
//后端登录响应
/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@SaIgnore
@PostMapping("/login")
public R<Map<String, Object>> login(@Validated @RequestBody LoginBody loginBody) {
Map<String, Object> ajax = new HashMap<>();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
return R.ok(ajax);
}
若依框架前端怎样判断token是否过期,并且要重新登录获取token:响应拦截器
在前端应用中,使用响应拦截器(response interceptor)来判断token是否过期,并在需要时重新登录以获取新的token是一种常见的做法。这通常涉及到以下几个步骤:
-
设置响应拦截器:在发起HTTP请求的库(如axios)中设置拦截器,以便在接收到响应时执行自定义逻辑。
-
检查响应状态码:在拦截器中检查响应的状态码,特别是那些表明需要身份验证的状态码(如401、403等)。
-
判断token是否过期:虽然401状态码通常表示未授权,但具体是否因为token过期导致需要额外的逻辑来判断。有时候,后端会在响应头中返回一个特定的字段(如
WWW-Authenticate
)来指示这一点,但更常见的是通过解析响应体中的信息(如错误信息)来判断。不过,在某些情况下,你可能只是简单地根据收到了401就认为token已过期。 -
重新登录获取token:如果确定token已过期,你可以尝试使用某种形式的“刷新token”来自动获取新的access token,或者引导用户重新登录。这取决于你的应用是否支持无感知重新登录(使用refresh token)以及后端的实现。
-
更新本地存储和重试请求:如果成功获取了新的token,更新本地存储中的token,并根据需要重试之前的请求。
-
错误处理:处理在重新登录过程中可能出现的任何错误,并向用户显示适当的反馈。
//utils/request.js
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
isRelogin.show = false;
store.dispatch('LogOut').then(() => {
location.href = process.env.VUE_APP_CONTEXT_PATH + "index";
})
}).catch(() => {
isRelogin.show = false;
});
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
}
// else if (code === 500) {
// Message({ message: msg, type: 'error' })//redis错误,待解决
// return Promise.reject(new Error(msg))
// }
else if (code === 601) {
Message({ message: msg, type: 'warning' })
return Promise.reject('error')
} else if (code !== 200) {
Notification.error({ title: msg })
return Promise.reject('error')
} else {
return res.data
}
},
error => {
console.log('err' + error)
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
Message({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
}
)
若报401错误表明token到期,401错误是一种常见的HTTP状态码,它表示“未授权”(Unauthorized)访问。当用户尝试访问受保护的资源时,如果未能提供有效的身份验证信息或提供的身份验证信息被拒绝,服务器就会返回401错误。这种错误通常出现在需要登录或授权才能访问的网站上,如银行网站、社交媒体网站等。
2. 获取认证信息(user、roles and permission)
不同用户的权限不同,右侧导航条【大层面】和页面中操作按钮【小层面】不同
- 将生成的token和用户的相关信息(如用户ID、角色、权限等)缓存起来,以便后续快速验证和查询。
//store/modules/user.js
// 获取用户信息【前端请求】
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(res => {
const user = res.data.user
const avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : user.avatar;
if (res.data.roles && res.data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', res.data.roles)
commit('SET_PERMISSIONS', res.data.permissions)
} else {
commit('SET_ROLES', ['ROLE_DEFAULT'])
}
commit('SET_NAME', user.userName)
commit('SET_AVATAR', avatar)
resolve(res)
}).catch(error => {
reject(error)
})
})
},
//system/SysLoginController
//后端响应返回用户信息【user、roles and permisssions】
/**
* 获取用户信息
*
* @return 用户信息
*/
@GetMapping("getInfo")
public R<Map<String, Object>> getInfo() {
LoginUser loginUser = LoginHelper.getLoginUser();
SysUser user = userService.selectUserById(loginUser.getUserId());
Map<String, Object> ajax = new HashMap<>();
ajax.put("user", user);
ajax.put("roles", loginUser.getRolePermission());
ajax.put("permissions", loginUser.getMenuPermission());
return R.ok(ajax);
}
管理员
普通员工
根据权限动态生成并添加路由
步骤 1: 定义静态路由和异步路由
首先,你需要定义你的路由,通常分为静态路由(不需要权限即可访问的路由,如登录页、404页面等)和异步路由(需要权限的路由,如用户管理、订单管理等)。
/** * Note: 路由配置项 * * hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1 * alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 * // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面 * // 若你想不管路由下面的 children 声明的个数都显示你的根路由 * // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由 * redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 * name:'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题 * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数 * roles: ['admin', 'common'] // 访问路由的角色权限 * permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限 * meta : { noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false) title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字 icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示 activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。 } */
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
// 静态路由
const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
}
]
// 异步路由(示例)
const asyncRoutes = [
{
path: '/user',
component: () => import('@/views/user/index'),
meta: { title: '用户管理', roles: ['admin', 'editor'] }
},
// 更多路由...
]
const createRouter = () => new Router({
// mode: 'history', // 需要后端支持
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const router = createRouter()
// 动态添加路由
function addAsyncRoutes(routes) {
routes.forEach(route => {
router.addRoute(route)
})
}
export {
router,
addAsyncRoutes,
asyncRoutes
}
步骤 2: 路由守卫与权限验证和路由添加
在用户登录后,你需要从后端获取用户的权限信息,并根据这些权限信息来过滤和添加路由。
你还需要设置路由守卫来确保用户在没有权限时不能访问某些页面。
//src/permission.js
//前端路由守卫
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
//获取路由的mata.title属性,并存储在Vuex中
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
if (store.getters.roles.length === 0) {
isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => {
isRelogin.show = false
//生成路由
store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
//添加路由
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})
}).catch(err => {
store.dispatch('LogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
} else {
next()
}
}
} else {
// 没有token
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
NProgress.done()
}
}
})
获取与添加**路由**
//生成路由 store.dispatch('GenerateRoutes').then(accessRoutes => { // 根据roles权限生成可访问的路由表 //添加路由 router.addRoutes(accessRoutes) // 动态添加可访问路由表 next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 })
GenerateRoutes
请求得到的sdata与rdata
//store/module/permisssion.js
// 生成路由
GenerateRoutes({ commit }) {
return new Promise(resolve => {
// 向后端请求路由数据
getRouters().then(res => {
const sdata = JSON.parse(JSON.stringify(res.data))
const rdata = JSON.parse(JSON.stringify(res.data))
//console.log(sdata,rdata); //array
const sidebarRoutes = filterAsyncRouter(sdata)
const rewriteRoutes = filterAsyncRouter(rdata, false, true)
// 动态路由,基于用户权限动态去加载
const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
router.addRoutes(asyncRoutes);
commit('SET_ROUTES', rewriteRoutes)
commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
commit('SET_DEFAULT_ROUTES', sidebarRoutes)
commit('SET_TOPBAR_ROUTES', sidebarRoutes)
resolve(rewriteRoutes)
})
})
}
// 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
return asyncRouterMap.filter(route => {
//type标识sidebarRoutes(false)与rewriteRoutes(true)
if (type && route.children) {
route.children = filterChildren(route.children)
}
if (route.component) {
// Layout ParentView 组件特殊处理
if (route.component === 'Layout') {
route.component = Layout
} else if (route.component === 'ParentView') {
route.component = ParentView
} else if (route.component === 'InnerLink') {
route.component = InnerLink
} else {
//指定目录下找对应组件
route.component = loadView(route.component)
}
}
if (route.children != null && route.children && route.children.length) {
//递归
route.children = filterAsyncRouter(route.children, route, type)
} else {
delete route['children']
delete route['redirect']
}
return true
})
}
function filterChildren(childrenMap, lastRouter = false) {
var children = []
childrenMap.forEach((el, index) => {
if (el.children && el.children.length) {
if (el.component === 'ParentView' && !lastRouter) {
el.children.forEach(c => {
c.path = el.path + '/' + c.path
if (c.children && c.children.length) {
children = children.concat(filterChildren(c.children, c))
return
}
children.push(c)
})
return
}
}
if (lastRouter) {
el.path = lastRouter.path + '/' + el.path
}
children = children.concat(el)
})
return children
}
export const loadView = (view) => {
if (process.env.NODE_ENV === 'development') {
return (resolve) => require([`@/views/${view}`], resolve)
} else {
// 使用 import 实现生产环境的路由懒加载
return () => import(`@/views/${view}`)
}
}
//权限判断
//store/modules/permission.js
// 动态路由遍历,验证是否具备权限
export function filterDynamicRoutes(routes) {
const res = []
routes.forEach(route => {
if (route.permissions) {
if (auth.hasPermiOr(route.permissions)) {
res.push(route)
}
} else if (route.roles) {
if (auth.hasRoleOr(route.roles)) {
res.push(route)
}
}
})
return res
}
//plugins/auth.js
import store from '@/store'
function authPermission(permission) {
const all_permission = "*:*:*";
const permissions = store.getters && store.getters.permissions
if (permission && permission.length > 0) {
return permissions.some(v => {
return all_permission === v || v === permission
})
} else {
return false
}
}
function authRole(role) {
const super_admin = "admin";
const roles = store.getters && store.getters.roles
if (role && role.length > 0) {
return roles.some(v => {
return super_admin === v || v === role
})
} else {
return false
}
}
export default {
// 验证用户是否具备某权限
hasPermi(permission) {
return authPermission(permission);
},
// 验证用户是否含有指定权限,只需包含其中一个
hasPermiOr(permissions) {
return permissions.some(item => {
return authPermission(item)
})
},
// 验证用户是否含有指定权限,必须全部拥有
hasPermiAnd(permissions) {
return permissions.every(item => {
return authPermission(item)
})
},
// 验证用户是否具备某角色
hasRole(role) {
return authRole(role);
},
// 验证用户是否含有指定角色,只需包含其中一个
hasRoleOr(roles) {
return roles.some(item => {
return authRole(item)
})
},
// 验证用户是否含有指定角色,必须全部拥有
hasRoleAnd(roles) {
return roles.every(item => {
return authRole(item)
})
}
}
addRoute
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
第一行:router.addRoutes(accessRoutes)
这行代码的作用是动态地向Vue Router的路由表中添加路由。addRoutes
方法是Vue Router早期版本(Vue Router 2.x)中用于动态添加路由的方法。然而,需要注意的是,在Vue Router 3.x及更高版本中,官方推荐使用addRoutes
的替代方案,即通过router.matcher.addRoutes
或者完全重新创建router实例来动态添加路由(因为addRoutes
方法在Vue Router 4.x中被移除)。但这里我们仍然以addRoutes
为例进行说明。
router
:这是Vue Router的实例,它包含了应用的所有路由配置。addRoutes
:这是Vue Router实例上的一个方法,用于向路由表中添加新的路由规则。accessRoutes
:这是一个数组,包含了要添加到路由表中的路由对象。每个路由对象都遵循Vue Router的路由配置规范,包括path
、component
、children
等属性。
第二行:next({ ...to, replace: true })
这行代码通常出现在Vue Router的导航守卫(Navigation Guards)中,用于控制路由的跳转。next
函数是导航守卫的回调函数,它决定了路由跳转的行为。
next
:这是一个必须调用的函数,用于解决守卫中的钩子。调用它时,可以传入一个位置对象或者一个错误。如果不调用,则整个路由导航都将被“挂起”。{ ...to, replace: true }
:这里使用了对象展开语法(...
)来复制to
对象的所有属性到一个新对象中,并添加或修改replace
属性为true
。to
对象通常包含了即将要跳转到的路由信息,如path
、query
等。replace: true
:这个选项的作用是,当使用next
函数进行路由跳转时,不是将新路由添加到历史记录堆栈中,而是替换掉当前的路由。这通常用于避免在路由跳转后留下无法退回到当前路由的“死胡同”。
结合使用这两行代码的场景
在实际应用中,这两行代码经常一起使用在基于用户权限的路由控制中。比如,当用户登录后,后端返回了用户的权限信息,前端根据这些权限信息动态生成可访问的路由表(accessRoutes
),并通过router.addRoutes(accessRoutes)
将这些路由添加到路由表中。然后,在导航守卫中,使用next({ ...to, replace: true })
来确保用户被重定向到目标路由,并且由于replace: true
,这个跳转不会在历史记录中留下当前路由的入口,从而避免了用户通过浏览器的前进/后退按钮访问到未授权的路由。
使用路由守卫保证addRoute执行完成:你可以设置一个全局前置守卫(beforeEach)或后置守卫(afterEach),并在其中检查路由是否已经存在于路由表中。然而,这种方法比较复杂且可能不够直观,因为它依赖于路由守卫的多次调用和状态检查。