目錄
SpringSecurity權限管理系統實戰—一、專案簡介和開發環境準備
SpringSecurity權限管理系統實戰—二、日志、介面檔案等實作
SpringSecurity權限管理系統實戰—三、主要頁面及介面實作
SpringSecurity權限管理系統實戰—四、整合SpringSecurity(上)
SpringSecurity權限管理系統實戰—五、整合SpringSecurity(下)
SpringSecurity權限管理系統實戰—六、SpringSecurity整合jwt
SpringSecurity權限管理系統實戰—七、處理一些問題
SpringSecurity權限管理系統實戰—八、AOP 記錄用戶日志、例外日志
SpringSecurity權限管理系統實戰—九、資料權限的配置
前言
后端五分鐘,前端半小時,,
每次寫js都頭疼,
自己寫前端是不可能的,這輩子不可能自己寫前端的,只能找找別人的模板才能維持的了生存這樣子,github,gitee上的模板又多,幫助檔案又詳細,我超喜歡這兩個平臺的,
(下一節進入springsecurity)
一、選單頁面
我們稍微分析一下資料表,只有選單頁面的增刪改查幾乎是沒有涉及多個表的,所以我們最先從選單頁面的邏輯開始寫,
在templates/system目錄下新建menu檔案夾,將PearAdmin自帶的power.html移動到menu下,修改一下路由,
頁面最終效果是這樣的
layui的table資料表格的用法可以在layui官網上找到示例,我這里對于前端部分就不詳細解釋了,因為前端我也不咋會,都是根據別人的代碼改了改,
我直接貼上完整的power.html完整代碼
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" th:href=https://www.cnblogs.com/codermy/p/"@{/PearAdmin/component/layui/css/layui.css}" />
<link rel="stylesheet" th:href="@{/PearAdmin/admin/css/pearCommon.css}"/>
<body class="pear-container">
<script type="text/html" id="power-toolbar">
</script>
<script type="text/html" id="power-bar">
</script>
<script type="text/html" id="power-type">
{{#if (d.type =='1') { }}
<span>選單</span>
{{# }else if(d.type == '2'){ }}
<span>按鈕</span>
{{# } }}
</script>
<script type="text/html" id="power-status">
<input type="checkbox" name="status" value=https://www.cnblogs.com/codermy/p/"{{d.id}}" lay-skin="switch" lay-text="啟用|禁用" lay-filter="user-status" checked = "{{ d.id == 10003 ?'true' : 'false' }}">
</script>
<script type="text/html" id="icon">
<i class="layui-icon {{d.icon}}"></i>
</script>
<script th:src=https://www.cnblogs.com/codermy/p/"@{/PearAdmin/component/layui/layui.js}" charset="utf-8"></script>
<script>
layui.use(['table','form','jquery','treetable'],function () {
let table = layui.table;
let form = layui.form;
let $ = layui.jquery;
let treetable = layui.treetable;
let MODULE_PATH = "operate/";
window.render = function(){
treetable.render({
treeColIndex: 1,
treeSpid: 0,
treeIdName: 'powerId',
treePidName: 'parentId',
skin:'line',
method:'post',
treeDefaultClose: true,
toolbar:'#power-toolbar',
elem: '#power-table',
url: '/api/menu',
page: false,
cols: [
[
{type: 'checkbox'},
{field: 'name', minWidth: 200, title: '選單標題'},
{field: 'icon', title: '圖示',templet:'#icon'},
{field: 'type', title: '型別',templet:'#power-type'},
{field: 'url', title: '路徑'},
{field: 'status', title: '是否可用',templet:'#power-status'},
{field: 'permission', title: '權限標識'},
{field: 'sort', title: '排序'},
{field: 'createTime', title: '創建日期'},
{title: '操作',templet: '#power-bar', width: 150, align: 'center'}
]
]
});
}
render();
table.on('tool(power-table)',function(obj){
if (obj.event === 'remove') {
window.remove(obj);
} else if (obj.event === 'edit') {
window.edit(obj);
}
})
table.on('toolbar(power-table)', function(obj){
if(obj.event === 'add'){
window.add();
} else if(obj.event === 'refresh'){
window.refresh();
} else if(obj.event === 'batchRemove'){
window.batchRemove(obj);
}
});
form.on('submit(menu-query)', function(data){ //模糊查詢方法
var formData = https://www.cnblogs.com/codermy/p/data.field;
var name = formData.name;
var type = formData.type;
table.reload(('power-table'),{ // table多載
where: {//這里傳參 向后臺
queryName: name,
queryType: type
//可傳多個引數到后臺... ,分隔
}
, url: '/api/menu'//后臺做模糊搜索介面路徑
, method: 'get'
});
return false;
});
window.add = function(){
layer.open({
type: 2,
title: '新增',
shade: 0.1,
area: ['450px', '500px'],
content: '/api/menu/add'
});
}
window.edit = function(obj){
var data = https://www.cnblogs.com/codermy/p/obj.data;
layer.open({
type: 2,
title:'修改',
shade: 0.1,
area: ['450px', '500px'],
content: '/api/menu/edit/?id='+data.id
});
}
window.remove = function(obj){
var data = https://www.cnblogs.com/codermy/p/obj.data;
layer.confirm('確定洗掉嗎,如果存在下級節點則一并洗掉,此操作不能撤銷!', {icon: 3, title:'提示'}, function(index){
layer.close(index);
let loading = layer.load();
$.ajax({
url: "/api/menu/?id=" + data.id,
dataType:'json',
type:'delete',
success:function(result){
layer.close(loading);
if(result.success){
layer.msg(result.msg,{icon:1,time:1000},function(){
obj.del();
});
}else{
layer.msg(result.msg,{icon:2,time:1000});
}
}
})
});
}
})
</script>
</body>
</html>
那么首先我們要給的是table的資料,因為考慮到有一個模糊查詢回傳的資料格式是一樣,所以可以合在一起寫,
MenuDao新建方法
/**
*
* @param queryName 查詢的表題
* @param queryType 查詢型別
* @return
*/
List<MyMenu> getFuzzyMenu(String queryName,Integer queryType);
因為之前在yml中已經配置了mapper.xml的路徑是在classpath:/mybatis-mappers/下,所以在resources目錄下新建mybatis-mappers檔案夾,在其中新建MenuMapper.xml檔案,
如果大家不想寫一些簡單的sql陳述句,推薦大家使用MybatisPlus或者JPA,MybatisPlus可能還要寫一些多表的sql陳述句,JPA幾乎見不到SQL,
<?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.codermy.myspringsecurityplus.dao.MenuDao">
<select id="getFuzzyMenu" resultType="com.codermy.myspringsecurityplus.entity.MyMenu">
-- 建議大家在寫查詢陳述句的時候不要寫select * ,可以通過這篇文章了解(https://blog.csdn.net/qq_36101933/article/details/93973266)
select t.id,t.parent_id,t.name,t.icon,t.url,t.permission,t.sort,t.type,t.create_time,t.update_time
from my_menu t
<where>
<if test="queryName != null and queryName != ''">
AND t.name like CONCAT('%', #{queryName}, '%')
</if>
<if test="queryType != null and queryType != ''">
AND t.type = #{queryType}
</if>
</where>
order by t.sort
</select>
</mapper>
這里再給大家安利一款idea的插件Free Mybatis plugin,它的作用就是可以快速通過xml找到mapper,或者mapper找到xml,效果如下圖
點擊箭頭就能快速定位到相應方法,非常好用,
然后就是service,impl,controller
/**
* @author codermy
* @createTime 2020/7/10
*/
public interface MenuService {
List<MyMenu> getMenuAll(String queryName,Integer queryType);
}
@Service//別忘了注解
public class MenuServiceImpl implements MenuService {
@Autowired
private MenuDao menuDao;
@Override
public List<MyMenu> getMenuAll(String queryName,Integer queryType) {
return menuDao.getFuzzyMenu(queryName,queryType);
}
}
@Controller
@RequestMapping("/api/menu")
@Api(tags = "系統:選單管理")
public class MenuController {
@Autowired
private MenuService menuService;
@GetMapping
@ResponseBody
@ApiOperation(value = https://www.cnblogs.com/codermy/p/"選單串列")
public Result getMenuAll(String queryName,Integer queryType){//這里沒選擇接收json字串,前端傳參通過/api/menu?queryName=測驗的方式
return Result.ok().data(menuService.getMenuAll(queryName,queryType)).code(ResultCode.TABLE_SUCCESS);
}
}
前端代碼我已經給出來了,重啟專案,打開就是那個效果,
這里稍微提一下RestFul風格
- GET :請求從服務器獲取特定資源,舉個例子:
GET /blog(獲取所有博客) - POST :在服務器上創建一個新的資源,舉個例子:
POST /blog(新建博客) - PUT :更新服務器上的資源,舉個例子:
PUT /blog/12(更新id為 12 的博客) - DELETE :從服務器洗掉特定的資源,舉個例子:
DELETE /blog/12(洗掉id為 12 的博客)
還有就是不要類似getAllBlog這種,冗余沒有意義,形式不固定,不同的開發者還需要了解檔案才能呼叫,
詳細看這篇文章
查已經完成了(模糊查詢同樣是這個介面,在前端頁面邏輯已經寫好了,里面給了注釋),接下來就是增刪改了,
MenuDao中添加如下方法
@Select("select t.id,t.parent_id,t.name,t.icon,t.url,t.permission,t.sort,t.type,t.create_time,t.update_time from my_menu t where t.id = #{id}")
MyMenu getMenuById(Integer id);
int update(MyMenu menu);
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into my_menu(parent_id, name, icon, url, permission, sort, type, create_time, update_time)values(#{parentId}, #{name}, #{icon}, #{url}, #{permission}, #{sort}, #{type}, now(), now())")
int save(MyMenu menu);
@Delete("delete from my_menu where id = #{id}")
int deleteById(Integer id);
@Delete("delete from my_menu where parent_id = #{parentId}")
int deleteByParentId(Integer parentId);
MenuMapper.xml中添加
<update id="update">
update my_menu t
<set>
<if test="parentId != null">
parent_id = #{parentId},
</if>
<if test="name != null">
`name` = #{name},
</if>
<if test="icon != null">
`icon` = #{icon},
</if>
<if test="url != null">
url = #{url},
</if>
<if test="permission != null">
permission = #{permission},
</if>
<if test="sort != null">
sort = #{sort},
</if>
<if test="type != null">
type = #{type},
</if>
update_time = #{updateTime}
</set>
where t.id = #{id}
</update>
MapperService
MyMenu getMenuById(Integer id)
Result updateMenu(MyMenu menu);
Result<MyMenu> save(MyMenu menu);
Result delete(Integer id);
MapperServiceImpl
@Override
public MyMenu getMenuById(Integer id) {
return menuDao.getMenuById(id);
}
@Override
public Result updateMenu(MyMenu menu) {
return (menuDao.update(menu) > 0) ? Result.ok().message("修改成功") : Result.error().message("修改失敗");
}
@Override
public Result<MyMenu> save(MyMenu menu) {
return (menuDao.save(menu) > 0) ? Result.ok().message("添加成功") : Result.error().message("添加失敗");
}
//如果這里洗掉了選單樹的父節點,把它的子節點一并洗掉
@Override
public Result delete(Integer id) {
menuDao.deleteById(id);
menuDao.deleteByParentId(id);
return Result.ok().message("洗掉成功");
}
我的后端邏輯寫的不是很完善,比如插入時選單名是否為空等等,只是在前端寫了一些,這樣普通用戶用是沒有什么問題,但是有些別有用心的人直接用你的介面,就會瘋狂報錯,造成服務器壓力,
MenuController中添加
@GetMapping(value = https://www.cnblogs.com/codermy/p/"/edit")
@ApiOperation(value = "跳轉修改選單頁面")
public String editPermission(Model model, MyMenu myMenu) {
model.addAttribute("myMenu",menuService.getMenuById(myMenu.getId()));
return "system/menu/menu-edit";
}
@PutMapping
@ResponseBody
@ApiOperation(value = "修改選單")
public Result updateMenu(@RequestBody MyMenu menu) {
return menuService.updateMenu(menu);
}
@GetMapping(value = "/add")
@ApiOperation(value = "跳轉添加選單頁面")
public String addMenu(Model model) {
model.addAttribute("myMenu",new MyMenu());
return "system/menu/menu-add";
}
@PostMapping
@ResponseBody
@ApiOperation(value = "添加選單")
public Result savePermission(@RequestBody MyMenu myMenu) {
return menuService.save(myMenu);
}
//todo 批量洗掉
@DeleteMapping
@ResponseBody
@ApiOperation(value = "洗掉選單")
public Result deleteMenu(Integer id) {
return menuService.delete(id);
}
那么不難發現我們還需要兩個頁面,分別是menu-add.html和menu-edit.html,
在對應位置創建,我直接給代碼
menu-add
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" th:href=https://www.cnblogs.com/codermy/p/"@{/PearAdmin/component/layui/css/layui.css}" />
<link rel="stylesheet" th:href="@{/PearAdmin/admin/css/pearCommon.css}"/>
<link rel="stylesheet" th:href="@{/PearAdmin/admin/css/pear-tree/dtree.css}" />
<link rel="stylesheet" th:href="@{/PearAdmin/admin/css/pear-tree/font/dtreefont.css}"/>
<body>
<script th:src="@{/PearAdmin/component/layui/layui.js}" charset="utf-8"></script>
<script>
layui.use(['iconPicker','dtree','form','jquery'],function(){
let form = layui.form;
let $ = layui.jquery;
let dtree = layui.dtree;
let formDate = null;
var iconPicker = layui.iconPicker;
// 初始化樹
dtree.render({
elem: "#dataTree",
initLevel: "1",
width: "100%",
method: 'get',
dataStyle: "layuiStyle", //使用layui風格的資料格式
response:{message:"msg",statusCode:200}, //修改response中回傳資料的定義
url: "/api/menu/build",
dataFormat: "list", //配置data的風格為list
select: true, //指定下拉樹模式
selectTips: "不選默認是頂級目錄",
selectCardHeight: "150"
});
iconPicker.render({
// 選擇器,推薦使用input
elem: '#iconPicker',
// 資料型別:fontClass/unicode,推薦使用fontClass
type: 'fontClass',
// 是否開啟搜索:true/false,默認true
search: true,
// 是否開啟分頁:true/false,默認true
page: true,
// 每頁顯示數量,默認12
limit: 16,
// 點擊回呼
click: function (data) {
console.log(data);
},
// 渲染成功后的回呼
success: function(d) {
console.log(d);
}
});
var param = dtree.getNowParam("dataTree");
formDate = $("#parentId");
dtree.on("node('dataTree')" ,function(obj){
var param = dtree.getNowParam("dataTree");
$("#parentId").val(param.nodeId);
formDate = $("#parentId");
});
form.verify({
name: function(value){
if(value.length < 2){
return '選單名至少2個字符';
}
},
sort: [
/^[1-9]\d*$/
,'只能是整數哦'
]
});
form.on('submit(user-save)', function(data){
var permissionId = formDate;
var bs = data.field.parentId
data.field.parentId = Number (bs)
var json = JSON.stringify(data.field)
$.ajax({
url:'/api/menu',
data:json,
dataType:'json',
contentType:'application/json',
type:'post',
success:function(result){
if(result.success){
layer.msg(result.msg,{icon:1,time:1000},function(){
parent.layer.close(parent.layer.getFrameIndex(window.name));//關閉當前頁
parent.location.reload();
});
}else{
layer.msg(result.msg,{icon:2,time:1000});
}
}
})
return false;
});
})
</script>
</body>
</html>
menu-edit
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" th:href=https://www.cnblogs.com/codermy/p/"@{/PearAdmin/component/layui/css/layui.css}" />
<link rel="stylesheet" th:href="@{/PearAdmin/admin/css/pearCommon.css}"/>
<link rel="stylesheet" th:href="@{/PearAdmin/admin/css/pear-tree/dtree.css}" />
<link rel="stylesheet" th:href="@{/PearAdmin/admin/css/pear-tree/font/dtreefont.css}"/>
<body>
<script th:src="@{/PearAdmin/component/layui/layui.js}" charset="utf-8"></script>
<script type="text/javascript">
layui.use(['iconPicker','dtree','form','jquery'],function(){
let form = layui.form;
let $ = layui.jquery;
let dtree = layui.dtree;
var iconPicker = layui.iconPicker;
// 初始化樹
dtree.render({
elem: "#dataTree",
initLevel: "1",
width: "100%",
method: 'get',
dataStyle: "layuiStyle", //使用layui風格的資料格式
response:{message:"msg",statusCode:200}, //修改response中回傳資料的定義
url: "/api/menu/build",
dataFormat: "list", //配置data的風格為list
select: true, //指定下拉樹模式
selectTips: "不選默認是頂級目錄",
selectCardHeight: "200",
});
iconPicker.render({
// 選擇器,推薦使用input
elem: '#iconPicker',
// 資料型別:fontClass/unicode,推薦使用fontClass
type: 'fontClass',
// 是否開啟搜索:true/false,默認true
search: true,
// 是否開啟分頁:true/false,默認true
page: true,
// 每頁顯示數量,默認12
limit: 12,
// 點擊回呼
click: function (data) {
console.log(data);
},
// 渲染成功后的回呼
success: function(d) {
console.log(d);
}
});
form.verify({
name: function(value){
if(value.length < 2){
return '選單名至少2個字符';
}
},
sort: [
/^[1-9]\d*$/
,'只能是整數哦'
]
});
form.on('submit(user-save)', function(data){
$.ajax({
url:'/api/menu',
data:JSON.stringify(data.field),
dataType:'json',
contentType:'application/json',
type:'put',
success:function(result){
if(result.success){
layer.msg(result.msg,{icon:1,time:1000},function(){
parent.layer.close(parent.layer.getFrameIndex(window.name));//關閉當前頁
parent.location.reload();//重繪頁面
});
}else{
layer.msg(result.msg,{icon:2,time:1000});
}
}
})
return false;
});
})
</script>
<script type="text/javascript">
</script>
</body>
</html>
重啟專案,訪問一下
這里的修改是通過model傳來的資料,.通過getMenuById方法回傳資料存入model,通過Thymeleaf模板引擎放入指定位置,這里批量洗掉的功能尚未實作,有興趣的同學可以自己實作,
這樣我們這個頁面基本就完成了,接下來的頁面基本都是一個套路,我就不貼全部的代碼了,挑其中部分來說說,全部的代碼可以在gitee和github中獲取,我已經按照每篇文章的進度添加tag,如果哪個部分沒出來的同學可以直接下載哪個部分.,
二、角色頁面
這個部分主要是有個選單樹,PearAdmin是選用的dtree來實作的,詳細用法請看官網 (我認為很全面了,基本的用法都能找到示例)
主要就是這個選單樹的資料怎么傳,在dtree官網上可以看到開啟復選框需要json中有個checkArr值,為0是未選中,1是選中,
那么我們新建一個MenuDto,來封裝一下我們需要的引數
@Data
public class MenuDto implements Serializable {
private Integer id;
private Integer parentId;
private String checkArr = "0";
private String title;
}
在MenuDao中添加如下方法
@Select("select t.id,t.parent_id,t.name,t.icon,t.url,t.permission,t.sort,t.type,t.create_time,t.update_time from my_menu t where t.id = #{id}")
MyMenu getMenuById(Integer id);
@Select("select p.id,p.parent_id,p.name from my_menu p inner join my_role_menu rp on p.id = rp.menu_id where rp.role_id = #{roleId}")
@Result(property = "title",column = "name")
List<MenuDto> listByRoleId(Integer roleId);
MenuServiceImpl中
@Override
public List<MenuDto> buildMenuAllByRoleId(Integer roleId) {
List<MenuDto> listByRoleId = menuDao.listByRoleId(roleId);
List<MenuDto> permissionDtos = menuDao.buildAll();
List<MenuDto> tree = TreeUtil.tree(listByRoleId, permissionDtos);
return tree;
}
這里我寫了一個TreeUtil工具類
public class TreeUtil {
//todo 判斷list是否為空
/**
*
* @param listByRoleId 通過角色id查詢的menuid
* @param menuDtos 回傳的menutree
* @return
*/
public static List<MenuDto> tree(List<MenuDto> listByRoleId, List<MenuDto> menuDtos ){
List<Integer> collect = listByRoleId.stream().map(MenuDto::getId).collect(Collectors.toList());
List<Integer> collect1 = menuDtos.stream().map(MenuDto::getId).collect(Collectors.toList());
for (Integer item : collect) {// 遍歷list2
if (collect1.contains(item)) {// 如果存在這個數
MenuDto menuDto = new MenuDto();
menuDto = menuDtos.get(item-1);
menuDto.setCheckArr("1");
menuDtos.set(item-1,menuDto);
}
}
return menuDtos;
}
}
這個工具類的作用就是通過角色id查詢這個角色所擁有的選單id,然后再查出所有的選單id,把他們比較,如果這其中有重復的選單id,就把這個id對應的MenuDto物件里的checkArr換成1,我這個方法可能會有點繞,如果有小伙伴有更好的方法,歡迎留言告訴我,
然后這個頁面的有需要注意的部分,就是再洗掉角色時,要先查詢是否已經有用戶是這個角色了,如果有就不能洗掉
三、用戶界面
這里無非也就是一些增刪改查,要寫的完善點的話也就是新增用戶時手機號是否能相同等等,我這里新增用戶時,會給他一個默認的密碼123456
@PostMapping
@ResponseBody
@ApiOperation(value = https://www.cnblogs.com/codermy/p/"添加用戶")
public Result saveUser(@RequestBody UserDto userDto){
MyUser myUser = null;
myUser = userService.getUserByPhone(userDto.getPhone());
if(myUser !=null && !(myUser.getId().equals(userDto.getId())) ){
return Result.error().code(20001).message("手機號已存在");
}
userDto.setPassword(MD5.crypt("123456"));
return userService.save(userDto,userDto.getRoleId());
}
目前用的時MD5的加密,但是這種密碼僅僅是加密了,相對而言會安全一些,但是如果兩個用戶的密碼是一樣的那么他們加密后的密碼也是一樣的,那么這其實也有辦法解決,就是給密碼加鹽,加鹽就是給密碼再加一個值,這樣即使不同用戶的相同的密碼在加密后也會不同,詳細解釋,之后會基于SpringSecurity的BCryptPasswordEncoder()方法進行加密,此方法自帶鹽,
那么這個部分的代碼就完成了,下一章正式進入SpringSecurity部分,
如果有同學不想寫前面部分,可以直接在gitee和github中下載v1.03的tag,里面是到本篇文章結束的所有代碼,
注意: 里面的是sql沒有更新,需要重新在倉庫中下載,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/69346.html
標籤:Java
上一篇:Gin框架介紹及使用
下一篇:文本相似度匹配,怎么辦,頭都禿了
