一、Shiro
Shiro是堡垒的意思,是一个身份认证和权限校验框架。通常来说在管理系统中权限是必不可少的一部分。公司所有的人都在同一个系统上进行操作,但是并不是所有的人都具备相同的权利。那么就需要在系统中将每个人所拥有的的权限明确的标识出来并同时在后端进行校验。
权限系统按照颗粒粒度分为:
按钮级别权限(决定某个用户能做什么不能做什么)
数据级别权限(决定用户在能做这件事的前提下,能对哪些数据做这件事)
要实现权限系统分为两个步骤:
1.所见即所得
我们需要在系统的界面层面,将用户所具备的菜单和按钮给用户呈现到界面上。如果不具备的菜单和按钮就不不予显示。
2.后端权限校验
仅仅在界面上做出处理是不够的,因为这无法对非法的请求(跨权限)做出拦截。所以每一个请求发送到后端,我们需要在后端进行一次判断,判断当前用户是否具备执行该业务的权限,如果没有权限不予执行。
二、数据库设计
经典RBAC数据库。
-- 部门表
CREATE TABLE DEPT(
ID INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(50)
);
-- 员工表
create table `user`(
id int primary key auto_increment,-- 用户id
username varchar(50),-- 登陆名
password varchar(50),-- 密码
phone varchar(11),-- 手机号码
sex int,-- 性别
age int,-- 年龄
did int
);
-- 角色表 记录系统中的角色信息
create table role(
id int primary key auto_increment,-- 角色id
name varchar(255)-- 角色名称
);
-- 菜单表 记录系统中所有的菜单信息 精确到按钮级别
create table Menu(
id int primary key auto_increment,-- 权限ID
name varchar(255),-- 权限名称
resource varchar(255),-- 当前权限所访问的系统中的资源地址
pid int,-- 记录权限的父级权限编号
level int-- 记录权限级别 (1:一级菜单 2:二级菜单,3:按钮)
);
-- 用户角色表 记录系统中的用户所拥有的角色信息
create table user_role(
id int primary key auto_increment,-- 主键
uid int,-- 用户id
rid int-- 角色id
);
-- 部门权限表
create table dept_permission(
id int primary key auto_increment,-- 主键
did int,-- 部门编号
mid int-- 菜单编号
);
-- 用户权限表
create table user_permission(
id int primary key auto_increment,-- 主键
uid int,-- 用户编号
mid int-- 菜单编号
);
-- 角色权限表
create table role_permission(
id int primary key auto_increment,-- 主键
rid int,-- 角色编号
mid int-- 菜单编号
);
-- 基础数据录入
-- 录入部门信息
INSERT INTO DEPT VALUES(NULL,'教学部'); -- 教学部部门编号1
INSERT INTO DEPT VALUES(NULL,'财务部'); -- 财务部部门编号2
-- 录入用户信息
INSERT INTO `USER` VALUES(NULL,'qiang','123456','13666666666',1,18,1); -- qiang的编号1
INSERT INTO `USER` VALUES(NULL,'cong','123456','13888888888',1,18,2);-- cong的编号2
-- 录入菜单信息
INSERT INTO MENU VALUES(NULL,'教学管理','',0,1);-- 菜单编号1 无父级菜单 一级菜单
INSERT INTO MENU VALUES(NULL,'课程管理','',1,2);-- 菜单编号2 父级菜单编号1 二级菜单
INSERT INTO MENU VALUES(NULL,'新增课程','',2,3);-- 菜单编号3 父级菜单编号2 按钮
INSERT INTO MENU VALUES(NULL,'删除课程','',2,3);-- 菜单编号4 父级菜单编号2 按钮
INSERT INTO MENU VALUES(NULL,'修改课程','',2,3);-- 菜单编号5 父级菜单编号2 按钮
INSERT INTO MENU VALUES(NULL,'财务管理','',0,1);-- 菜单编号6 无父级菜单 一级菜单
INSERT INTO MENU VALUES(NULL,'报销管理','',6,2);-- 菜单编号7 父级菜单6 二级菜单
INSERT INTO MENU VALUES(NULL,'审核报销','',7,3);-- 菜单编号8 父级菜单7 按钮
INSERT INTO MENU VALUES(NULL,'申请报销','',6,2);-- 菜单编号9 父级菜单6 二级菜单
INSERT INTO MENU VALUES(NULL,'撤回','',9,3);-- 菜单编号10 父级菜单9 按钮
INSERT INTO MENU VALUES(NULL,'系统管理','',0,1);-- 菜单编号11 无父级菜单 一级菜单
INSERT INTO MENU VALUES(NULL,'部门权限管理','',11,2);-- 菜单编号12 父级菜单11 二级菜单
INSERT INTO MENU VALUES(NULL,'角色权限管理','',11,2);-- 菜单编号13 父级菜单11 二级菜单
INSERT INTO MENU VALUES(NULL,'用户权限管理','',11,2);-- 菜单编号14 父级菜单11 二级菜单
INSERT INTO MENU VALUES(NULL,'变更角色权限','',13,3);-- 菜单编号15 父级菜单13 按钮
-- 录入角色信息
INSERT INTO ROLE VALUES(NULL,'系统管理员');-- 角色编号1
INSERT INTO ROLE VALUES(NULL,'总经理');-- 角色编号2
INSERT INTO ROLE VALUES(NULL,'部门经理');-- 角色编号3
-- 录入用户角色信息
INSERT INTO USER_ROLE VALUES(NULL,1,1);-- 用户编号1的qiang为管理员角色
INSERT INTO USER_ROLE VALUES(NULL,2,2);
INSERT INTO USER_ROLE VALUES(NULL,2,3);-- 用户编号2的cong为总经理兼部门经理
-- 录入部门权限
INSERT INTO DEPT_PERMISSION VALUES(NULL,1,1);-- 教学部门拥有教学管理菜单
INSERT INTO DEPT_PERMISSION VALUES(NULL,1,2);-- 教学部门拥有教学管理菜单下的课程管理(查询)
INSERT INTO DEPT_PERMISSION VALUES(NULL,1,6);-- 教学部门拥有财务管理菜单
INSERT INTO DEPT_PERMISSION VALUES(NULL,1,9);-- 教学部门拥有财务管理菜单下的申请报销
INSERT INTO DEPT_PERMISSION VALUES(NULL,2,6);-- 财务部门拥有财务管理菜单
INSERT INTO DEPT_PERMISSION VALUES(NULL,2,7);-- 财务部门拥有财务管理菜单下的报销管理
INSERT INTO DEPT_PERMISSION VALUES(NULL,2,8);-- 财务部门拥有财务管理菜单下的报销管理(审核报销)
INSERT INTO DEPT_PERMISSION VALUES(NULL,2,9);-- 财务部门拥有财务管理菜单下的申请报销
-- 录入角色权限
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,1);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,2);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,3);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,4);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,5);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,6);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,7);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,8);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,9);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,10);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,11);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,12);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,13);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,14);
INSERT INTO ROLE_PERMISSION VALUES(NULL,1,15);
INSERT INTO ROLE_PERMISSION VALUES(NULL,2,1);
INSERT INTO ROLE_PERMISSION VALUES(NULL,2,2);
INSERT INTO ROLE_PERMISSION VALUES(NULL,2,3);
INSERT INTO ROLE_PERMISSION VALUES(NULL,2,4);
INSERT INTO ROLE_PERMISSION VALUES(NULL,2,5);
INSERT INTO ROLE_PERMISSION VALUES(NULL,2,6);
INSERT INTO ROLE_PERMISSION VALUES(NULL,2,7);
INSERT INTO ROLE_PERMISSION VALUES(NULL,2,8);
INSERT INTO ROLE_PERMISSION VALUES(NULL,2,9);
INSERT INTO ROLE_PERMISSION VALUES(NULL,2,10);
-- SQL 查询1号用户所拥有的角色权限菜单(1级菜单和2级菜单)
SELECT M.* FROM USER_ROLE UR LEFT JOIN ROLE_PERMISSION RP ON UR.RID=RP.RID LEFT JOIN MENU M ON RP.MID=M.ID WHERE UR.UID=1 AND M.LEVEL<3
-- SQL 查询1号用户所拥有的的部门权限菜单(1级菜单和2级菜单)
SELECT M.* FROM USER U LEFT JOIN DEPT_PERMISSION DP ON U.DID=DP.DID LEFT JOIN MENU M ON DP.MID=M.ID WHERE U.ID=1 AND M.LEVEL<3
-- SQL 查询1号用户所拥有的的用户权限(1级菜单和2级菜单)
SELECT M.* FROM USER_PERMISSION UP LEFT JOIN MENU M ON UP.MID=M.ID WHERE UP.UID=1 AND M.LEVEL<3
-- 合并去重
SELECT M.* FROM USER_ROLE UR LEFT JOIN ROLE_PERMISSION RP ON UR.RID=RP.RID LEFT JOIN MENU M ON RP.MID=M.ID WHERE UR.UID=2 AND M.LEVEL<3
UNION
SELECT M.* FROM USER U LEFT JOIN DEPT_PERMISSION DP ON U.DID=DP.DID LEFT JOIN MENU M ON DP.MID=M.ID WHERE U.ID=2 AND M.LEVEL<3
UNION
SELECT M.* FROM USER_PERMISSION UP LEFT JOIN MENU M ON UP.MID=M.ID WHERE UP.UID=2 AND M.LEVEL<3
-- 为了便于mybatis进行一对多的菜单封装,SQL还需改进
SELECT
M1.ID ID1,M1.NAME NAME1, M1.RESOURCE RESOURCE1,M1.PID PID1,M1.LEVEL LEVEL1,
M2.ID ID2,M2.NAME NAME2, M2.RESOURCE RESOURCE2,M2.PID PID2,M2.LEVEL LEVEL2
FROM
(
SELECT M.* FROM USER_ROLE UR LEFT JOIN ROLE_PERMISSION RP ON UR.RID=RP.RID LEFT JOIN MENU M ON RP.MID=M.ID WHERE UR.UID=#{UID} AND M.LEVEL=2
UNION
SELECT M.* FROM USER U LEFT JOIN DEPT_PERMISSION DP ON U.DID=DP.DID LEFT JOIN MENU M ON DP.MID=M.ID WHERE U.ID=#{UID} AND M.LEVEL=2
UNION
SELECT M.* FROM USER_PERMISSION UP LEFT JOIN MENU M ON UP.MID=M.ID WHERE UP.UID=#{UID} AND M.LEVEL=2
) M2 LEFT JOIN MENU M1 ON M2.PID=M1.ID
<!--菜单查询映射结果-->
<resultMap id="menuMap" type="Menu">
<id column="id1" property="id"></id>
<result column="name1" property="name"></result>
<result column="resource1" property="resource"></result>
<result column="pid1" property="pid"></result>
<result column="level1" property="level"></result>
<collection property="children" ofType="Menu">
<id column="id2" property="id"></id>
<result column="name2" property="name"></result>
<result column="resource2" property="resource"></result>
<result column="pid2" property="pid"></result>
<result column="level2" property="level"></result>
</collection>
</resultMap>
三、使用Shiro实现身份认证
Apache Shiro 是ASF旗下的一款开源软件(Shiro发音为“shee-roh”,日语“堡垒(Castle)”的意思),提供的一个强大而灵活的安全框架。
Apache Shiro提供了认证、授权、加密和会话管理功能,将复杂的问题隐藏起来,提供清晰直观的API使开发者可以很轻松地开发自己的程序安全代码。
Subject:即”用户”,外部应用都是和Subject进行交互的,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。 Subject在shiro中是一个接口,接口中定义了很多认证授权相关的方法,外部程序通过subject进行认证授权,而subject是通过SecurityManager安全管理器进行认证授权(Subject相当于SecurityManager的门面)。
SecurityManager:即安全管理器,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等。
Authentication:是一个对用户进行身份验证(登录)的组件。
Authorization:即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。就是用来判断是否有权限,授权,本质就是访问控制,控制哪些URL可以访问.
Realm:即领域,用于封装身份认证操作和授权操作,如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
在使用Shiro之前首先要明确的Shiro工作内容,Shiro只负责对用户进行身份认证和权限验证,并不负责权限的管理,也就是说网页中的按钮是否显示、系统中有哪些角色、用户拥有什么角色、每个角色对应的权限有哪些,这些都需要我们自己来实现,换句话说Shiro只能利用现有的数据进行工作,而不能对数据库的数据进行修改。
1、引入shiro依赖
<!--shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
2、新建一个领域类
新建一个领域类,该类用于封装登陆和授权操作。
/*
封装认证和授权操作
*/
public class UserRealm extends AuthorizingRealm {
//封装登陆方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
//封装授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
}
3、初始化Shiro
配置领域、配置安全管理器、配置过滤器
请求->过滤器->根据黑白名单判断是够需要登陆->黑名单->判断session会话对应的subject,判断subject是否已经登陆,如果没有登陆重定向到某个页面。
@Configuration
public class ShiroConfig {
@Bean
public UserRealm initUserRealm(){
return new UserRealm();
}
@Bean
public SecurityManager initSecurityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(initUserRealm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter() throws UnsupportedEncodingException {
//实例化Shiro过滤器工厂
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//在工厂中注入安全管理器
shiroFilterFactoryBean.setSecurityManager(initSecurityManager());
//创建一个有序键值对用于存储黑白名单
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//anon表示无须登陆就能访问的资源地址
filterChainDefinitionMap.put("/page/login.html","anon");
filterChainDefinitionMap.put("/user", "anon");
//需要在登陆之后才能访问的资源
filterChainDefinitionMap.put("/**", "authc");
//如果没有登陆shiro自动重定向的地址
shiroFilterFactoryBean.setLoginUrl("/page/login.html");
//将黑白名单配置到shiro过滤器
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
}
4、使用Shiro完成登录
流程图:
- 在控制层中将用户名密码封装为UsernamePasswordToken
UsernamePasswordToken token=new UsernamePasswordToken(username,password);
- 从SecurityUtils取出Subject对象
Subject subject=SecurityUtils.getSubject();
Subject是主体对象,是Shiro对于用户的抽象。当用户第一次访问服务器时,请求经过Shiro过滤器,在过滤器中就会创建一个HttpSession对象。在创建一个Subject对象,此时Subject对象的登陆状态是未登录(未认证)。并将Subject存储到SecurityManager中,只要HttpSession不变,该Subject就是当前用户主体对象。Subject对象在登陆成功以后会自动将User信息存储起来,后续要使用用户信息则通过Subject对象来获取。
User user = (User) SecurityUtils.getSubject().getPrincipal();
- 判断登陆状态,如果没有登陆则通过Subject进行登录
if(!subject.isAuthenticated()){
subject.login(token);
}
- 执行登陆方法,最终会执行到领域类的认证方法中
在该方法中完成登录业务的调用,根据返回值封装认证信息对象
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username= (String) authenticationToken.getPrincipal();//取出用户名
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
User user = userService.getOne(wrapper);
//封装为一个认证信息对象
SimpleAuthenticationInfo info=null;
if(user!=null){
info=new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
return info;
}
当用户不存在时,返回值为NULL,一旦此处返回Null,Shiro就认定用户名不存在则会抛出账户名不存在的异常。如果用户存在,就需要将用户信息认证信息返回给Shiro,Shiro会判断查询出的密码和token中的密码是否一致,如果不一致则抛出密码错误的异常,如果密码正确则正常返回到控制层。所以我们需要提供全局异常处理器来处理这两类异常,分别作出对应的响应。
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UnknownAccountException.class)
@ResponseBody
public JSONResult handlerUnknowAccountException(){
return new JSONResult("1002","用户名不存在",null,null);
}
@ResponseBody
@ExceptionHandler(IncorrectCredentialsException.class)
public JSONResult handlerIncorrectCredentialsException(){
return new JSONResult("1002","密码错误",null,null);
}
}
四、使用RememberMe
1、在ShiroConfig中配置Cookie管理器
配置Cookie管理器的目的是设置cookie名称以及AES加密秘钥,通过Base64将一个24长度的字符串加密为16长度的字节数组,每一个字节占8位,该字节数组总长度128位,AES加密秘钥长度必须为128位、192、256。
@Bean
public CookieRememberMeManager initCookieRememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
SimpleCookie rememberMe = new SimpleCookie("rememberMe");
rememberMe.setMaxAge(7*24*60*60);
cookieRememberMeManager.setCookie(rememberMe);
//设置加密秘钥
cookieRememberMeManager.setCipherKey(Base64.decode("Woniuxywuyanzu520niubi=="));
return cookieRememberMeManager;
}
//将Cookie管理器添加到安全管理器中
@Bean
public SecurityManager initSecurityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(initUserRealm());
securityManager.setRememberMeManager(initCookieRememberMeManager());
return securityManager;
}
2、修改登陆页面,提供记住我选项
<input type="checkbox" v-model="user.remember">7天免登陆
<script>
data:{
user:{
username:"",
password:"",
remember:true
}
}
</script>
3、在控制层接收记住我参数
在控制层接收前端传递的多选框参数remember,直接将该数据封装到Token中,如果该值为true,Shiro就会开启RememberMe功能,如果为false则不开启。开启功能之后,在登陆成功以后会将Subject数据进行序列化加密响应到Cookie中。注意由于Subject中存储了User数据的,所以User数据也会同时序列化,User类必须实现序列化接口。
//1.将用户名和密码封装为token
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword(),remember);
//2.调用Subject提供的认证方法
Subject subject = SecurityUtils.getSubject();
//3.判断当前用户的登陆状态
if(!subject.isAuthenticated()&&!subject.isRemembered()){
subject.login(token);
}
4、修改过滤器拦截规则
将/**拦截状态修改为user,表示在认证状态和记住我状态都可以正常访问这些资源。
//需要在登陆之后才能访问的资源
//user表示认证或记住我这两种状态都能访问
filterChainDefinitionMap.put("/**", "user");
5、注销功能实现
在ShiroConfig的过滤器配置中添加一个注销地址映射:
//添加注销地址
filterChainDefinitionMap.put("/logout","logout");
//需要在登陆之后才能访问的资源
//user表示认证或记住我这两种状态都能访问
filterChainDefinitionMap.put("/**", "user");
在网页上通过超链接访问/logout地址,就完成了注销。
所有的请求都会经过Shiro提供的过滤器,Shiro如果发现我们访问的是logout地址,它就会清空cookie,改变Subject状态,重定向到登录登录页面。
五、授权功能实现
1、生成菜单时,在li中添加a标签
通过vue的属性绑定将菜单的resource属性绑定到href属性中,给a标签添加target属性,target属性值等于一个iframe的name值,iframe不能放到vue挂载的标签中
<div id="content">
<div class="left">
<a href="/logout">注销</a>
<ul>
<li v-for="m1 in list">
<h1>{{m1.name}}</h1>
<ul>
<li v-for="m2 in m1.children">
<a :href="m2.resource+'?id='+m2.id" target="main">{{m2.name}}</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
<div class="right">
<iframe name="main"></iframe>
</div>
2、创建新网页网页地址和数据库菜单的resource保持一致
3、先查询出所有的角色信息,将角色信息渲染为一个下拉框
<div id="content">
<select v-model="currentRole">
<option value="0">请选择角色</option>
<option v-for="role in roles" :value="role.id" v-text="role.name"></option>
</select>
</div>
new Vue({
el:"#content",
data:{
roles:[]
},
created:function(){
this.selectRoles();
},
methods:{
selectRoles:function(){
var _this=this;
$.ajax({
url:"",
type:"get",
success:function(data){
_this.roles=data.list;
}
});
}
}
});
@GetMapping
public JSONResult select() throws Exception{
return new JSONResult("1000","success",null,roleService.list());
}
在data中定义currentRole数据,该数据默认值为”0”,将currentRole竖向绑定到下拉框,实现下拉框与当前角色ID进行绑定。
new Vue({
el:"#content",
data:{
roles:[],
currentRole:"0"
},
created:function(){
this.selectRoles();
},
methods:{
selectRoles:function(){
var _this=this;
$.ajax({
url:"",
type:"get",
success:function(data){
_this.roles=data.list;
}
});
}
}
});
4、查询所有的菜单
先查询出所有的菜单,用多选框将菜单渲染到网页上。
在data中定义一个menu1属性,保存所有的以及菜单集合,以及菜单中嵌套二级菜单,二级中嵌套了三级,定义函数查询所有的菜单,在钩子函数中调用
new Vue({
el:"#content",
data:{
roles:[],
currentRole:"0",
menu1:[]
},
created:function(){
this.selectRoles();
this.selectMenus();
},
methods:{
selectRoles:function(){
var _this=this;
$.ajax({
url:"",
type:"get",
success:function(data){
_this.roles=data.list;
}
});
},
selectMenus:function(){
var _this=this;
$.ajax({
url:"/menu/all",
type:"get",
success:function(data){
_this.menus1=data.list;
}
});
}
}
});
<div id="content">
<select v-model="currentRole">
<option value="0">请选择角色</option>
<option v-for="role in roles" :value="role.id" v-text="role.name"></option>
</select>
<div>
<div id="menu">
<div v-for="m1 in menus1"><!--一级菜单-->
<div class="menu">
<input type="checkbox" :value="m1.id">
<span>{{m1.name}}</span>
</div>
<div class="l2" v-for="m2 in m1.children">
<div class="menu">
<input type="checkbox" :value="m2.id">
<span>{{m2.name}}</span>
</div>
<div class="l3" v-for="m3 in m2.children">
<div class="menu"><input type="checkbox" :value="m3.id"><span>{{m3.name}}</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
@GetMapping("all")
public JSONResult select() throws Exception{
//查询一级
QueryWrapper<Menu> wrapper1 = new QueryWrapper<Menu>();
wrapper1.eq("level",1);
List<Menu> menus1 = menuService.list(wrapper1);
for(Menu l1:menus1){
QueryWrapper<Menu> wrapper2 = new QueryWrapper<Menu>();
wrapper2.eq("pid",l1.getId());
List<Menu> menus2 = menuService.list(wrapper2);
l1.setChildren(menus2);
for(Menu l2:menus2){
QueryWrapper<Menu> wrapper3 = new QueryWrapper<Menu>();
wrapper3.eq("pid",l2.getId());
List<Menu> menus3 = menuService.list(wrapper3);
l2.setChildren(menus3);
}
}
return new JSONResult("1001","success",null,menus1);
}
5、根据选择的角色查询该角色的权限并渲染到多选框
侦听currentRole数据,当选择角色下拉框发生改变,该数据会对应发生改变。触发侦听器,在侦听器中将该数据作为参数传到后端,后端使用该数据查询出该角色的所有权限。
在data中定义permission数据,该数据时一个数组,用于存储当前角色所拥有的的所有菜单编号
new Vue({
el:"#content",
data:{
roles:[],
currentRole:"0",
menu1:[],
permission:[]
},
created:function(){
this.selectRoles();
this.selectMenus();
},
methods:{
selectRoles:function(){
var _this=this;
$.ajax({
url:"",
type:"get",
success:function(data){
_this.roles=data.list;
}
});
},
selectMenus:function(){
var _this=this;
$.ajax({
url:"/menu/all",
type:"get",
success:function(data){
_this.menus1=data.list;
}
});
},
selectPermission:function(){
var _this=this;
$.ajax({
url:"/rolePermission/"+_this.currentRole,
type:"get",
success:function(data){
_this.permission=data.object;
}
});
}
},
watch:{
currentRole:function(){
if(this.currentRole=="0"){
this.permission=[];
}else{
this.selectPermission();
}
}
}
});
前端将当前选中的角色编号传递到后端,后端通过角色编号查询出该角色所拥有的的所有菜单(RolePermission),遍历角色权限集合,将该对象的菜单编号封装为一个数组,将数组响应给前端。
@RestController
@RequestMapping("/rolePermission")
public class RolePermissionController {
@Resource
private RolePermissionService rolePermissionService;
@GetMapping("{rid}")
public JSONResult selectByRoleId(@PathVariable("rid") int rid) throws Exception{
QueryWrapper<RolePermission> wrapper = new QueryWrapper<>();
wrapper.eq("rid",rid);
List<RolePermission> list = rolePermissionService.list(wrapper);
int[] ids=new int[list.size()];
for(int i=0;i<list.size();i++){
ids[i]=list.get(i).getMid();
}
return new JSONResult("1001","success",ids,null);
}
}
通过v-model将后端响应的菜单编号数组双向绑定到所有的多选框上。绑定逻辑是多选框的value值在数组中存在则选中该多选框,取消多选框选中数组中的菜单编号也会删除。
<div id="content">
<select v-model="currentRole">
<option value="0">请选择角色</option>
<option v-for="role in roles" :value="role.id" v-text="role.name"></option>
</select>
<div>
<div id="menu"><!--一级菜单-->
<div v-for="m1 in menus1">
<div class="menu">
<input v-model="permission" type="checkbox" :value="m1.id">
<span>{{m1.name}}</span>
</div>
<div class="l2" v-for="m2 in m1.children">
<div class="menu">
<input v-model="permission" type="checkbox" :value="m2.id">
<span>{{m2.name}}</span>
</div>
<div class="l3" v-for="m3 in m2.children">
<div class="menu"><input v-model="permission" type="checkbox" :value="m3.id"><span>{{m3.name}}</span></div>
</div>
</div>
</div>
</div>
</div>
使用Jquery完成多选框级联选中和不选中
给所有的多选框绑定点击事件,@click=”choose($event)”
methods:{
choose:function(event){
var currentCheckbox=$(event.target);//点击的这个多选框对象
var c = $(currentCheckbox).prop("checked");
if(c){
var e1= $(currentCheckbox).parent().parent().prevAll("div").children("input");
e1.prop("checked",c);
var e2=e1.parent().parent().prevAll("div").children("input");
e2.prop("checked",c);
}else{
$(currentCheckbox).parent().nextAll("div").find("input").prop("checked",c);
}
}
}
6、更新权限信息
动态生成更新权限信息的按钮,按钮绑定事件执行updatePermission函数。将currentRole和permission使用Json的方式传到后端,在后端定义一个VO类来接收这两个参数。
updatePermission:function(){
var _this=this;
$.ajax({
url:"/rolePermission/",
type:"post",
contentType:"application/json",
data:JSON.stringify({
rid:_this.currentRole,
permission:_this.permission
}),
success:function(data){
alert(data.message);
}
});
}
后端接收参数,在业务层中先根据角色ID删除该角色的所有权限,然后通过循环将新的权限信息写入数据库
@PostMapping
public JSONResult updatePermission(@RequestBody UpdateRolePermissionVo vo)throws Exception{
rolePermissionService.updatePermission(vo);
return new JSONResult("1001","success",null,null);
}
@Override
@Transactional
public void updatePermission(UpdateRolePermissionVo vo) throws Exception {
rolePermissionMapper.delete(new QueryWrapper<RolePermission>().eq("rid",vo.getRid()));
for(int i=0;i<vo.getPermission().length;i++){
rolePermissionMapper.insert(new RolePermission(vo.getRid(),vo.getPermission()[i]));
System.out.println(1/0);
}
}
六、后端权限校验
权限校验流程:
配置流程:
1、在领域类中完善授权方法
查询数据库将该用户的所有权限查询出来,将这些权限信息封装到SimpleAuthorizationInfo对象,使用菜单名称作为权限名称。
//封装授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//查询用户所有的权限
User user = (User) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
try {
List<Menu> permissions = menuService.selectPermission(user.getId());
for(Menu menu:permissions){
simpleAuthorizationInfo.addStringPermission(menu.getName());
}
} catch (Exception e) {
e.printStackTrace();
}
return simpleAuthorizationInfo;
}
2、配置Shiro的权限校验通知
在ShiroConfig中添加两个Bean,这两个Bean一个是通知类,一个是代理类。
//权限校验AOP配置
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(initSecurityManager());
return advisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator app=new DefaultAdvisorAutoProxyCreator();
app.setProxyTargetClass(true);
return app;
}
3、在控制器方法上通过注解描述所需权限
@RequiresPermissions({权限1,权限2})
在控制器方法上通过上述注解描述该方法所需权限。
@GetMapping
@RequiresPermissions({"角色管理"})
public JSONResult select() throws Exception{
return new JSONResult("1000","success",null,roleService.list());
}
4、在全局异常处理器中针对没有权限异常进行处理
@ExceptionHandler(AuthorizationException.class)
public JSONResult handlerAuthorizationException(){
return new JSONResult("1004","权限不足",null,null);
}
七、Jwt
目前使用的Shiro进行用户认证内部通过Session识别Subject,服务器识别依赖JSESSIONID的Cookie。但是在前后端分离的项目中,前端项目会单独运行挂载另外一个服务器中和后端项目的服务器不同。前端向后端发送请求时是跨域的无法携带JSESSIONID的Cokie的,就会导致每一个请求都是一个新的Session。问题的根源在传统的会话跟踪技术(Session+Cookie)存在弊端:
1.跨域问题
2.集群问题
那么必须找一个新的技术来实现会话跟踪,我们接触过一个东西token,令牌机制是一种很好的解决方案。但是在前面的应用中token存在数据库,需要频繁的访问数据库,这是极其影响服务器性能的。那么在传统的token之上再次升级就是Jwt(JSONS WEB TOKEN)。
要使用一种新的技术来完成会话跟踪,必须满足以下要求:
1.不能接触Cookie,要能够跨域传输
2.能够识别用户身份
3.足够安全
JWT是一种规范,是对token提出的一种标准。详细的描述了一个token应该具备哪些数据。每一段数据的具体作用如何。
1、JWT规范
JWT是一段字符串,这段字符串由三个部分组成,每一部分之间使用.分隔。例如:xxxxxxx.yyyyyyy.zzzzzz。
这三部分分别是:
Header(头信息):
头信息是一段JSON数据,描述jwt的加密方式和类型,这一段内容基本不变。
{
"alg": "HS256",//加密方式
"typ": "JWT"//类型
}
将这样一段JSON进行Base64URL加密处理之后得到的就是JWT的第一段内容。
Payload(荷载)
荷载同样是一段JSON数据,描述JWT信息本体。规范中指出荷载可选的7个预定义属性为:
iss (issuer):签发人
sub (subject):主体,存储用户ID
iat (Issued At):签发时间
exp (expiration time):过期时间
nbf (Not Before):生效时间,在此之前是无效的
jti (JWT ID):编号
aud (audience):受众
例如:
{
"iss": "http://localhost:8000/auth/login",
"sub": "1",
"iat": 1451888119,
"exp": 1454516119,
"nbf": 1451888119,
"jti": "37c107e4609ddbcc9c096ea5ee76c667",
"aud": "dev"
}
将这样一段JSON数据进行Base64URL加密处理之后得到的就是JWT的第二段内容
signature(签名):
签名是JWT安全性的最大保障。因为Base64是可逆的,如果客户端将荷载使用Base64解密,修改sub,然后在加密覆盖原本的荷载,就可以伪造JWT。为了防止客户端伪造JWT,我们将Header的内容+Payload的内容进行一种安全性更高的加密(HS256),加密之后的内容就是签名。HS256是一种带秘钥的摘要加密,摘要加密的特点是不可逆。
我们在后端解析JWT识别用户身份,用户身份在荷载中,这个通过Base64可以拿到。但是在解析JWT之前我们会先校验JWT的合法性。
签名对jwt安全性保障:
2、JWT的使用
使用第三方的JWT生成器:
引入依赖:
<!-- jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
JWT工具类:
//JWT工具类
public class JWTUtils {
//定义加密秘钥
private final static String KEY="wuyanzudemiyao";
//定义JWT的有效时间
private final static long TIME=3*24*60*60*1000;
/*
生成JWT的方法
*/
public static String generatorJWT(String id){
JwtBuilder builder = Jwts.builder()
.setSubject(id)//设置用户ID
.setIssuedAt(new Date())//设置签发时间
.setExpiration(new Date(new Date().getTime()+TIME))//设置过期时间
.signWith(SignatureAlgorithm.HS256, KEY);//设置签名方式和秘钥
String token = builder.compact();
return token;
}
/*
校验JWT
*/
public static void validateJWT(String token) throws Exception{
Jwts.parser().setSigningKey(KEY).parseClaimsJws(token);
}
/*
解析token并获取subject
*/
public static String getId(String token) throws Exception{
Claims claims = Jwts.parser().setSigningKey(KEY).parseClaimsJws(token).getBody();
return claims.getSubject();
}
}
3、JWT和实际业务的结合
Jwt用于用户身份认证业务流程:
Jwt签发:
jwt校验:
4、Shiro+JWT实现用户认证
项目和依赖和前面的Shiro项目一致
- 实现登陆并签发token
在前面的认证中,登陆放到了领域中,在Shiro+Jwt做认证的项目中登陆还是放到原本的控制器中。登陆成功以后生成一个token,将token放到JSONResult一起响应给客户端。原本的领域我们只用来进行token的校验。
@RestController
@RequestMapping("user")
@CrossOrigin
public class UserController {
@GetMapping("login")
public JSONResult login(String username, String password) throws Exception{
if("admin".equals(username)&&"123456".equals(password)){
//使用工具生成token
String token = JWTUtils.generatorJWT("1");
return new JSONResult("1001","success",token,null);
}
return new JSONResult("1001","fail",null,null);
}
}
<script>
$("#btn").click(function(){
$.ajax({
url:"http://localhost/user/login",
type:"get",
data:$("#login-form").serialize(),
success:function(data){
alert(data.message);
//将token存储到localStorage
localStorage.setItem("token",data.object);
}
});
});
</script>
- 前端在发送请求时需要在请求头中携带token
$.ajax({
url:"http://localhost/user",
type:"get",
headers:{"token":localStorage.getItem("token")},
success:function(data){
alert(data.object);
}
});
- 在控制器中可以从请求头中取出token
从请求头中取出token,使用JWTUtils取出其中的subject数据,参与业务
@GetMapping
public JSONResult selectUser(@RequestHeader("token") String token) throws Exception{
String id=JWTUtils.getId(token);
return new JSONResult("1001","success","项神YYDS",null);
}
- 使用Shiro完成JWT的认证
自定义一个过滤器,在该过滤器中完成对于jwt的校验
public class JWTFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//返回值true表示请求向下继续执行
//判断请求头中是否包含token
HttpServletRequest req= (HttpServletRequest) request;
if(req.getHeader("token")!=null){
//调用认证方法,认证结果就代表本次是否放行
try {
return executeLogin(request,response);
} catch (Exception e) {
e.printStackTrace();
}
}
//返回值false shiro会抛出401异常
return false;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req= (HttpServletRequest) request;
//调用领域类中的方法执行认证
Subject subject = SecurityUtils.getSubject();
JWTToken jwtToken = new JWTToken(req.getHeader("token"));
//让shiro通过领域类完成认证
subject.login(jwtToken);//执行认证 不是登陆
return true;
}
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
//处理跨域
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
//如果请求方式是options,代表着是预检请求,因此,直接放行
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
要去执行认证需要传入Shiro中的Token类对象,需要自定义一个Token,用户名和密码都是jwt的token
public class JWTToken implements AuthenticationToken {
public JWTToken(String token){
this.token=token;
}
private String token;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
在领域类中处理认证逻辑
public class JWTRealm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;//判定token类型,如果是JWTToken则允许执行认证
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token= (String) authenticationToken.getPrincipal();
//进行token校验
//通过校验说明token有效 返回认证信息
try {
JWTUtils.validateJWT(token);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(token, token,getName());
return info;
} catch (Exception e) {
//没有通过校验返回null
e.printStackTrace();
return null;
}
}
}
- 将自定义过滤器配置到Shiro中
在之前的项目中,我们使用的anon、user、logout都是Shiro自带的过滤器,我们要通过过滤器实现JWT校验需要将自己的过滤器添加进去。
@Configuration
public class ShiroConfig {
@Bean
public JWTRealm initUserRealm(){
return new JWTRealm();
}
@Bean
public SecurityManager initSecurityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(initUserRealm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter() throws UnsupportedEncodingException {
//实例化Shiro过滤器工厂
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//在工厂中注入安全管理器
shiroFilterFactoryBean.setSecurityManager(initSecurityManager());
//将我们自己的filter添加到Shiro
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("jwt",new JWTFilter());
//创建一个有序键值对用于存储黑白名单
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//anon表示无须登陆就能访问的资源地址
filterChainDefinitionMap.put("/user/login", "anon");
//其余所有请求地址均需要通过jwt校验
filterChainDefinitionMap.put("/**", "jwt");
//将黑白名单配置到shiro过滤器
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
}
5、授权
Shiro+JWT的授权过程和单独使用Shiro的授权过程是一样的。唯一的区别是在查询用户权限时,需要的用户ID不能从Session中获取的,而是从Subject取出存进去的token,将token进行解析得到其中的id。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//取出token
String token= (String) principalCollection.getPrimaryPrincipal();
//使用JWTUtils解析并获取subject
String id = null;
SimpleAuthorizationInfo info=null;
System.out.println("授权");
try {
id = JWTUtils.getId(token);
System.out.println(id);
//模拟查询数据库权限
info= new SimpleAuthorizationInfo();
info.addStringPermission("角色管理");
info.addStringPermission("新增角色");
info.addStringPermission("用户管理");
} catch (Exception e) {
e.printStackTrace();
}
return info;
}