一、权限管理后端
1.1、权限管理介绍
每个系统的权限功能都不尽相同,各有其自身的业务特点,对权限管理的设计也都各有特色。不过不管是怎样的权限设计,大致可归为三种:页面权限(菜单级)、操作权限(按钮级)、数据权限。当前系统只总结:
菜单权限与按钮权限的控制
。
1.1.1、菜单权限
菜单权限就是对页面的控制,就是有这个权限的用户才能访问这个页面,没这个权限的用户就无法访问,它是以整个页面为维度,对权限的控制并没有那么细,所以是一种粗颗粒权限。
1.1.2、按钮权限
按钮权限就是将页面的操作视为资源,比如删除操作,有些人可以操作有些人不能操作。对于后端来说,操作就是一个接口。于前端来说,操作往往是一个按钮,是一种细颗粒权限。
1.1.3、权限管理设计思路
前面总结了
用户管理
、角色管理
及菜单管理
,我把菜单权限分配给角色,把角色分配给用户,那么用户就拥有了角色的所有权限(权限包含:菜单权限与按钮权限
)。
接下来需要实现这两个接口:
- 1、用户登录
- 2、登录成功根据
token
获取用户相关信息(菜单权限及按钮权限数据等)
用户登录我们需要用到JWT
,接下来先学哈JWT
。
1.2、JWT
1.2.1、JWT介绍
JWT是JSON Web Token的缩写,即JSON Web令牌,是一种自包含令牌。 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
JWT最重要的作用就是对 token信息的防伪作用。
1.2.2、JWT令牌的组成
一个JWT由三个部分组成:JWT头、有效载荷、签名哈希
最后由这三者组合进行base64url编码得到JWT
- 典型的,一个JWT看起来如下图:该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。
JWT头
JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示。
{
"alg": "HS256",
"typ": "JWT"
}
- 在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);
- typ属性表示令牌的类型,JWT令牌统一写为JWT。
- 最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。
有效载荷
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择。
iss: jwt签发者
sub: 主题
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
除以上默认字段外,我们还可以自定义私有字段,如下例:
{
"name": "Helen",
"role": "editor",
"avatar": "helen.jpg"
}
- 请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。
- JSON对象也使用Base64 URL算法转换为字符串保存。
签名哈希
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret) ==> 签名hash
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。
Base64URL算法
如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。
作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+“,”/“和”=“,由于在URL中有特殊含义,因此Base64URL中对他们做了替换:”=“去掉,”+“用”-“替换,”/“用”_"替换,这就是Base64URL算法。
1.2.3、项目集成JWT
操作模块:common-util
1.2.3.1、 引入依赖(前面已经引入了)
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
1.2.3.2、添加JWT 帮助类
直接复制即可
package com.gansu.common.utils;
import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;
import java.util.Date;
public class JwtHelperUtils {
//token过期时间
private static long tokenExpiration = 365 * 24 * 60 * 60 * 1000;
//加密密钥
private static String tokenSignKey = "123456";
//根据用户id和用户名称生成token字符串
public static String createToken(String userId, String username) {
String token = Jwts.builder()
.setSubject("AUTH-USER")
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.claim("userId", userId)
.claim("username", username)
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
//从token字符串获取用户id
public static Long getUserId(String token) {
try {
if (StringUtils.isEmpty(token)) return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
Integer userId = (Integer) claims.get("userId");
return userId.longValue();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//从token字符串获取用户名
public static String getUsername(String token) {
try {
if (StringUtils.isEmpty(token)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return (String) claims.get("username");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
1.2.3.3、测试
import com.gansu.common.utils.JwtHelperUtils;
public class JwtHelperUtilsTest {
public static void main(String[] args) {
String token = JwtHelperUtils.createToken("1", "gansu");
System.out.println(token);
System.out.println(JwtHelperUtils.getUserId(token));
System.out.println(JwtHelperUtils.getUsername(token));
}
}
运行如下所示:
1.3、用户登录
1.3.1、修改添加用户密码加密
① 导入 MD5
② 修改SysUserController
类添加用户方法
// 2.新增用户接口
@ApiOperation(value = "新增用户")
@PostMapping("addUser")
public Result addUser(@RequestBody SysUser sysUser){
//把输入的密码进行MD5加密
String md5Password = MD5.encrypt(sysUser.getPassword());
sysUser.setPassword(md5Password);
boolean isIsuccess = sysUserService.save(sysUser);
if(isIsuccess){
return Result.ok();
}else{
return Result.fail();
}
}
1.3.2、 修改IndexController
类登录方法
@Autowired
private SysUserService sysUserService;
//1.login登录方式
@PostMapping("login") //根据浏览器请求方法决定
public Result login(@RequestBody LoginVo loginVo){
//获取用户登录名
SysUser sysUser = sysUserService.getUserInfoUsernameById(loginVo.getUsername());
//判断用户名是否为null 不存在
if(null == sysUser){
throw new GansuException(20001,"用户不存在");
}
//判断密码与数据库中的是否一致,不一致也抛出异常
if(MD5.encrypt(loginVo.getPassword()).equals(sysUser.getPassword())){
throw new GansuException(20002,"密码不正确,请输入正确的密码");
}
//判断用户名状态是否被禁用,禁用则抛出异常
if(sysUser.getStatus() == 0){
throw new GansuException(20003,"该用户已经被禁用");
}
String token = JwtHelperUtils.createToken(sysUser.getId(), sysUser.getUsername());
Map<String,Object> map = new HashMap<>();
map.put("token",token);
return Result.ok(map);
}
1.3.3、 添加service接口及实现
SysUserService
//获取用户名
SysUser getUserInfoUsernameById(String username);
接口实现:SysUserServiceImpl
@Override
public SysUser getUserInfoUsernameById(String username) {
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username",username);
return baseMapper.selectOne(queryWrapper);
}
1.3.4、 再次修改IndexController
类登录方法
//2.info 登录方式
@ApiOperation(value = "info 登录方式")
@GetMapping("info")
public Result info(HttpServletRequest request){
//获取请求头
String token = request.getHeader("token");
//获取请求头中的用户名
String username = JwtHelperUtils.getUsername(token);
//获取用户名信息
Map<String,Object> usernameMessage = sysUserService.getUsernameInfo(username);
return Result.ok(usernameMessage);
}
1.3.5、 再次添加service接口及实现
①
SysUserService
//获取用户信息
Map<String, Object> getUsernameInfo(String username);
接口实现:
SysUserServiceImpl
@Override
public Map<String, Object> getUsernameInfo(String username) {
//根据username 查询用户的基本信息
SysUser sysUser = this.getUserInfoUsernameById(username);
//根据userId获取用户菜单权限
List<RouterVo> menuList = sysMenuService.getUserMenuInfo(sysUser.getId());
//根据userId获取用户按钮权限
List<String> buttonList = sysMenuService.getUserButtonInfo(sysUser.getId());
Map<String,Object> map = new HashMap<>();
map.put("roles","[admin]");
map.put("introduction","I am a super administrator");
map.put("avatar","https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif");
map.put("name",username);
//获取用户菜单权限
map.put("routers",menuList);
//获取用户按钮权限
map.put("buttons",buttonList);
return map;
}
1.3.6、 添加 Router 帮助类
导入即可:
RouterHelperUtils
package com.gansu.system.utils;
import org.springframework.util.CollectionUtils;
import com.gansu.model.system.SysMenu;
import com.gansu.model.vo.MetaVo;
import com.gansu.model.vo.RouterVo;
import org.springframework.util.StringUtils;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/**
*根据菜单数据构建路由的工具类
*/
public class RouterHelperUtils {
/**
* 根据菜单构建路由
* @param menus
* @return
*/
public static List<RouterVo> buildRouters(List<SysMenu> menus) {
List<RouterVo> routers = new LinkedList<RouterVo>();
for (SysMenu menu : menus) {
RouterVo router = new RouterVo();
router.setHidden(false);
router.setAlwaysShow(false);
router.setPath(getRouterPath(menu));
router.setComponent(menu.getComponent());
router.setMeta(new MetaVo(menu.getName(), menu.getIcon()));
List<SysMenu> children = menu.getChildren();
//如果当前是菜单,需将按钮对应的路由加载出来,如:“角色授权”按钮对应的路由在“系统管理”下面
if(menu.getType().intValue() == 1) {
List<SysMenu> hiddenMenuList = children.stream().filter(item -> !StringUtils.isEmpty(item.getComponent())).collect(Collectors.toList());
for (SysMenu hiddenMenu : hiddenMenuList) {
RouterVo hiddenRouter = new RouterVo();
hiddenRouter.setHidden(true);
hiddenRouter.setAlwaysShow(false);
hiddenRouter.setPath(getRouterPath(hiddenMenu));
hiddenRouter.setComponent(hiddenMenu.getComponent());
hiddenRouter.setMeta(new MetaVo(hiddenMenu.getName(), hiddenMenu.getIcon()));
routers.add(hiddenRouter);
}
} else {
if (!CollectionUtils.isEmpty(children)) {
if(children.size() > 0) {
router.setAlwaysShow(true);
}
router.setChildren(buildRouters(children));
}
}
routers.add(router);
}
return routers;
}
/**
* 获取路由地址
*
* @param menu 菜单信息
* @return 路由地址
*/
public static String getRouterPath(SysMenu menu) {
String routerPath = "/" + menu.getPath();
if(menu.getParentId().intValue() != 0) {
routerPath = menu.getPath();
}
return routerPath;
}
}
1.4 获取用户菜单权限
说明:获取菜单权限数据,我们要将菜单数据构建成路由数据结构
1.4.1、 再次添加service接口及实现
接口
SysMenuService
//根据userId获取用户菜单权限
List<RouterVo> getUserMenuInfo(String id);
//根据userId获取用户按钮权限
List<String> getUserButtonInfo(String id);
接口实现
SysMenuServiceImpl
//根据userId获取用户菜单权限
@Override
public List<RouterVo> getUserMenuInfo(String userId) {
//admin是超级管理员,操作所有的内容
List<SysMenu> sysMenuList = null;
//判断userid值是1代表超级管理员,查询所有权限数据
if("1".equals(userId)){
//判断状态是否为1,即启用不是禁用状态
//排序为1则为超级管理员
QueryWrapper<SysMenu> wrapper = new QueryWrapper<>();
wrapper.eq("status",1);
wrapper.orderByAsc("sort_value");
sysMenuList = baseMapper.selectList(wrapper);
}else{
//如果userid不是1,其他类型用户,查询这个用户权限
if(!("1".equals(userId))){
baseMapper.findMenuListUserId(userId);
}
}
//转换成我们需要的数据类型(构建树形结构)
List<SysMenu> sysMenuList1 = MenuHelpUtils.bulidTree(sysMenuList);
//导入帮助类 RouterHelperUtils
List<RouterVo> routerVos = RouterHelperUtils.buildRouters(sysMenuList);
return routerVos;
}
1.4.2、 添加mapper接口
SysMenuMapper
类
package com.gansu.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gansu.model.system.SysMenu;
import org.springframework.stereotype.Repository;
@Repository
public interface SysMenuMapper extends BaseMapper<SysMenu> {
//根据userId获取用户菜单权限
void findMenuListUserId(String userId);
}
1.4.3、 添加xml方法
分析(需要查询的表如下):
新建SysMenuMapper.xml
文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gansu.system.mapper.SysMenuMapper">
<resultMap id="sysUserMenuRoleMap" type="com.gansu.model.system.SysMenu" autoMapping="true">
</resultMap>
<select id="findMenuListUserId" resultMap="sysUserMenuRoleMap">
SELECT
DISTINCT m.id,m.parent_id,m.name,m.type,m.path,m.component,
m.perms,m.icon,m.sort_value,m.status,m.create_time,m.update_time,m.is_deleted
FROM sys_menu m
INNER JOIN sys_role_menu rm ON rm.menu_id = m.id
INNER JOIN sys_user_role ur ON ur.role_id = rm.role_id
WHERE
ur.user_id = #{userId}
AND m.status = 1
AND rm.is_deleted = 0
AND ur.is_deleted = 0
AND m.is_deleted = 0
</select>
</mapper>
1.4.4、 进行接口测试
先生成一个token
再进行测试
http://localhost:8800/doc.html
admin
test
1.5 获取用户按钮权限
说明:只需要获取按钮标识即可
其他的前面开发了,就只剩下下面接口实现:
1.5.1、接口实现 SysMenuServiceImpl
//根据userId获取用户按钮权限
@Override
public List<String> getUserButtonInfo(String userId) {
List<SysMenu> sysMenuList = null;
if("1".equals(userId)){
sysMenuList = baseMapper.selectList(new QueryWrapper<SysMenu>().eq("status", 1));
}else {
baseMapper.findMenuListUserId(userId);
}
List<String> permissionList = new ArrayList<>();
//遍历sysMenuList
for (SysMenu sysMenu :sysMenuList) {
//type = 2 对应数据库为 2 就是按钮
if(sysMenu.getType() == 2){
String permis = sysMenu.getPerms();
permissionList.add(permis);
}
}
return permissionList;
}
1.5.2、进行最终接口测试
先生成一个token
再进行测试
http://localhost:8800/doc.html
admin
二、权限管理前端
2.1、前端对接实现
可直接引入前端项目完整代码,如下:
2.1.1、修改request.js文件
2.1.2、store/modules/user.js
新增菜单及按钮处理
const getDefaultState = () => {
return {
token: getToken(),
name: '',
avatar: '',
buttons: [], // 新增
menus: '' //新增
}
}
const mutations = {
RESET_STATE: (state) => {
Object.assign(state, getDefaultState())
},
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
// 新增
SET_BUTTONS: (state, buttons) => {
state.buttons = buttons
},
// 新增
SET_MENUS: (state, menus) => {
state.menus = menus
}
}
// get user info
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(response => {
const { data } = response
if (!data) {
return reject('Verification failed, please Login again.')
}
const { name, avatar } = data
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
commit("SET_BUTTONS", data.buttons)
commit("SET_MENUS", data.routers)
resolve(data)
}).catch(error => {
reject(error)
})
})
}
2.1.3、store/getters.js
新增菜单及按钮处理
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
//新增
buttons: state => state.user.buttons,
menus: state => state.user.menus
}
export default getters
2.1.4、src/router
先在
router
这个目录下新建两个js
文件,开发环境和生产环境导入组件的方式略有不同
_import_production.js
_import_development.js
// 生产环境导入组件
module.exports = file => () => import('@/views/' + file + '.vue')
// 开发环境导入组件
module.exports = file => require('@/views/' + file + '.vue').default // vue-loader at least v13.0.0+
2.1.5、src/permission.js
整体替换该文件
import router from './router'
import store from './store'
import { getToken } from '@/utils/auth'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // 水平进度条提示: 在跳转路由时使用
import 'nprogress/nprogress.css' // 水平进度条样式
import getPageTitle from '@/utils/get-page-title' // 获取应用头部标题的函数
import Layout from '@/layout'
import ParentView from '@/components/ParentView'
const _import = require('./router/_import_'+process.env.NODE_ENV) // 获取组件的方法
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login'] // no redirect whitelist
router.beforeEach(async(to, from, next) => {
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
// determine whether the user has logged in
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done()
} else {
const hasGetUserInfo = store.getters.name
if (hasGetUserInfo) {
next()
} else {
try {
// get user info
await store.dispatch('user/getInfo')// 请求获取用户信息
if (store.getters.menus.length < 1) {
global.antRouter = []
next()
}
const menus = filterAsyncRouter(store.getters.menus)// 1.过滤路由
console.log(menus)
router.addRoutes(menus) // 2.动态添加路由
let lastRou = [{ path: '*', redirect: '/404', hidden: true }]
router.addRoutes(lastRou)
global.antRouter = menus // 3.将路由数据传递给全局变量,做侧边栏菜单渲染工作
next({
...to,
replace: true
})
//next()
} catch (error) {
// remove token and go to login page to re-login
console.log(error)
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else { /* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => { // finish progress bar
NProgress.done()
}) // // 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap) {
const accessedRouters = asyncRouterMap.filter(route => {
if (route.component) {
if (route.component === 'Layout') {
route.component = Layout
} else if (route.component === 'ParentView') {
route.component = ParentView
} else {
try {
route.component = _import(route.component)// 导入组件
} catch (error) {
debugger
console.log(error)
route.component = _import('dashboard/index')// 导入组件
}
}
}
if (route.children && route.children.length > 0) {
route.children = filterAsyncRouter(route.children)
} else {
delete route.children
}
return true
})
return accessedRouters
}
2.1.6、src/router
删除
index.js
中自定义的路由,以下注释内容即为要删除的内容
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
// {
// path: '/404',
// component: () => import('@/views/404'),
// hidden: true
// },
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: 'Dashboard', icon: 'dashboard' }
}]
}
//添加我们的路由
// {
// path: '/system',
// component: Layout,
// meta: {
// title: '系统管理',
// icon: 'el-icon-s-tools'
// },
// alwaysShow: true,
// children: [
// {
// path: 'sysRole',
// component: () => import('@/views/system/sysRole/list'),
// meta: {
// title: '角色管理',
// icon: 'el-icon-s-help'
// },
// },
// {
// path: 'sysUser',
// component: () => import('@/views/system/sysUser/list'),
// meta: {
// title: '用户管理',
// icon: 'el-icon-s-help'
// },
// },
// {
// name: 'sysMenu',
// path: 'sysMenu',
// component: () => import('@/views/system/sysMenu/list'),
// meta: {
// title: '菜单管理',
// icon: 'el-icon-s-unfold'
// },
// },
// {
// path: 'assignAuth',
// component: () => import('@/views/system/sysRole/assignAuth'),
// meta: {
// activeMenu: '/system/sysRole',
// title: '角色授权'
// },
// hidden: true,
// }
// ]
// },
// 404 page must be placed at the end !!!
// { path: '*', redirect: '/404', hidden: true }
]
2.1.7、src/components
在
scr/components
目录下新建ParentView
文件夹,添加index.vue
<template >
<router-view />
</template>
2.1.8、layout/components/SideBar/index.vue
computed: {
...mapGetters([
'sidebar'
]),
routes() {
//return this.$router.options.routes
return this.$router.options.routes.concat(global.antRouter)
},
2.1.9、utils/btn-permission.js
在uitls
目录添加btn-permission.js
文件
import store from '@/store'
/**
* 判断当前用户是否有此按钮权限
* 按钮权限字符串 permission
*/
export default function hasBtnPermission(permission) {
// 得到当前用户的所有按钮权限
const myBtns = store.getters.buttons
// 如果指定的功能权限在myBtns中, 返回true ==> 这个按钮就会显示, 否则隐藏
return myBtns.indexOf(permission) !== -1
}
2.1.10、main.js
//新增
import hasBtnPermission from '@/utils/btn-permission'
Vue.prototype.$hasBP = hasBtnPermission
2.1.11、按钮权限控制
$hasBP('bnt.sysRole.add')
控制按钮是否显示
如:角色管理添加按钮,我们没让按钮隐藏,而是让按钮不可操作
<el-button type="success" icon="el-icon-plus" size="mini" @click="add" :disabled="$hasBP('bnt.sysRole.add') === false">添 加</el-button>
2.2、整体测试
暂且修改下登录,其余下节总结再放开:
启动前后端可按照如下方案进行测试:
接口测是通的,所以前端哪里出现了错误 ? ? ?😢😢😢😢
2.3、总结
当前我们已经实现前端菜单及按钮的权限控制,服务器端还没加任何控制,那么服务器端怎么控制呢?其实很简单,就是要在页面按钮对应的
controller
方法上面加对应的权限控制,即在进入controller
方法前判断当前用户是否有访问权限。
- 怎么实现呢?如果我们自己实现,那么肯定想到的就是
Fillter
加Aop
就可以实现,有现成的开源技术框架吗?答案是肯定的,如:Spring Security
、Shiro
等一系列开源框架可供选择。那么进入下节总结看啦!!!