# 权限管理
1. 什么是权限管理?
不同身份的用户进入到系统中能够完成的操作是不同的,我们对于不同的用户进行的可执行的操作的管理称之为权限管理。
# Shiro 简介
# 认证授权流程
认证:对用户的身份进行检查(登录验证)
授权:对用户的权限进行检查(是否有对应的操作权限)
# 安全框架
帮助我们完成用户身份认证及权限检查功能框架
常用的安全框架
- Shrio:Apache Shiro 是一个功能强大并且易用的 Java 安全框架(小而简单)
- spring Security:基于 Spring 的一个安全框架,依赖于 Spring
- OAuth2:第三方授权登录
- 自定义安全认证中心
# Shiro
Apache Shiro 是一个功能强大并且易用的 Java 安全框架
可以完成用户认证、授权、密码以及会话管理
可以在任何应用系统中使用(主要针对于单体项目的权限管理)
# Shiro 的工作原理
# Shiro 的核心功能
Authentication:认证,验证用户是否有相应的身份 - 登录验证;
Authorization:授权,即权限验证;对已经通过的用户检查是否具有某个权限或者角色,从而控制是否能进行某种操作
Session Management:会话管理,用户在认证成功之后创建会话,在没有退出之前,之前用户的所有信息都将会保存在这个会话中,可以是普通的 JavaSE 应用,也可以是 Web 应用;
Cryptography:加密,对敏感信息进行加密处理,shiro 就提供这种加密机制;
支持的特性:
- Web Support - Shiro 提供 l 过滤器,可以通过过滤器拦截 Web 请求处理 web 应用的访问控制
- Caching 缓存支持,shiro 可以缓存用户信息以及用户的角色授权信息,可以提高执行效率
- Concurrency shiro 支持多线程应用
- Testing 提供测试支持
- Run As 允许一个用户以另一种身份去访问
- Remember me
说明:Shiro 是一个安全框架,不提供用户以及权限的维护(用户权限的权限管理需要我们自己去设计)
# Shiro 核心组件
Shiro 三大核心组件:Subject、Security Manager、Realms
- Subject,表示待认证和授权的用户
- Security Manager,他是 Shiro 框架的核心,Shiro 就是通过 Security Manger 来进行内部实例的管理,并通过它来提供安全框架的各种服务
- Authenticator,认证器
- Anthorizer,授权器
- SessionManager,会话管理器
- CacheManager,缓存管理器
- Realms,相当于 Shiro 进行认证授权的数据源,充当了 Shiro 与安全数据之间的 “桥梁” 或者 “连接器”,也就是说,当用户进行认证(登录)和授权(访问控制)验证时,Shiro 会用应用配置的 Realm 中查找用户以及权限信息
# 基于 JavaSE 应用
# 创建 Maven 项目
# 导入 Shiro 依赖
<dependency> | |
<groupId>org.apache.shiro</groupId> | |
<artifactId>shiro-core</artifactId> | |
<version>1.4.1</version> | |
</dependency> |
# 创建 Shiro 配置文件
在 resource 目录下创建名为 shiro.ini 的文件
在文件中完成用户角色以及权限的配置
[users] | |
zhangsan=123456,seller | |
lisi=123456,ckmanager | |
admin=222222,admin | |
[roles] | |
admin=* | |
seller=order-add,order-del,order-list | |
ckmanager=ck-add,ck-del,ck-list |
Shiro 的基本使用
package com.hu.shiro; | |
import org.apache.shiro.SecurityUtils; | |
import org.apache.shiro.authc.UsernamePasswordToken; | |
import org.apache.shiro.mgt.DefaultSecurityManager; | |
import org.apache.shiro.realm.text.IniRealm; | |
import org.apache.shiro.subject.Subject; | |
import java.util.Scanner; | |
public class TestShiro { | |
public static void main(String[] args) { | |
Scanner scanner = new Scanner(System.in); | |
System.out.println("请输入账号"); | |
String username = scanner.nextLine(); | |
System.out.println("请输入密码"); | |
String password = scanner.nextLine(); | |
//1. 创建安全管理器 | |
DefaultSecurityManager securityManager = new DefaultSecurityManager(); | |
//2. 创建 realm | |
IniRealm iniRealm = new IniRealm("classpath:shiro.ini"); | |
//3. 将 realm 设置给安全管理器 | |
securityManager.setRealm(iniRealm); | |
//4. 将 realm 设置给 SecurityUtils 工具 | |
SecurityUtils.setSecurityManager(securityManager); | |
//5. 通过 SecurityUtils 工具过去 Subject 对象 | |
Subject subject = SecurityUtils.getSubject(); | |
// 认证流程 | |
//1. 将认证账号密码封装到 token 对象中 | |
UsernamePasswordToken token = new UsernamePasswordToken(username,password); | |
//2. 通过 subject 对象调用 login 方法进行认证申请 | |
boolean flag = false; | |
try { | |
subject.login(token); | |
flag = true; | |
}catch (Exception e){ | |
flag = false; | |
} | |
System.out.println(flag?"登录成功":"登录失败"); | |
// 授权 | |
// 判断是否有某个角色 | |
System.out.println(subject.hasRole("seller")); | |
// 判断是否有某个权限 | |
System.out.println(subject.isPermitted("order-del")); | |
} | |
} |
# Shiro 认证流程
1. 通过 subject.login (token) 进行登录验证,就会将 token 包含的用户信息(账号和密码)传递给 SecurityManager
2.SecurityManger 将会调用 Authenticator 进行身份验证
3.Authenticator 把 token 传递给对应的 Realm
4.Realm 根据得到的 token,调用 doGetAuthenticationInfo 方法进行认证(如果认证失败通过抛出异常提示认证器)
5. 将认证结果一层一层的返回到 subject(如果 subject.login 抛出异常则表示认证失败 )
# SpringBoot 应用整合 Shiro
# 创建 SpringBoot 应用
lombok
spring web
thymeleaf
# 整合 Druid 和 Mybatis
依赖
<!--druid--> | |
<dependency> | |
<groupId>com.alibaba</groupId> | |
<artifactId>druid-spring-boot-starter</artifactId> | |
<version>1.1.7</version> | |
</dependency> | |
<!--mysql--> | |
<dependency> | |
<groupId>mysql</groupId> | |
<artifactId>mysql-connector-java</artifactId> | |
<version>8.0.20</version> | |
</dependency> | |
<!--mybatis--> | |
<dependency> | |
<groupId>org.mybatis.spring.boot</groupId> | |
<artifactId>mybatis-spring-boot-starter</artifactId> | |
<version>2.1.1</version> | |
</dependency> |
配置
spring: datasource: druid: url: jdbc:mysql://localhost:3306/user driver-class-name: com.mysql.jdbc.Driver username: root password: 123456 initial-size: 1 min-idle: 1 max-active: 20 mybatis: mapper-locations: classpath:mapper/*.xml
3. 整合 Shiro
导入依赖
<!--shiro--> | |
<dependency> | |
<groupId>org.apache.shiro</groupId> | |
<artifactId>shiro-spring</artifactId> | |
<version>1.4.1</version> | |
</dependency> |
配置
SpringBoot 默认没有提供 Shiro 的自动配置
package com.example.demo.config; import org.apache.shiro.realm.text.IniRealm; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Controller; import java.util.HashMap; import java.util.Map;@Configuration
public class ShiroConfig {@Bean
public IniRealm getIniRealm(){ IniRealm iniRealm = new IniRealm("classpath:shiro.ini"); return iniRealm;}
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(IniRealm iniRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();//securityManager 要完成校验,需要 realm
securityManager.setRealm(iniRealm); return securityManager;}
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager){ ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();// 过滤器就是 Shiro 进行权限校验的核心,进行认证和授权是需要 SecurityManager 的
filter.setSecurityManager(securityManager);// 设置 shiro 的拦截规则
//anon 匿名用户可访问
//authc 认证用户可访问
//user 使用 RememberMe 的用户可访问
//perms 对应权限可访问
//role 对应角色可访问
Map<String,String> filterMap = new HashMap<>(); filterMap.put("/","anon"); filterMap.put("/login.html","anon"); filterMap.put("/user/login","anon"); filterMap.put("/static/**","anon"); filterMap.put("/**","authc"); filter.setFilterChainDefinitionMap(filterMap); filter.setLoginUrl("/login.html");// 设置未授权的访问路径
filter.setUnauthorizedUrl("/login.html"); return filter;}
}
认证测试
UserServiceImpl.java
package com.example.demo.service; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Service;@Service
public class UserServiceImpl { public void checkLogin(String username,String password) throws Exception{ Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username,password); subject.login(token);}
}
UserController
package com.example.demo.controller; import com.example.demo.service.UserServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping;@Controller
@RequestMapping("/user") public class UserController {@Autowired
private UserServiceImpl userService; @RequestMapping("/login") public String login(String username,String password){ try { userService.checkLogin(username,password); System.out.println("登录成功"); return "index"; } catch (Exception e) { System.out.println("登录失败"); return "login";}
}
}
login.html
<form action="user/login">
<p>账号:<input type="text" name="username"></p> <p>密码:<input type="text" name="password"></p> <p><input type="submit" value="登录"></p></form>
# SpringBoot 应用整合 Shiro - 案例(JdbcRealm)
# JdbcRealm 介绍
如果使用 JdbcRealm,则必须提供 JdbcRealm 所需的表结构(权限设计)
# JdbcRealm 规定的表结构
-- 创建用户表 create table users( id int primary key auto_increment, username varchar(20) not null unique, password varchar(20) not null, password_salt varchar(20) ); insert into users(username,password) values('zhangsan','123456'); insert into users(username,password) values('lisi','123456'); insert into users(username,password) values('wangwu','123456'); insert into users(username,password) values('zhaoliu','123456'); insert into users(username,password) values('chengqi','123456'); -- 用户角色表 create table user_roles( id int primary key auto_increment, username varchar(60) not null, role_name varchar(100) not null ); -- admin 系统管理员 -- cmanager 仓库人员 -- xmanager 销售人员 -- kmanager 客服人员 -- zmanager 行政人员 insert into user_roles(username,role_name) values('zhangsan','admin'); insert into user_roles(username,role_name) values('lisi','cmanager'); insert into user_roles(username,role_name) values('wangwu','xmanager'); insert into user_roles(username,role_name) values('zhaoliu','kmanager'); insert into user_roles(username,role_name) values('chengqi','zmanager'); -- 角色权限表 create table roles_permissions( id int primary key auto_increment, role_name varchar(100) not null, permission varchar(100) not null ); -- 管理员 insert into roles_permissions(role_name,permission) values('admin','*'); -- 仓库人员 insert into roles_permissions(role_name,permission) values('cmanager','sys:c:save'); insert into roles_permissions(role_name,permission) values('cmanager','sys:c:delete'); insert into roles_permissions(role_name,permission) values('cmanager','sys:c:update'); insert into roles_permissions(role_name,permission) values('cmanager','sys:c:find'); -- 销售人员 insert into roles_permissions(role_name,permission) values('xmanager','sys:x:save'); insert into roles_permissions(role_name,permission) values('xmanager','sys:x:delete'); insert into roles_permissions(role_name,permission) values('xmanager','sys:x:update'); insert into roles_permissions(role_name,permission) values('xmanager','sys:x:find'); -- 客服人员 insert into roles_permissions(role_name,permission) values('cmanager','sys:c:update'); insert into roles_permissions(role_name,permission) values('cmanager','sys:c:find'); -- 行政人员 insert into roles_permissions(role_name,permission) values('zmanager','sys:z:find');
# Springboot 整合 Shiro
创建 Springboot 应用
整合 Druid 和 Mybatis
整合 Shiro
添加依赖
配置 shiro
package com.example.demo.config; import org.apache.shiro.realm.jdbc.JdbcRealm; import org.apache.shiro.realm.text.IniRealm; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map;@Configuration
public class ShiroConfig {@Bean
public JdbcRealm getJdbcRealm(DataSource dataSource){ JdbcRealm jdbcRealm = new JdbcRealm();//JdbcRealm 会自动从数据查询用户及权限数据(数据库的表结构要符合 JdbcRealm 的规范)
jdbcRealm.setDataSource(dataSource);//JdbcRealm 默认开启认证功能,需要手动开启授权功能
jdbcRealm.setPermissionsLookupEnabled(true); return jdbcRealm;}
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(JdbcRealm jdbcRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();//securityManager 要完成校验,需要 realm
securityManager.setRealm(jdbcRealm); return securityManager;}
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager){ ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();// 过滤器就是 Shiro 进行权限校验的核心,进行认证和授权是需要 SecurityManager 的
filter.setSecurityManager(securityManager);// 设置 shiro 的拦截规则
//anon 匿名用户可访问
//authc 认证用户可访问
//user 使用 RememberMe 的用户可访问
//perms 对应权限可访问
//role 对应角色可访问
Map<String,String> filterMap = new HashMap<>(); filterMap.put("/","anon"); filterMap.put("/login.html","anon"); filterMap.put("/user/login","anon"); filterMap.put("/static/**","anon"); filterMap.put("/**","authc"); filter.setFilterChainDefinitionMap(filterMap);//filter.setLoginUrl("/");
// 设置未授权的访问路径
filter.setUnauthorizedUrl("/login.html"); return filter;}
}
七、Shiro 的标签使用
当用户认证进入到主页之后,需要显示用户信息以及当前用户的权限信息,Shiro 就提供了一套标签用于页面来进行权限数据的呈现
Shiro 提供了可供 JSP 使用的标签以及 thymeleaf 中标签
JSP 页面引用:
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
thymeleaf 模板中引用
在 pom.xml 文件中导入 thymeleaf 模板对 shiro 标签支持的依赖
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
在 ShiroConfig 配置 Shiro 的
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
thymeleaf 模板中引入 Shiro 的命名空间
<html xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro ">
</html>
常用标签
guest,判断用户是否是游客身份,如果是游客身份则显示此标签内容
<shiro:guest>
欢迎游客访问,<a href="login.html">登录</a>
</shiro:guest>
user,判断用户是否是认证身份,如果是认证身份则显示此标签内容
principal,获取当前登录用户名
<shiro:user>
用户[<shiro:principal/>]欢迎您!
</shiro:user>
noAuthenticated/authenticated
hasRole
hasPermission
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro ">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
index
<hr/>
<shiro:guest>
欢迎游客访问,<a href="login.html">登录</a>
</shiro:guest>
<shiro:user>
用户[<shiro:principal/>]欢迎您!
当前用户为<shiro:hasRole name="admin">超级管理员</shiro:hasRole>
<shiro:hasRole name="cmanager">仓库人员</shiro:hasRole>
<shiro:hasRole name="xmanager">销售人员</shiro:hasRole>
<shiro:hasRole name="kmanager">客服人员</shiro:hasRole>
<shiro:hasRole name="zmanager">行政人员</shiro:hasRole>
</shiro:user>
<hr/>
仓库管理
<ul>
<shiro:hasPermission name="sys:c:save"><li><a href="#">入库</a></li></shiro:hasPermission>
<shiro:hasPermission name="sys:c:delete"><li><a href="#">出库</a></li></shiro:hasPermission>
<shiro:hasPermission name="sys:c:update"><li><a href="#">修改</a></li></shiro:hasPermission>
<shiro:hasPermission name="sys:c:find"><li><a href="#">查询</a></li></shiro:hasPermission>
</ul>
订单管理
<ul>
<shiro:hasPermission name="sys:x:save"><li><a href="#">添加订单</a></li></shiro:hasPermission>
<shiro:hasPermission name="sys:x:delete"><li><a href="#">删除订单</a></li></shiro:hasPermission>
<shiro:hasPermission name="sys:x:update"><li><a href="#">修改订单</a></li></shiro:hasPermission>
<shiro:hasPermission name="sys:x:find"><li><a href="#">查询订单</a></li></shiro:hasPermission>
</ul>
客户管理
<ul>
<shiro:hasPermission name="sys:k:save"><li><a href="#">添加客户</a></li></shiro:hasPermission>
<shiro:hasPermission name="sys:k:delete"><li><a href="#">删除客户</a></li></shiro:hasPermission>
<shiro:hasPermission name="sys:k:update"><li><a href="#">修改客户</a></li></shiro:hasPermission>
<shiro:hasPermission name="sys:k:find"><li><a href="#">查询客户</a></li></shiro:hasPermission>
</ul>
</body>
</html>
# Springboot 整合 Shiro 完成权限管理案例 — 自定义 Realm
使用 JdbcRealm 可以完成用户权限管理,但是我们必须提供 Jdbc 规定的数据表结构,如果我们的项目开发中,这个 JdbcReal 规定的数据表结构不能满足开发需求,如何处理?
- 自定义数据库表结构
- 自定义 Realm 实现认证和授权
# 数据库设计
-- 用户信息表 create table user( id int primary key auto_increment, username varchar(60) not null unique, password varchar(20) not null, password_salt varchar(60) ); insert into user(username,password) values('zhangsan','123456'); insert into user(username,password) values('lisi','123456'); insert into user(username,password) values('wangwu','123456'); insert into user(username,password) values('zhaoliu','123456'); insert into user(username,password) values('chengqi','123456'); -- 角色信息表 create table role( id int primary key auto_increment, name varchar(60) not null ); insert into role(name) values('admin'); insert into role(name) values('cmanager');-- 仓库 insert into role(name) values('xmanager');-- 销售 insert into role(name) values('kmanager');-- 客服 insert into role(name) values('zmanager');-- 行政 -- 权限信息表 create table permission( id int primary key auto_increment, `code` varchar(60) not null, name varchar(60) ); insert into permission(code,name) values('sys:c:save','入库'); insert into permission(code,name) values('sys:c:delete','出库'); insert into permission(code,name) values('sys:c:update','修改'); insert into permission(code,name) values('sys:c:find','查询'); insert into permission(code,name) values('sys:x:save','新增订单'); insert into permission(code,name) values('sys:x:delete','删除订单'); insert into permission(code,name) values('sys:x:update','修改订单'); insert into permission(code,name) values('sys:x:find','查询订单'); insert into permission(code,name) values('sys:k:save','新增客户'); insert into permission(code,name) values('sys:k:delete','删除客户'); insert into permission(code,name) values('sys:k:update','修改客户'); insert into permission(code,name) values('sys:k:find','查询客户'); -- 用户角色表 create table user_role( uid int not null, rid int not null -- primary key(uid,rid), -- constraint FK_user foreign key(uid) references user(id), -- constraint FK_role foreign key(rid) references role(id), ) insert into user_role(uid,rid) values(1,1); insert into user_role(uid,rid) values(1,2); insert into user_role(uid,rid) values(1,3); insert into user_role(uid,rid) values(1,4); insert into user_role(uid,rid) values(1,5); insert into user_role(uid,rid) values(2,2); insert into user_role(uid,rid) values(3,3); insert into user_role(uid,rid) values(4,4); insert into user_role(uid,rid) values(5,5); -- 角色权限表 create table role_permission( rid int not null, pid int not null ) -- 给仓库角色分配权限 insert into role_permission(rid,pid) values(2,1); insert into role_permission(rid,pid) values(2,2); insert into role_permission(rid,pid) values(2,3); insert into role_permission(rid,pid) values(2,4); -- 给销售角色分配权限 insert into role_permission(rid,pid) values(3,5); insert into role_permission(rid,pid) values(3,6); insert into role_permission(rid,pid) values(3,7); insert into role_permission(rid,pid) values(3,8); insert into role_permission(rid,pid) values(3,9); insert into role_permission(rid,pid) values(3,10); insert into role_permission(rid,pid) values(3,11); insert into role_permission(rid,pid) values(3,12); -- 给客服角色分配权限 insert into role_permission(rid,pid) values(4,11); insert into role_permission(rid,pid) values(4,12); -- 给行政角色分配权限 insert into role_permission(rid,pid) values(5,4); insert into role_permission(rid,pid) values(5,8); insert into role_permission(rid,pid) values(5,12);
# DAO 实现
Shiro 进行认证需要用户信息
- 根据用户名查询用户信息
Shiro 进行授权管理需要当前用户的角色和权限
- 根据用户名查询当前用户的角色列表(3 张表连接查询)
- 根据用户名查询当前用户的权限列表(5 张表连接查询)
# 创建 Springboot 项目,整合 Mybatis
# 更具用户名查用户信息
创建 Bean
@Data
public class User {
private Integer id;
private String username;
private String password;
private String pwdSalt;
}
创建 Dao
public interface UserDao {
User queryUserByUsername(String username) throws Exception;
}
映射配置
<resultMap id="userMap" type="User">
<id column="id" property="id"></id>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="password_salt" property="pwdSalt"/>
</resultMap>
<select id="queryUserByUsername" resultMap="userMap">
select * from user where username = #{username};
</select>
# 根据用户名查询角色名列表
创建 Dao
public interface RoleDao {
Set<String> queryRoleNameByUsername(String username) throws Exception;
}
映射配置
<select id="queryRoleNameByUsername" resultSets="java.util.set" resultType="String">
SELECT r.name FROM `user` u
LEFT JOIN user_role ur ON u.id = ur.uid
LEFT JOIN role r ON ur.rid = r.id
WHERE u.username = #{username}
</select>
# 整合 Shiro
导入依赖
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
配置 Shiro - 基于 Java 配置方式
自定义 Realm
package com.example.demo.config;
import com.example.demo.beans.User;
import com.example.demo.dao.PermissionDao;
import com.example.demo.dao.RoleDao;
import com.example.demo.dao.UserDao;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import javax.annotation.Resource;
import java.util.Set;
/**
* 1. 创建一个继承 AuthorizingRealm 类(实现 Realm 接口的类)
* 2. 重新 doGetAuthorizationInfo 和 doGetAuthenticationInfo 方法
* 3. 重新 getName 方法
*/
public class MyRealm extends AuthorizingRealm {
@Resource
private UserDao userDao;
@Resource
private RoleDao roleDao;
@Resource
private PermissionDao permissionDao;
@Override
public String getName() {
return "myRealm";
}
/**
* 获取授权数据 (将当前用户的角色以及权限信息查询出来)
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取用户的用户名
String username = (String) principalCollection.iterator().next();
// 根据用户名查询当前用户角色列表
Set<String> roleNames = roleDao.queryRoleNameByUsername(username);
// 根据用户名查询当前用户权限列表
Set<String> permission = permissionDao.queryPermissionByUsername(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roleNames);
info.setStringPermissions(permission);
return info;
}
/**
* 获取认证数据(从数据库查询的用户的正确数据)
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 参数 authenticationToken 就是传递的 subject.login (token)
// 从 token 获取用户名
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
// 根据用户名,从数据库查询当前用户的安全数据
User user = userDao.queryUserByUsername(username);
if (user == null){
return null;
}
AuthenticationInfo info = new SimpleAuthenticationInfo(
username, // 当前用户名
user.getPassword(), // 从数据库查询出来的安全密码
getName());
return info;
}
}
# 加密
# 加密介绍
明文 ——(加密规则)—— 密文
加密规则可以自定义,在项目开发中我们通常使用 BASE64 和 MD5 编码方式
BASE64:可反编码的编码方式
明文 —— 密文
密文 —— 明文
MD5:不可逆的编码方式(非对称)
明文 —— 密文
如果数据库用户的密码存储的密文,Shiro 改如何验证
使用 Shiro 提供的加密功能,对输入的密码进行加密之后再进行认证
# Shiro 使用加密认证
配置 Shiro
package com.example.demo.config; import at.pollux.thymeleaf.shiro.dialect.ShiroDialect; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.realm.jdbc.JdbcRealm; import org.apache.shiro.realm.text.IniRealm; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map;@Configuration
public class ShiroConfig {//...
@Bean
public HashedCredentialsMatcher getHashedCredentialsMatcher(){ HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();//matcher 就是用来指定加密规则
matcher.setHashAlgorithmName("md5");//hash 次数
matcher.setHashIterations(1);// 次数循环次数要与用户注册时密码加密次数一致 return matcher;}
/**
* 自定义 Realm */@Bean
public MyRealm getMyRealm(HashedCredentialsMatcher matcher){ MyRealm realm = new MyRealm(); realm.setCredentialsMatcher(matcher); return realm;}
//...
}
# 用户注册密码加密处理
register.html
<form action="user/register">
<p>账号:<input type="text" name="username"></p> <p>密码:<input type="text" name="password"></p> <p><input type="submit" value="注册"></p></form>
UserController.java
package com.example.demo.controller; import com.example.demo.service.UserServiceImpl; import org.apache.shiro.crypto.hash.Md5Hash; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import java.util.Random;@Controller
@RequestMapping("/user") public class UserController {@Autowired
private UserServiceImpl userService; @RequestMapping("/register") public String register(String username,String password){ System.out.println("注册");// 注册的时候要对密码进行加密存储
Md5Hash md5Hash = new Md5Hash(password); System.out.println(md5Hash.toHex());// 加盐加密
int num = new Random().nextInt(90000)+10000;//10000-99999 System.out.println("salt:"+num); Md5Hash md5Hash2 = new Md5Hash(password,num+""); System.out.println(md5Hash2);// 加盐加密 + 多次 hash
Md5Hash md5Hash3 = new Md5Hash(password,num+"",3); System.out.println(md5Hash3);// 将用户信息保存到数据库时,保存加密后的米,如果生成的随机盐,盐也要保存
return "login";}
}
# 如果密码进行了加密处理,则 Realm 再但会认证数据时需要返回盐
MyRealm.java
package com.example.demo.config; import com.example.demo.beans.User; import com.example.demo.dao.PermissionDao; import com.example.demo.dao.RoleDao; import com.example.demo.dao.UserDao; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import javax.annotation.Resource; import java.util.Set;/**
* 1. 创建一个继承 AuthorizingRealm 类(实现 Realm 接口的类) * 2. 重新 doGetAuthorizationInfo 和 doGetAuthenticationInfo 方法 * 3. 重新 getName 方法 */ public class MyRealm extends AuthorizingRealm {@Resource
private UserDao userDao;@Resource
private RoleDao roleDao;@Resource
private PermissionDao permissionDao;//...
/**
* 获取认证数据(从数据库查询的用户的正确数据) * @param authenticationToken * @return * @throws AuthenticationException */@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {// 参数 authenticationToken 就是传递的 subject.login (token)
// 从 token 获取用户名
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername();// 根据用户名,从数据库查询当前用户的安全数据
User user = userDao.queryUserByUsername(username); if (user == null){ return null;}
/*AuthenticationInfo info = new SimpleAuthenticationInfo (
username, // 当前用户名 user.getPassword (), // 从数据库查询出来的安全密码 getName ());*/// 如果数据库是加了盐的
AuthenticationInfo info = new SimpleAuthenticationInfo( username, // 当前用户名 user.getPassword(), // 从数据库查询出来的安全密码 ByteSource.Util.bytes(user.getPwdSalt()), getName()); return info;}
}
# 退出登录
在 shiro 过滤器中进行配置,配置 logOut 对应的路径
filterMap.put("/exit","logout"); |
在页面的退出按钮上,跳转到 exit 对应的 url
<a href="exit">退出</a> |
# 授权
用户登录成功之后,要进行的操作就需要有对应的权限,在进行操作之前对权限进行检查 — 授权
权限控制通常有两种做法:
- 不同身份的用户登录,我们显示不同的操作菜单(没有权限的菜单不显示)
- 对所有用户显示所有菜单,当用户点击菜单以后再验证当前用户是否有次权限,如果没有则提示权限不足
# html 授权
在菜单页面只显示当前用户拥有权限操作的菜单
shiro 标签
<shiro:hasPermission name="sys:c:save"><dd><a href="c_add.html" >入库</a></dd></shiro:hasPermission>
# 过滤器授权
在 shiro 过滤器中请求的 url 进行权限设置
filterMap.put("/c_add.html","perms[sys:c:save]");// 设置未授权的访问路径
filter.setUnauthorizedUrl("/lesspermission.html");
# 注解授权
配置 Spring 对 Shiro 的支持,ShiroConfig.java
@Bean
public DefaultAdvisorAutoProxyCreator getAdvisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); autoProxyCreator.setProxyTargetClass(true); return autoProxyCreator;}
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){ AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor;}
在请求的控制器添加权限注解
package com.example.demo.controller; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping;@Controller
@RequestMapping("/customer") public class CustomerController { @RequestMapping("/list")/**
* 如果没有 sys:k:find 的权限,则不需要执行此方法 */ @RequiresPermissions("sys:k:find") public String list(){ return "customer_list";}
}
通过全局异常处理,指定权限不足时的页面跳转
package com.example.demo.utils; import org.apache.shiro.authz.AuthorizationException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler;@ControllerAdvice
public class GlobalExceptionhandler {@ExceptionHandler
public String doException(Exception e){ if (e instanceof AuthorizationException){ return "lesspermission";}
return null;}
}
# 手动授权
在代码中进行手动的权限校验
Subject subject = SecurityUtils.getSubject(); if (subject.isPermitted("sys:k:find")){ return "customer_list"; }else { return "lesspermission";}
# 缓存使用
使用 Shiro 进行权限管理过程中,每次授权都会访问 realm 中的 doGetAuthenticationInfo 方法查询当前用户的角色及权限信息,如果系统的用户量比较大则会对数据库造成比较大的压力
Shiro 支持缓存以降低对数据库的访问压力(缓存的时授权信息)
# 缓存的使用
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId></dependency>
<dependency>
<groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId></dependency>
<dependency>
<groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.4.0</version></dependency>
# 配置缓存策略
在 resources 目录下创建一个 xml 文件,ehcache.xml
<?xml version="1.0" encoding="UTF-8" ?>
<ehcache updateCheck="false" dynamicConfig="false">
<diskStore path="C:\TEMP" />
<cache name="user" timeToLiveSeconds="300" maxEntriesLocalHeap="1000"/>
<defaultCache name="defaultCache"
maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="false" maxElementsOnDisk="100000" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"/><!-- 缓存淘汰策略:当缓存空间比较紧张时,我们要存储新的数据进来,就必然删除一些老的数据
LRU 最近最少使用 FIFO 先进先出 LFU 最少使用 --></ehcache>
# 加入缓存
ShiroConfig
@Bean
public EhCacheManager getEhCacheManager(){ EhCacheManager ehCacheManager = new EhCacheManager(); ehCacheManager.setCacheManagerConfigFile("classpth:ehcache.xml"); return ehCacheManager;}
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(MyRealm realm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();//securityManager 要完成校验,需要 realm
securityManager.setRealm(realm); securityManager.setCacheManager(getEhCacheManager()); return securityManager;}
# session 管理
Shiro 进行认证和授权时基于 session
如果我们需要对 session 进行管理
- 自定义 session 管理器
- 将自定义的 session 管理器设置给 SecurityManager
配置自定义 SessionManager:ShiroConfig.java
@Bean
public DefaultWebSessionManager getDefaultWebSessionManager(){ DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); System.out.println(sessionManager.getGlobalSessionTimeout());//1800000// 配置 sessionManager
sessionManager.setGlobalSessionTimeout(15*1000); return sessionManager;}
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(MyRealm realm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();//securityManager 要完成校验,需要 realm
securityManager.setRealm(realm);// 设置缓存
securityManager.setCacheManager(getEhCacheManager());// 设置 session
securityManager.setSessionManager(getDefaultWebSessionManager()); return securityManager;}
# RememberMe
将用户对页面的访问权限分为三个级别
- 未认证 — 可访问的页面
- login.html、register.html
- 曾认证 — 可访问的页面
- info.html
- 已认证 — 可访问的页面
- 转账.html
# 在过滤器中设置 “记住我” 可访问的 url
//anon 匿名用户可访问 | |
//authc 认证用户可访问 | |
//user 使用 RememberMe 的用户可访问 | |
//perms 对应权限可访问 | |
//logout 退出指定的 url | |
Map<String,String> filterMap = new HashMap<>(); | |
filterMap.put("/","anon"); | |
filterMap.put("/login.html","anon"); | |
// filterMap.put("/index.html","anon"); | |
filterMap.put("/index.html","user"); | |
filterMap.put("/register.html","anon"); | |
filterMap.put("/user/login","anon"); | |
filterMap.put("/user/register","anon"); | |
filterMap.put("/static/**","anon"); | |
filterMap.put("/**","authc"); | |
filterMap.put("/c_add.html","perms[sys:c:save]"); | |
filterMap.put("/exit","logout"); |
# 在 ShiroConfig.java 中配置基于 Cookie 的 rememberMe 管理器
@Bean
public CookieRememberMeManager getCookieRememberMeManager(){ CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();//cookie 必须设置 name
SimpleCookie cookie = new SimpleCookie("rememberMe"); cookie.setMaxAge(30*24*3600); rememberMeManager.setCookie(cookie); return rememberMeManager;}
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(MyRealm realm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();//securityManager 要完成校验,需要 realm
securityManager.setRealm(realm);// 设置缓存
securityManager.setCacheManager(getEhCacheManager());// 设置 session
securityManager.setSessionManager(getDefaultWebSessionManager());// 设置 rememberMe 管理器
securityManager.setRememberMeManager(getCookieRememberMeManager()); return securityManager;}
# 登录认证时设置 token “记住我”
登录页面
<form action="user/login">
<p>账号:<input type="text" name="username"></p> <p>密码:<input type="text" name="password"></p> <p>记住我:<input type="checkbox" name="rememberMe"></p> <p><input type="submit" value="登录"></p></form>
控制器
package com.example.demo.controller; import com.example.demo.service.UserServiceImpl; import org.apache.shiro.crypto.hash.Md5Hash; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import java.util.Random;@Controller
@RequestMapping("/user") public class UserController {@Autowired
private UserServiceImpl userService; @RequestMapping("/login") public String login(String username,String password,boolean rememberMe){ try { userService.checkLogin(username,password,rememberMe); System.out.println("登录成功"); return "index.html"; } catch (Exception e) { System.out.println("登录失败"); return "login";}
}
//...
}
service
package com.example.demo.service; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Service;@Service
public class UserServiceImpl { public void checkLogin(String username, String password, boolean rememberMe) throws Exception{ Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username,password); token.setRememberMe(rememberMe); subject.login(token);}
}
# Shiro 多 Realm 配置
# 使用场景
当 Shiro 进行权限管理,数据来自不同的数据源时,我们可以给 SecurityManager 配置多个 Realm
# 多个 Realm 的处理方式
1. 链式处理
- 多个 Realm 一次进行认证
2. 分支处理
- 根据不同的条件从多个 Realm 中选择一个进行认证处理
# 多 Realm 配置(链式处理)
定义多个 Realm
UserRealm.java
package com.example.demo.realm; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection;@Slf4j
public class UserRealm extends AuthorizingRealm {@Override
public String getName() { return "UserRealm";}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null;}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { log.info("-------UserRealm--------"); UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,"123456",getName()); return info;}
}
ManagerRealm.java
package com.example.demo.realm; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection;@Slf4j
public class ManagerRealm extends AuthorizingRealm {@Override
public String getName() { return "ManagerRealm";}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null;}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { log.info("-------ManagerRealm--------"); UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,"222222",getName()); return info;}
}
在 ShiroConfig.java 中为 Securitymanager 配置多个 Realm
package com.example.demo.config; import com.example.demo.realm.ManagerRealm; import com.example.demo.realm.UserRealm; import org.apache.shiro.realm.Realm; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map;@Configuration
public class ShiroConfig {@Bean
public UserRealm getUserRealm(){ return new UserRealm();}
@Bean
public ManagerRealm getManagerRealm(){ return new ManagerRealm();}
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();//securityManager 要完成校验,需要 realm
Collection<Realm> realms = new ArrayList<>(); realms.add(getUserRealm()); realms.add(getManagerRealm()); securityManager.setRealms(realms); return securityManager;}
//....
}
测试代码
login.html
<form action="user/login">
<p>账号:<input type="text" name="username"></p> <p>密码:<input type="text" name="password"></p><input type="submit" value="登录">
</form>
UserController.java
package com.example.demo.controller; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping;@Controller
@RequestMapping("user")@Slf4j
public class UserController { @RequestMapping("/login") public String login(String username,String password){ log.info("----UserController---"); try { UsernamePasswordToken token = new UsernamePasswordToken(username,password); Subject subject = SecurityUtils.getSubject(); subject.login(token); return "index"; }catch (Exception e){ return "login";}
}
}
# 多 Realm 配置(分支处理)
根据不同的条件执行不同的 Realm
实现 案例:用户不同身份登录执行不同的 Realm
自定义 Realm(UserRealm、ManagerRealm)
- 当登录页面选择 “普通用户” 登录,则执行 UserRealm 的认证
- 当登录页面选择 “管理员” 登录,则执行 ManagerRealm 的认证
Realm 的声明及配置
自定义 Token
package com.example.demo.config; import org.apache.shiro.authc.UsernamePasswordToken; public class MyToken extends UsernamePasswordToken { private String loginType; public MyToken(String username,String password,String loginType){ super(username,password); this.loginType = loginType;}
public String getLoginType() { return loginType;}
public void setLoginType(String loginType) { this.loginType = loginType;}
}
自定义认证器
package com.example.demo.config; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.pam.ModularRealmAuthenticator; import org.apache.shiro.realm.Realm; import java.util.ArrayList; import java.util.Collection;@Slf4j
public class MymodularRealmAuthenticator extends ModularRealmAuthenticator {@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { log.info("-----MymodularRealmAuthenticator---------"); Collection<Realm> realms = this.getRealms(); MyToken myToken = (MyToken) authenticationToken; String loginType = myToken.getLoginType(); log.info("-------------loginType:"+loginType); Collection<Realm> typeRealms = new ArrayList<>(); for (Realm realm : realms){ if (realm.getName().startsWith(loginType)){ typeRealms.add(realm);}
}
if (typeRealms.size() == 1){ return this.doSingleRealmAuthentication((Realm) typeRealms.iterator().next(),authenticationToken); }else { return this.doMultiRealmAuthentication(typeRealms,authenticationToken);}
}
}
配置自定义认证器
package com.example.demo.config; import com.example.demo.realm.ManagerRealm; import com.example.demo.realm.UserRealm; import org.apache.shiro.realm.Realm; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map;@Configuration
public class ShiroConfig {@Bean
public UserRealm getUserRealm(){ return new UserRealm();}
@Bean
public ManagerRealm getManagerRealm(){ return new ManagerRealm();}
@Bean
public MymodularRealmAuthenticator getMymodularRealmAuthenticator(){ return new MymodularRealmAuthenticator();}
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();// 配置自定义认证器 (防在 realms 设置之前)
securityManager.setAuthenticator(getMymodularRealmAuthenticator());//securityManager 要完成校验,需要 realm
Collection<Realm> realms = new ArrayList<>(); realms.add(getUserRealm()); realms.add(getManagerRealm()); securityManager.setRealms(realms); return securityManager;}
//...
}