一、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
三、使用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>
new Vue({
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、在领域类中完善授权方法 查询数据库将该用户的所有权限查询出来,将这些权限信息封装到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);
}
下一章-Shiro+JWT 无论你在学习上有任何问题,重庆蜗牛学院欢迎你前来咨询,联系QQ:296799112