记得当时在学校的时候,觉得这个实现起来真的超级困难,想想就头大,毫无头绪,即便那时候去查资料看了很多大佬写的文章,看的时候感觉恍然大悟,直拍大腿,但是当我想要动手自己去做的时候,又不知道从哪开始切入,于是一直没有动手去做,直到最近在实习的时候,给了我这个任务,当我带着恐惧去自己动手实现这个功能的时候,当静下心来,发现是多么的有趣和清晰,那我们一起来看一下吧
一些专业术语网上都有,我在这就不多叙述,主打一个思路,把思路理明白了,一切就容易了
所谓的权限就是给用户赋予特定的角色
(一个用户可以有很多角色,就像我们在现实中的身份<角色>可以有很多个),给角色赋予特定的权限
(一个角色可以有很多权限,就像我们既可以吃喝玩乐又可以好好学习),这样用户就有了角色,角色有对应的权限,至此,用户就有了"权限"
这里的权限只举权限菜单
的例子,主要理解思路
首先我们需要准备几张基础的数据表,你得有用户表,角色表,菜单表
注意:不用太纠结表单的字段,主要是思路!思路!
例如:
sys_user表
字段名称 | 字段类型 |
---|---|
id | varchar |
user_name | varchar |
pass_word | varchar |
status | varchar |
create_time | datetime |
create_by | varchar |
edite_time | datetime |
edite_by | varchar |
delete_flag | char |
这里的delete_flag
是用作删除标识,默认为0
,这也是我进入实习之后才了解到的,不会真的去删除数据
然后就是,然后每个表都要有的字段就不再展示了:create_time
,create_by
,edite_time
,edite_by
,delete_flag
sys_role表(角色表)
字段名称 | 字段类型 |
---|---|
id | varchar |
role_code | varchar |
role_name | varchar |
role_type | varchar |
role_status | varchar |
remark | varchar |
sys_menu(菜单表)
字段名称 | 字段类型 |
---|---|
id | varchar |
menu_code | varchar |
menu_name | varchar |
menu_path | varchar |
busi_status | varchar |
…… | …… |
这是基础的三张表,当然你还需要有菜单的子父级关系,这不是本篇文章的重点
好了我们需要了解怎么将用户、角色、权限菜单关联起来?
我们还需要两张表,一张用户角色表
,一张角色权限表
用户角色表用来关联用户和角色
sys_right_user(用户角色表)
字段名称 | 字段类型 |
---|---|
id | varchar |
role_id | varchar |
role_code | varchar |
user_id | varchar |
busi_status | varchar |
remark | varchar |
怎么使用呢,首先我们需要创建角色信息
自己定义几个角色,我这随便起的名字
企业临时角色是因为我想实现新的企业注册的时候,给一个临时的角色,临时角色只可以查看自己的企业信息,然后通过提交信息,被审核之后才赋予对应的企业角色,这个不是必要的,根据自己的需求来
角色创建好了,你想先跟谁关联其实都是可以的,你可以先跟用户关联,也可以先跟权限菜单关联(这部分代码比较复杂
)
我提供一种思路
如果你要想跟用户关联的话,你可以在编辑用户的时候给它对应的权限,例如
下面我只展示部分代码(仅供参考),代码其实很多种方式
这个是编辑表单中的角色的select框
<el-form-item label="角色" prop="roleList">
<el-select v-model="userInfo.roleList" multiple filterable allow-create default-first-option placeholder="请选择角色">
<el-option v-for="item in roleOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
@ResponseBody
@PostMapping("/editedUser")
public JsonResult editedUser(@RequestBody UserVO userVO) {
JsonResult jsonResult = JsonResult.getSuccessResult();
try {
userService.editedUser(userVO);
} catch (Exception e) {
logger.error("editedUser", e);
jsonResult.setException(e);
}
return jsonResult;
}
sysRightUser对应的是sys_right_user表中的字段
sysRole对应的是sys_role表中的字段
@Transactional(rollbackFor = Exception.class)
@Override
public void editedUser(UserVO userVO) {
Date now = DateUtil.getNow();
List<SysRole> sysRoleList = userVO.getRoleList();
List<SysRightUser> sysRightUsers = new ArrayList<>();
if (sysRoleList != null && sysRoleList.size() > 0) {
for (SysRole sysRole : sysRoleList) {
SysRightUser sysRightUser = new SysRightUser();
sysRightUser.setId(IdWorker.get32UUID());
sysRightUser.setUserId(userVO.getId());
sysRightUser.setRoleId(sysRole.getId());
sysRightUser.setRoleCode(sysRole.getRoleCode());
sysRightUser.setCreateTime(DateUtil.formatDate(now));
sysRightUser.setCreateBy(Constant.CREATE_BY);
sysRightUsers.add(sysRightUser);
}
}
sysRightUserService.saveBatch(sysRightUsers);
}
当然这个方法没有做全,还需要根据前端传递的角色列表判断和之前的角色列表的不同,现在有的 以前没有的 需要新增,现在有的 以前也有的 不需要操作,现在没有的 以前有的 需要做逻辑删除
那第二张表
sys_right_menu(角色权限表)
字段名称 | 字段类型 |
---|---|
id | varchar |
role_id | varchar |
role_code | varchar |
menu_id | varchar |
menu_code | varchar |
busi_status | varchar |
remark | varchar |
那现在就来到了角色和权限菜单关联了,道理是一样的,在编辑角色中,编辑角色可以访问的权限菜单就可以了,例如:
这个是编辑角色中对应的权限菜单的form
<el-form-item label="权限菜单" prop="checkedMenus">
<el-tree
:data="menuList"
:props="defaultProps"
node-key="value"
show-checkbox
:check-on-click-node="true"
:default-checked-keys="defaultCheckedKeys"
:default-expanded-keys="defaultCheckedKeys"
@check="handleCheck"
ref="menuTree"
>
</el-tree>
</el-form-item>
defaultProps: {
children: 'children',
label: 'label'
},
这个部分其实还是比较复杂的,你要在后端处理树结构,网上也有很多教程,大家可以自行搜索,本篇我们主要理清思路
sysRole对应的sys_role的字段
sysRightMenu对应的sys_right_menu中的字段
我下面的处理其实是很复杂的,大体看一下就可以
@Transactional(rollbackFor = Exception.class)
@Override
public void updateRole(RoleVO roleVO) {
// 获取当前时间
Date now = DateUtil.getNow();
// 创建SysRole对象
SysRole sysRole = new SysRole();
// 设置角色ID
sysRole.setId(roleVO.getId());
// 设置角色编码
sysRole.setRoleCode(roleVO.getRoleCode());
// 设置角色名称
sysRole.setRoleName(roleVO.getRoleName());
// 设置角色状态
sysRole.setRoleStatus(roleVO.getRoleStatus());
// 设置角色类型
sysRole.setRoleType(roleVO.getRoleType());
// 设置编辑人
sysRole.setEditBy(Constant.CREATE_BY);
// 设置编辑时间
sysRole.setEditTime(DateUtil.formatDate(now));
// 更新角色信息
this.baseMapper.updateById(sysRole);
// 获取角色对应的菜单列表
List<CommonTree> commonTreeList = roleVO.getCommonTreeList();
// 初始化要插入的菜单列表
List<SysRightMenu> sysRightMenuList = new ArrayList<>();
// 初始化要删除的菜单列表
List<CommonTree> sysRightMenusToDelete = new ArrayList<>();
if (commonTreeList != null && commonTreeList.size() > 0) {
// 获取当前角色已关联的菜单ID集合
Set<String> currentSysRightMenusIds = new HashSet<>();
List<CommonTree> currentSysRightMenus = sysRightMenuService.listByRoleId(sysRole.getId());
for (CommonTree currentSysRightMenu : currentSysRightMenus) {
currentSysRightMenusIds.add(currentSysRightMenu.getValue());
}
// 遍历传入的菜单列表
for (CommonTree tree : commonTreeList) {
// 如果菜单已存在,则跳过
if (currentSysRightMenusIds.contains(tree.getValue())) {
continue;
}
// 创建SysRightMenu对象
SysRightMenu sysRightMenu = new SysRightMenu();
// 设置菜单ID
sysRightMenu.setId(IdWorker.get32UUID());
// 设置角色ID
sysRightMenu.setRoleId(sysRole.getId());
// 设置角色编码
sysRightMenu.setRoleCode(sysRole.getRoleCode());
// 设置菜单ID
sysRightMenu.setMenuId(tree.getValue());
// 设置创建时间
sysRightMenu.setCreateTime(DateUtil.formatDate(now));
// 设置创建人
sysRightMenu.setCreateBy(Constant.CREATE_BY);
// 将菜单添加到要插入的列表
sysRightMenuList.add(sysRightMenu);
}
// 遍历当前角色已关联的菜单列表
for (CommonTree currentSysRightMenu : currentSysRightMenus) {
// 如果菜单不在传入的菜单列表中,则添加到要删除的列表
if (!commonTreeList.contains(currentSysRightMenu)) {
sysRightMenusToDelete.add(currentSysRightMenu);
}
}
}
// 批量插入菜单
sysRightMenuService.saveBatch(sysRightMenuList);
// 批量删除菜单
sysRightMenuService.deleteRoleMenu(sysRightMenusToDelete, roleVO.getId());
}
那么现在其实用户有了角色
,角色有了对应的权限菜单
,那怎么根据用户渲染对应的菜单呢?
有很多方式,我这里说一下我的思路
假如现在用户有角色(新注册用户,注册逻辑里一般会给默认角色),登录的时候会经过filter
和LoginServlet
在loginServlet中进行登录验证(判断账号密码和验证码等逻辑),如果验证成功,我这里是用的redis存储的,就把用户的用户信息,角色,权限菜单存到redis中,然后登录成功之后会请求其他页面,这个时候会被filter拦截,判断是否登录进而判断权限菜单,最后将存到redis中的权限菜单列表返回给前端请求渲染列表的地方就完成了整个流程
下面是部分代码,可能写的不是很好,因为是手搓出来的,之前刚理清一点点思路写的哈哈,仅供参考仅供参考!
public class LoginServlet extends HttpServlet {
private SysUserServiceImpl sysUserService;
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取请求体中的数据
StringBuilder jsonData = new StringBuilder();
String line;
try (BufferedReader reader = request.getReader()) {
while ((line = reader.readLine()) != null) {
jsonData.append(line);
}
}
try {
// 将请求体中的数据转换为JSON对象
JSONObject jsonObject = new JSONObject(jsonData.toString());
// 从JSON对象中获取用户名
String username = jsonObject.getString("userName");
// 从JSON对象中获取密码
String password = jsonObject.getString("passWord");
// 从JSON对象中获取验证码
String captcha = jsonObject.getString("captcha");
if(captcha != null && captcha.length() > 0){
// 从session中获取验证码
String sessionCaptcha = (String) request.getSession().getAttribute("captcha");
// 比较session中的验证码和请求中的验证码是否一致
if(!sessionCaptcha.equalsIgnoreCase(captcha)){
// 如果验证码错误,则返回状态码401,表示未授权
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json; charset=utf-8");
response.getWriter().write("{\"success\": false, \"message\": \"验证码错误!\"}");
return;
}
}
// 创建UserParam对象,并设置用户名和密码
UserParam userParam = new UserParam();
userParam.setUserName(username);
userParam.setPassWord(password);
// 进行登录验证
Boolean login = sysUserService.login(userParam);
if (login){
// 进行登录成功后的处理
sysUserService.loginSuccess(request,response,userParam);
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader("Content-Type", "application/json");
response.getWriter().write("{\"success\": true, \"message\": \"登录成功!\"}");
}else {
// 登录失败,返回状态码401,表示未授权
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json; charset=utf-8");
response.getWriter().write("{\"success\": false, \"message\": \"用户名或密码错误!\"}");
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
@Override
public void init() throws ServletException {
// 获取WebApplicationContext对象
WebApplicationContext springContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
// 从Spring容器中获取SysUserServiceImpl类型的Bean对象,并赋值给sysUserService变量
sysUserService = springContext.getBean(SysUserServiceImpl.class);
}
}
这个是上面出现的登录成功之后处理逻辑loginSuccess方法,当然下面的方法如果角色和角色之间有重复的菜单也会被添加进去,我是在前端取的时候做的处理,其实我写的还是比较复杂的,肯定有更好的方法,大家仅供参考
@Transactional(rollbackFor = Exception.class)
@Override
public Map<String, Object> loginSuccess(HttpServletRequest request, HttpServletResponse response,UserParam userParam) {
// 创建一个存储数据的Map
Map<String, Object> data = new HashMap<>();
// 调用sysUserMapper的userLogin方法,根据用户参数查询用户信息,并将结果赋值给userVO
UserVO userVO = sysUserMapper.userLogin(userParam);
// 调用sysRightUserMapper的queryRightUserByUserId方法,根据用户ID查询用户角色列表,并将结果赋值给roleVOS
ArrayList<RoleVO> roleVOS = sysRightUserMapper.queryRightUserByUserId(userVO.getId());
// 创建一个空的菜单列表
List<CommonTree> menuList = new ArrayList<>();
// 如果角色列表不为空且大小大于0
if(roleVOS != null && roleVOS.size() > 0){
// 遍历角色列表
for (RoleVO roleVO : roleVOS) {
// 调用sysRightMenuMapper的listByRoleId方法,根据角色ID查询菜单列表,并将结果赋值给menus
List<CommonTree> menus = sysRightMenuMapper.listByRoleId(roleVO.getRoleId());
// 遍历菜单列表
for (CommonTree menu : menus) {
// 如果菜单列表中没有包含当前菜单
if (!menuList.contains(menu)) {
// 将当前菜单添加到菜单列表中
menuList.add(menu);
}
}
}
}
// 生成一个随机的ticketId
String ticketId = UUID.randomUUID().toString();
// 拼接ticket的键
String ticket = CacheUtil.PREFIX_TICKET_USER + ticketId;
// 将ticketId存入data中
data.put("ticketId", ticketId);
// 将用户名存入data中
data.put("username", userVO.getUserName());
// 将用户ID存入data中
data.put("userId", userVO.getId());
// 将角色列表存入data中
data.put("roleList", roleVOS);
// 将菜单列表存入data中
data.put("menuList", menuList);
// 将data存入redis中,并设置过期时间为1200秒
redisUtil.set(ticket,data,1200);
// 将ticket存入session中
request.getSession().setAttribute("ticket", ticket);
// 将用户ID存入session中
request.getSession().setAttribute("userID", userVO.getId());
// 返回data
return data;
}
这样一步用户的信息就存进去了,登录之后肯定紧接着会请求到首页,紧接着会被filter拦截
@Component
public class LoginFilter implements Filter {
@Autowired
private RedisUtil redisUtil;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
String requestURL = request.getRequestURI();
// 过滤不需要处理的请求URL
//根据需要
if(requestURL.contains("/login") || requestURL.contains("/register") || requestURL.contains("/captcha")) {
filterChain.doFilter(request, response);
return;
}
// 检查用户是否已登录
// 用户未登录,跳转到登录页面
try {
if (!isUserLoggedIn(request)) {
response.sendRedirect("/index.html");
return;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
// 用户已登录,放行
filterChain.doFilter(request, response);
}
private boolean isUserLoggedIn(HttpServletRequest request) throws Exception {
// 从Session中获取Ticket
// 刚刚在loginSuccess中存的
String ticket = (String) request.getSession().getAttribute("ticket");
if (ticket != null && !ticket.isEmpty()) {
// 判断Redis中是否存在Ticket
boolean hasKey = redisUtil.hasKey(ticket);
if (hasKey) {
// 从Redis中获取用户信息
Map<String,Object> userMap = (Map<String, Object>) redisUtil.get(ticket);
// 获取用户角色列表
ArrayList<RoleVO> roleList = (ArrayList<RoleVO>) userMap.get("roleList");
// 获取用户菜单列表
ArrayList<CommonTree> menuList = (ArrayList<CommonTree>) userMap.get("menuList");
// 构建树形菜单列表
List<CommonTree> commonTrees = CommonTreeUtil.buildListTree(menuList, "-1");
// 将角色列表和菜单列表设置到request属性中
request.setAttribute("roleList", roleList);
request.setAttribute("commonTrees", commonTrees);
}
return hasKey;
}
return false;
}
}
这样权限列表已经存到request中了,我们只需要在前端请求的时候,返回给他就行了
我前段初始化会调用这个接口 /getCallingTreeByUser
@PostMapping("/getCallingTreeByUser")
public JsonResult<CommonTree> getCallingTreeByUser(HttpServletRequest request) {
//获取到requst中存储的权限菜单列表并进行处理成树结构
List<CommonTree> menuList = (List<CommonTree>) request.getAttribute("commonTrees");
JsonResult<CommonTree> jsonResult = JsonResult.getSuccessResult();
jsonResult.setData(menuList);
return jsonResult;
}
好了,到这基本上就结束了,有没有明白一些呢,我也是最近刚刚理清一点点,所有肯定有很多不是很规范,不严谨的地方,也有很多我还没有涉及到的方面,还请大佬们多多指导!