文章目录
- 写在前面
- 一、Shrio
- 1.1. 什么是Apache Shiro?
- 1.2. Shiro有哪些功能
- 1.3 主要概念
- Subject
- SecurityManager
- Realms
- 1.4. Shiro的核心体系结构概念
- 二、Shiro-Springboot代码准备工作
- 2.1 准备测试使用的SQL语句
- 2.2 项目需要的依赖
- 2.3 mybatis-plus快速生成代码
- 2.4 在Shiro中继承了MD5的算法,所以可以直接使用它来实现对密码进行加密测试
- 2.4.1 配置配置信息
- 2.4.2 使用Shrio自带的MD5加密,生成密码
- 2.5 自定义Realm做准备
- 2.5.1 创建根据用户名字查找用户
- 2.5.2 根据登录用户的id查询到其拥有的所有角色的编码
- 2.5.3 根据登录用户的id查询到其拥有的所有权限表达式
- 2.5.4 打包遇到的问题,.xml放置在java目录下,而不是resource目录下
- 2.6 自定义Realm
- 2.7 自定义 FormAuthenticationFilter修改对认证结果的处理
- 2,7,1 Json结果封装类, 主要用于返回给前端数据
- 2.7.2 修改默认认证过滤器中对认证结果的处理
- 2.8 Shiro的spring配置类
- 2.8.1 Shiro配置类
- 2.8.2 设置登录Controller和登录成功Controller
- 2.8.3 认证登录测试
- 2.9 授权
- 2.10 访问的controller
- 2.11 统一异常处理
- 2.12 完整流程测试
- 写在最后
写在前面
官方文档:http://shiro.apache.org/introduction.html 别看我很牛逼,其实下面这些理论都是我从官网抄录下来的
前半段是理论,不敢兴趣的人,直接跳过。。。看后面的实际操作,所谓程序员先做后学,才记忆深刻。
本文较长,希望各位同学耐心看完,最好跟我一起制作demo
本文重在讲授权、认证流程,其他功能,我们在学会了基础之后,再慢慢深入了解
一、Shrio
1.1. 什么是Apache Shiro?
Apache Shiro是一个功能强大且灵活的开源安全框架,可以干净地处理身份验证,授权,企业会话管理和加密。
1.2. Shiro有哪些功能
Shiro的目标是Shiro开发团队所称的“应用程序安全的四个基石”——认证、授权、会话管理和加密:
- (一)Authentication 身份验证:有时被称为“登录”,这是一种证明用户是他们所说的那个人的行为。
- (二)Authorization 授权:访问控制的过程,即决定“谁”可以访问“什么”。
- (三)Session Management 会话管理:管理特定于用户的会话,甚至在非web或EJB应用程序中也是如此。
- (四)Cryptography 密码学:保持数据安全使用加密算法,同时仍然易于使用。
在不同的应用程序环境中,还有一些额外的特性来支持和加强这些关注,特别是:
- (一)、Web Support Web支持:Shiro的Web支持api帮助轻松地保护Web应用程序。
- (二)、Caching 缓存:在Apache Shiro的API中,缓存是第一层公民,以确保安全操作保持快速和高效。
- (三)Concurrency 并发性:Apache Shiro支持具有并发性特性的多线程应用程序。
- (四)Testing 测试:存在测试支持来帮助您编写单元和集成测试,并确保您的代码将如预期的那样得到保护。
- (五)“Run As” “以用户身份运行”:允许用户假设另一个用户的身份(如果允许的话)的特性,有时在管理场景中非常有用。
- (六)“Remember Me” “记住我”:在会话中记住用户的身份,这样他们只需要在强制登录时登录。
1.3 主要概念
以上说完Shiro的主要功能之后,我们再来看看Shiro有那些概念
在最高的概念层次上,Shiro的架构有3个主要概念:主题、安全管理器和领域。下图是这些组件如何交互的高级概述,我们将介绍下面的每个概念:
Subject
Subject实例都绑定到(并且需要)一个SecurityManager。当您与一个Subject交互时,这些交互转换为与SecurityManager的特定于Subject的交互。
SecurityManager
SecurityManager是Shiro架构的核心,充当一种“保护伞”对象,协调其内部安全组件,这些组件一起形成一个对象图。但是,一旦为应用程序配置了SecurityManager及其内部对象图,它通常就会被搁置,应用程序开发人员几乎把所有时间都花在Subject API上。
Realms
从这个意义上说,领域本质上是一个特定于安全性的DAO:它封装了数据源的连接细节并使之具有asso特性根据需要向Shiro提供关联数据。在配置Shiro时,必须指定至少一个用于身份验证和/或授权的领域。SecurityManager可以配置多个域,但至少需要一个域。
1.4. Shiro的核心体系结构概念
涉及的关键词有:
- Subject 主题(org.apache.shiro.subject.Subject)
- SecurityManager (org.apache.shiro.mgt.SecurityManager)
- Authenticator 身份验证(org.apache.shiro.authc.Authenticator)
- Authentication Strategy 身份验证策略(org.apache.shiro.authc.pam.AuthenticationStrategy)
- Authorizer 授权人(org.apache.shiro.authz.Authorizer)
- SessionManager (org.apache.shiro.session.mgt.SessionManager)
- SessionDAO (org.apache.shiro.session.mgt.eis.SessionDAO)
- CacheManager 缓存管理器(org.apache.shiro.cache.CacheManager)
- Cryptography 加密(org.apache.shiro.crypto。*)
- Realms领域(org.apache.shiro.realm.Realm)
上面的概念图还是东西有点多,我来简化一下
二、Shiro-Springboot代码准备工作
2.1 准备测试使用的SQL语句
直接复制到数据库软件运行建表就可以。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`age` int(11) NULL DEFAULT NULL,
`admin` bit(1) NULL DEFAULT NULL,
`dept_id` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 66 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
INSERT INTO `employee` VALUES (1, 'admin', 'e00cf25ad42683b3df678c61f42c6bda', '123@163.com', 20, b'1', 2);
INSERT INTO `employee` VALUES (2, '鲁班', 'd3b284c242a1c1fa0615e8d0bdf234bb', '123@163.com', 35, b'0', 1);
INSERT INTO `employee` VALUES (3, '赵云', 'a46fbb88724b575acc9bf5e164ae4ac3', '123@163.com', 25, b'0', 1);
DROP TABLE IF EXISTS `employee_role`;
CREATE TABLE `employee_role` (
`employee_id` bigint(20) NULL DEFAULT NULL,
`role_id` bigint(20) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
INSERT INTO `employee_role` VALUES (1, 1);
INSERT INTO `employee_role` VALUES (2, 2);
INSERT INTO `employee_role` VALUES (3, 3);
INSERT INTO `employee_role` VALUES (3, 4);
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`expression` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 21 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
INSERT INTO `permission` VALUES (1, '远程攻击', 'department:list');
INSERT INTO `permission` VALUES (2, '近身攻击', 'department:');
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`sn` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
INSERT INTO `role` VALUES (1, '麻花藤', 'BOSS');
INSERT INTO `role` VALUES (2, '射手', 'SS');
INSERT INTO `role` VALUES (3, '战士', 'ZS');
INSERT INTO `role` VALUES (4, '刺客', 'CK');
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
`role_id` bigint(20) NULL DEFAULT NULL,
`permission_id` bigint(20) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
INSERT INTO `role_permission` VALUES (1, 1);
INSERT INTO `role_permission` VALUES (1, 2);
INSERT INTO `role_permission` VALUES (2, 1);
INSERT INTO `role_permission` VALUES (3, 2);
INSERT INTO `role_permission` VALUES (4, 2);
SET FOREIGN_KEY_CHECKS = 1;
2.2 项目需要的依赖
- 2.2.1 创建一个springboot的项目
注意:为了节省大家的时间,请严格按照的我包路径进行配置,下面有自动生成代码的方式,如果一样的包路径,就不需要进行多余的修改。
包路径为:com.cancan.shirospringboot - 2.2.2 pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
<!--mybatis-plus自动的维护了mybatis以及mybatis-spring的依赖,
在springboot中这三者不能同时的出现,避免版本的冲突,表示:跳进过这个坑-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.1</version>
</dependency>
<!-- 引入Druid依赖,阿里巴巴所提供的数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.29</version>
</dependency>
<!-- 提供mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.1-android</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
2.3 mybatis-plus快速生成代码
/**
* 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
*
* @author cancan
* @since 2020-03-23
*/
public class CodeGenerator {
/**
* 配置模块与表
*/
private static Map<String,List<String>> moduleTableMap = Maps.newHashMap();
static{
//key为当前所有文件放置的包名,如果为空指的就是packageConfig父类目录下,value为在该包名下生成的表,可以多张表,存放在list
moduleTableMap.put("", Arrays.asList("employee","permission","role"));
}
public static void main(String[] args) {
autoGenerator();
}
private static void autoGenerator(){
if(MapUtils.isNotEmpty(moduleTableMap)){
for(Map.Entry<String,List<String>> entry : moduleTableMap.entrySet()){
List<String> tables = entry.getValue();
if(CollectionUtils.isNotEmpty(tables)){
tables.stream().forEach(e->{
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
//全局配置
String projectPath = globalConfig(mpg);
//数据库配置
dataSourceConfig(mpg);
//包配置
PackageConfig pc =packageConfig(mpg,entry.getKey());
injectionConfig(mpg,pc,projectPath);
strategyConfig(mpg,pc,e);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
});
}
}
}else{
throw new RuntimeException("请配置模块");
}
}
/**
* 设置全局配置.
*
* @param mpg 自动代码生成对象
* @return 设置全局配置
*/
private static String globalConfig(AutoGenerator mpg){
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("cancan");
gc.setOpen(false);
gc.setDateType(DateType.ONLY_DATE);
gc.setIdType(IdType.UUID);
mpg.setGlobalConfig(gc);
return projectPath;
}
/**
* 数据源配置
* @param mpg 自动代码生成对象
*/
private static void dataSourceConfig(AutoGenerator mpg){
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=utf-8&serverTimezone=UTC");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("admin");
mpg.setDataSource(dsc);
}
/**
* 设置包配置.
*
* @param mpg 自动代码生成对象
* @param moduleName 当前包的名字
* @return 包的配置
*/
private static PackageConfig packageConfig(AutoGenerator mpg, String moduleName){
PackageConfig pc = new PackageConfig();
//模块名
pc.setModuleName(moduleName);
pc.setParent("com.cancan.shirospringboot");
mpg.setPackageInfo(pc);
return pc;
}
/**
* 自定义配置
*
* @param mpg 自动代码生成对象
* @param pc 包配置
* @param projectPath 设置全局配置
*/
private static void injectionConfig(AutoGenerator mpg, PackageConfig pc, String projectPath){
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
List<FileOutConfig> focList = new ArrayList<>();
focList.add(new FileOutConfig("/templates/mapper.xml.ftl") {
@Override
public String outputFile(TableInfo tableInfo) {
String outPath = projectPath+"/src/main/java/"+pc.getParent().replaceAll("\\.","/")
+"/mapper/"+tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
return outPath;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
mpg.setTemplate(new TemplateConfig().setXml(null));
}
/**
* 策略配置
*
* @param mpg 自动代码生成对象
* @param pc 包配置
* @param tableName 表格名字
*/
private static void strategyConfig(AutoGenerator mpg, PackageConfig pc, String tableName){
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
strategy.setInclude(tableName);
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
}
}
2.4 在Shiro中继承了MD5的算法,所以可以直接使用它来实现对密码进行加密测试
2.4.1 配置配置信息
将 application.properties 修改为 application.yml
由于 2.3 添加了数据库信息,所以需要配置数据库的连接池和连接账号密码
server:
port: 8081
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/shiro?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
username: root
password: admin
2.4.2 使用Shrio自带的MD5加密,生成密码
直接在springboot项目的test运行即可
@SpringBootTest
class ShiroSpringbootApplicationTests {
private final String password = "1";
//盐,默认是用账号来当盐
private final String salt = "admin";
@Test
void testMD5() throws Exception{
//直接加密
Md5Hash hash= new Md5Hash(password);
System.out.println("账号为:"+salt+", 密码为:"+password+", 直接加密:"+hash);
//带着盐加密,本演示项目只是加密一次
Md5Hash hash1 = new Md5Hash(password, salt);
System.out.println("账号为:"+salt+", 密码为:"+password+",带着盐加密:"+hash1);
//带着盐加密,而且加密多次
//Md5Hash hash2 = new Md5Hash(password, salt, 2);
//System.out.println("带着盐加密两次"+hash2);
}
}
测试结果:
账号为:admin, 密码为:1, 直接加密:c4ca4238a0b923820dcc509a6f75849b
账号为:admin, 密码为:1,带着盐加密:e00cf25ad42683b3df678c61f42c6bda
2.5 自定义Realm做准备
自定义Realm需要认证/授权, 认证需要查找用户的信息出来比对,授权需要查找用户关联的角色和权限。下面是这几个功能的sql语句。
2.5.1 创建根据用户名字查找用户
EmployeeMapper.java
public interface EmployeeMapper extends BaseMapper<Employee> {
Employee selectEmployeeByUsername(String username);
}
EmployeeMapper.xml
<select id="selectEmployeeByUsername" resultType="com.cancan.shirospringboot.entity.Employee">
SELECT
id,
name,
password,
email,
age,
admin,
dept_id
FROM employee
WHERE name = #{username}
</select>
2.5.2 根据登录用户的id查询到其拥有的所有角色的编码
RoleMapper.java
public interface RoleMapper extends BaseMapper<Role> {
List<String> selectRoleSnsByEmpId(Long id);
}
RoleMapper.xml
<select id="selectRoleSnsByEmpId" resultType="java.lang.String">
SELECT r.sn
FROM role r
LEFT JOIN employee_role er
ON r.id = er.role_id
WHERE er.employee_id = #{employeeId}
</select>
2.5.3 根据登录用户的id查询到其拥有的所有权限表达式
PermissionMapper.java
public interface PermissionMapper extends BaseMapper<Permission> {
List<String> selectExpressionByEmpId(Long id);
}
PermissionMapper.xml
<select id="selectExpressionByEmpId" resultType="java.lang.String">
SELECT p.expression
from permission p
LEFT JOIN role_permission rp ON p.id = rp.permission_id
LEFT JOIN employee_role er ON rp.role_id = er.role_id
WHERE er.employee_id = #{employeeId}
</select>
2.5.4 打包遇到的问题,.xml放置在java目录下,而不是resource目录下
pom.xml配置的maven插件
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
<filtering>false</filtering>
</resource>
<!--把放置在java目录下的.xml文件放置在合适的路径下-->
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
2.6 自定义Realm
//将自定义的realm 由shiroconfig类 交给Spring容器管理
public class MyRealm extends AuthorizingRealm {
//根据用户名字在数据库查对应的角色出来
@Autowired
private EmployeeMapper mapper;
//员工id查角色
@Autowired
private RoleMapper roleMapper;
//员工id查权限
@Autowired
private PermissionMapper permissionMapper;
//com.pci.shiro01.shiro.MyRealm.doGetAuthenticationInfo()
//如果上面的方法没有加盐, 就不能配置盐
//将容器中的配置的凭证匹配器注入给当前的Realm对象
// 在该Realm中使用指定的凭证匹配器来完成密码匹配的操作
@Override
@Autowired
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher){
super.setCredentialsMatcher(credentialsMatcher);
}
//获取认证信息
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户的用户名
//有的同学该位置有点疑问,灿灿老师说明一下,AuthenticationToken为接口,真正由spring管理的类为 UsernamePasswordToken
//其中getPrincipal()方法获取的值为用户传进来的 username。
String username = token.getPrincipal().toString();
//根据用户的账号去数据库中查询用户的信息
Employee employee = mapper.selectEmployeeByUsername(username);
if(employee != null){
//身份: 类似我们之前在session中共享用户对象
//凭证: 从数据库中查询出来的密码
return new SimpleAuthenticationInfo(
employee, //身份信息
employee.getPassword(), //凭证
ByteSource.Util.bytes(employee.getName()), //盐
"crmRealm"); //Realm名称
}
return null;
}
//获取授权信息
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获取到当前登录的用户
Employee employee = (Employee) principals.getPrimaryPrincipal();
//如果是超级管理员, 授予其admin的角色和所有权限
if(employee.getAdmin()){
info.addRole("admin");
info.addStringPermission("*:*");
}else{
//根据登录用户的id查询到其拥有的所有角色的编码
List<String> roleSns = roleMapper.selectRoleSnsByEmpId(employee.getId());
//根据登录用户的id查询到其拥有的所有权限表达式
List<String> expressions = permissionMapper.selectExpressionByEmpId(employee.getId());
//将用户拥有的角色和权限i俺家到授权信息对象中, 供shiro授权校验时使用
info.addRoles(roleSns);
info.addStringPermissions(expressions);
}
return info;
}
}
2.7 自定义 FormAuthenticationFilter修改对认证结果的处理
2,7,1 Json结果封装类, 主要用于返回给前端数据
/**
* 返回给前端的json结果封装类
* @author cancan
* @since 2020-05-17
*/
@Data
public class JsonResult {
private boolean success = true;
private String msg;
public JsonResult() {
}
//如果失败的使用封装错误信息
public void mark(String msg){
this.success = false;
this.msg = msg;
}
public JsonResult(boolean success, String msg) {
this.success = success;
this.msg = msg;
}
}
2.7.2 修改默认认证过滤器中对认证结果的处理
该图为认证的过程
//修改默认认证过滤器中对认证结果的处理
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
response.setContentType("application/json;charset=UTF-8");
//响应页面中需要的数据
JsonResult result = new JsonResult();
response.getWriter().print(JSON.toJSONString(result));
HttpServletResponse response1 = (HttpServletResponse) response;
response1.sendRedirect("/login");
return false;
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
response.setContentType("application/json;charset=UTF-8");
//响应页面中需要的数据
JsonResult result = new JsonResult();
String msg = "";
if (e instanceof UnknownAccountException) {
msg = "账号错误";
} else if (e instanceof IncorrectCredentialsException) {
msg = "密码错误";
} else {
msg = "未知错误";
}
e.printStackTrace();
try {
result.mark(msg);
response.getWriter().print(JSON.toJSONString(result));
} catch (IOException e1) {
e1.printStackTrace();
}
return false;
}
}
2.8 Shiro的spring配置类
2.8.1 Shiro配置类
前置知识: 默认的自动可用过滤器实例由DefaultFilter enum定义,enum的name字段是可用于配置的名称。它们是:
过滤器名称 | 类 |
anon | org.apache.shiro.web.filter.authc.AnonymousFilter |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
authcBearer | org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter |
logout | org.apache.shiro.web.filter.authc.LogoutFilter |
noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
port | org.apache.shiro.web.filter.authz.PortFilter |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
ssl | org.apache.shiro.web.filter.authz.SslFilter |
user | org.apache.shiro.web.filter.authc.UserFilter |
当运行一个web应用程序时,Shiro会创建一些有用的默认过滤器实例,并使它们在[main]部分自动可用。您可以像配置任何其他bean一样在main中配置它们,并在链定义中引用它们。
/**
* shiro配置文件
* @author cancan
* @since 2020-05-16
*/
@Configuration
public class ShiroConfig {
//通过配置,设置session过期时间,没有配置默认10分钟过期
@Bean
public DefaultWebSessionManager sessionManager(@Value("${server.servlet.session.timeout:10}") long globalSessionTimeout, SessionDAO sessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setSessionValidationInterval(globalSessionTimeout * 60 * 1000);
sessionManager.setGlobalSessionTimeout(globalSessionTimeout * 60 * 1000);
sessionManager.setSessionDAO(sessionDAO);
return sessionManager;
}
@Bean
public SessionDAO sessionDAO() {
return new MemorySessionDAO();
}
//定义ShiroFilter
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//修改默认认证过滤器中对认证结果的处理
Map<String, javax.servlet.Filter> filters = shiroFilterFactoryBean.getFilters();
MyFormAuthenticationFilter filter = new MyFormAuthenticationFilter();
//将默认的登录账户username修改为account
filter.setUsernameParam("account");
filters.put("authc", filter);
shiroFilterFactoryBean.setFilters(filters);
// 默认跳转到登陆页面,com.cancan.shirospringboot.controller.LoginController.index
shiroFilterFactoryBean.setLoginUrl("/index");
// 登陆成功后的页面,com.cancan.shirospringboot.controller.LoginController.login
shiroFilterFactoryBean.setSuccessUrl("/login");
// 权限控制map
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 配置不会被拦截的链接 顺序判断
/*
* anon:无需认证就可以访问
* authc:必须认证了才能访问
* user:必须用有了 记住我 功能才能用
* perms:拥有对某个资源的权限才能访问
* role:拥有某个角色权限才能访问
*/
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/favicon.ico", "anon");
filterChainDefinitionMap.put("/style/", "anon");
filterChainDefinitionMap.put("/logout.do", "logout");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
//修改默认认证过滤器中对认证结果的处理
//注入bean, 为了交给shiroFilterFactoryBean 进行管理
@Bean
public MyFormAuthenticationFilter myFormAuthenticationFilter(){
MyFormAuthenticationFilter myFormAuthenticationFilter = new MyFormAuthenticationFilter();
return myFormAuthenticationFilter;
}
//指定使用哪一个安全管理器, 将安全管理器交给Sdpring容器管理器
//将定义类CRMRealm extends AuthorizingRealm 交给管理
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(MyRealm myRealm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setSessionManager(sessionManager);
securityManager.setRememberMeManager(null);
securityManager.setRealm(myRealm);
return securityManager;
}
//添加自己的Realm
@Bean
public MyRealm crmRealm(){
MyRealm myRealm = new MyRealm();
//如果没有添加盐, 就不要加盐验证
myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myRealm;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
//开启shiro注解授权
//使用代理方式;所以需要开启代码支持;
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
//指定当前需要使用的凭证匹配器
//在CRMRealm进行autowired使用
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//指定加密算法
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
hashedCredentialsMatcher.setHashIterations(1);
return hashedCredentialsMatcher;
}
//Shiro生命周期处理器
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
2.8.2 设置登录Controller和登录成功Controller
package com.cancan.shirospringboot.controller;
/**
* @author cancan
* @version 1.0
* @date 2020/5/17 20:06
*/
@RestController
public class LoginController {
//登录界面
@RequestMapping("/index")
public String index(){
return "请登录";
}
//登录成功的界面
@RequestMapping("/login")
public String login(){
return "登录成功";
}
}
2.8.3 认证登录测试
post请求: localhost:8081/index?account=admin&password=1
退出登录:
post请求: localhost:8081/logout.do
登录失败,账号错误
localhost:8081/index?account=admin1&password=1
{"msg":"账号错误","success":false}
登录失败,密码错误
localhost:8081/index?account=admin&password=2
{"msg":"密码错误","success":false}
2.9 授权
然后在控制器类上使用shiro提供的种注解来做控制:
注解 | 功能 |
@RequiresGuest | 只有游客可以访问 |
@RequiresAuthentication | 需要登录才能访问 |
@RequiresUser | 已登录的用户或“记住我”的用户能访问 |
@RequiresRoles | 已登录的用户需具有指定的角色才能访问 |
@RequiresPermissions | 已登录的用户需具有指定的权限才能访问 |
最开始的sql,我定义了3个用户,每个角色分配角色和权限
2.10 访问的controller
添加上认证的注解
注意:Shiro @RequiresRoles注解相关参数说明
@RequiresRoles(value={“admin”,“user”},logical = Logical.OR)
@RequiresPermissions(value={“add”,“update”},logical = Logical.AND)
如果有多个权限/角色验证的时候中间用“,”隔开,默认是所有列出的权限/角色必须同时满足才生效。但是在注解中有logical = Logical.OR这块。这里可以让权限控制更灵活些。
如果将这里设置成OR,表示所列出的条件只要满足其中一个就可以,如果不写或者设置成logical = Logical.AND,表示所有列出的都必须满足才能进入方法。
/**
* @author cancan
* @version 1.0
* @date 2020/5/17 20:06
*/
@RestController
public class LoginController {
//登录界面
@RequestMapping("/index")
public String index(){
return "请登录";
}
//登录成功的界面
@RequestMapping("/login")
@ResponseBody
public String login(){
return "登录成功";
}
@RequiresGuest
@ResponseBody
@RequestMapping("/hello")
public String hello(){
return "游客你好";
}
@RequiresRoles(value = {"admin","SS"},logical = Logical.OR)
@RequestMapping("/get")
@ResponseBody
public String get(){
return "拥有角色权限登录";
}
@RequiresPermissions(value = "js")
@RequestMapping("/getdist")
@ResponseBody
public String getDist(){
return "拥有近身攻击权限";
}
}
2.11 统一异常处理
/**
* 统一异常处理
* @author cancan
* @since 2020-05-16
*/
@ControllerAdvice
public class UnauthorizedExceptionUtil {
@ExceptionHandler(UnauthorizedException.class)
public void handler(HttpServletResponse response,
HandlerMethod method, UnauthorizedException e)
throws IOException {
if(method.getMethod().isAnnotationPresent(ResponseBody.class)){
response.setContentType("text/json;charset=UTF-8");
JsonResult result = new JsonResult();
result.mark("对不起, 您没有权限执行该操作");
response.getWriter().print(JSON.toJSONString(result));
}else{
throw e;
}
}
}
2.12 完整流程测试
写在最后
Shrio的用法本身就不是很难,但是还需要注意一些细节。
本文虽然简单,但是对于刚刚接触Shiro的人,还是很有帮助的,如果可以帮助你,请点赞,谢谢
本人长期从事java开发,如果有疑问,请留言