前言
以前开发过springboot整合shiro的权限部分,但是后来不开发,都忘了。
现在整理一下,从shiro的单应用到shiro开发,一步步到项目的整合开发,以及源码的分析。
一、初识 shiro
1. 简介
shiro是apache的一个开源框架,而且呢是一个权限管理的框架,用于实现用户认证、用户授权。
spring 中也有一个权限框架 spring security (原名Acegi),它和 spring 依赖过于紧密,没有 shiro 使用简单。
shiro 不依赖于 spring,shiro 不仅可以实现 web应用的权限 管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。使用shiro实现系统的权限 管理,有效提高开发效率,从而降低开发成本。
2. 逻辑与关键字
- subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。
- security Manager:安全管理器,主体进行认证和授权都是通过securityManager进行。
- authenticator:认证器,主体进行认证最终通过authenticator进行的。
- authorizer:授权器,主体进行授权最终通过authorizer进行的。
- sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。
- SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
- cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
- realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。
记住一点,Shiro 不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro 即可。
3. 认证流程
- 构建SecurityManager环境
- 主体提交认证
- SecurityManager 处理
- 流转到 Authenticator 执行认证
- 通过 Realm 获取相关的用户信息(获取验证数据进行验证)
4. 授权流程
- 创建构建SecurityManager环境
- 主体提交授权认证
- SecurityManager 处理
- 流转到 Authorizor 授权器执行授权认证
- 通过 Realm 从数据库或配置文件获取角色权限数据返回给授权器,进行授权。
二、使用springboot 测试单元行模拟测试
1. 创建项目
New----->Project----->Spring Initializr 一路默认后就创建好一个以spring boot构建的web项目了
主要界面:
2. 加入依赖包 1.4.1
<!-- shiro spring. -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
三、 shiro 用户认证 (demo1)
1. 流程逻辑
- 构建安全管理器环境 DefaultWebSecurityManager
- 构建数据源 这里先使用 SimpleAccountRealm (
后面可以做成读取动态读取数据库
) - 获取主体
- 用户名和密码生成token(
这里也就是 用户输入的用户名密码
) - 进行登入(提交认证)
这里先熟悉下流程,认证和源码过程再下面讲
2. 编写 Ch01_authentication 测试类
package com.feng;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class Ch01_authentication {
/**
* shiro 认证 初使用
*/
@Test
public void authentication() {
// 1. 构建安全管理器环境
/*
* 如果爆这个错:java.lang.IllegalArgumentException: SessionContext must be an HTTP compatible implementation.
* 1). 因为 版本低,要 1.4.1以上,
* 2). 或者使用 DefaultSecurityManager 这个类,这个是单机版,DefaultWebSecurityManager 这个是 web 版本
* */
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 2. 构建数据源,创建一个SimpleAccountRealm 域
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
// 添加一个测试账号(后面可以做成读取动态读取数据库)
simpleAccountRealm.addAccount("feng", "123456");
// 设置Realm
securityManager.setRealm(simpleAccountRealm);
SecurityUtils.setSecurityManager(securityManager);
// 3. 获取主体
Subject subject = SecurityUtils.getSubject();
// 4. 用户名和密码(这里也就是 用户输入的用户名密码 )生成token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("feng", "123456");
System.out.println(usernamePasswordToken.getUsername());
System.out.println(usernamePasswordToken.getPassword());
System.out.println(usernamePasswordToken.getPrincipal());
System.out.println(usernamePasswordToken.getCredentials());
System.out.println(usernamePasswordToken.getHost());
System.out.println(usernamePasswordToken.toString());
try {
// 5. 进行登入(提交认证)
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
//username wasn't in the system, show them an error message?
System.out.println("账号不存在");
} catch (IncorrectCredentialsException ice) {
System.out.println("用户名密码不匹配");
//password didn't match, try again?
} catch (LockedAccountException lae) {
//account for that username is locked - can't login. Show them a message?
System.out.println("账号已被禁用,请联系系统管理员");
} catch (AuthenticationException ae) {
//unexpected condition - error?
System.out.println("用户认证失败");
}
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
// 登出logout
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
}
}
3. 测试
- 当用户输入用户名密码为“feng”、“123456” 程序输出
- 当用户输入的用户名密码为‘feng“、”1234567“(密码错误) 程序输出
- 当用户输入的用户名密码为"feng1"、“123456”(用户名错误) 程序输出
四、 shiro 用户授权 (demo2)
上面的小 demo 是 shiro 实现认证的一个过程,做权限管理的时候,分为两块,一个认证,一个是授权,认证是判断用户是否账号密码正确,授权是判断用户登入以后有什么权限
1. 流程逻辑
与上面的用户认证一致,我就直接复制过来了,
- 构建安全管理器环境 DefaultWebSecurityManager
- 构建数据源 这里先使用 SimpleAccountRealm (
后面可以做成读取动态读取数据库
) - 获取主体
- 用户名和密码生成token(
这里也就是 用户输入的用户名密码
) - 进行登入(提交认证)
2. 编写Ch02_authorization 测试类
package com.feng;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class Ch02_authorization {
/**
* shiro 授权 初使用
*/
@Test
public void authorization() {
// 1. 构建安全管理器环境
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 2. 构建数据源,创建一个SimpleAccountRealm 域
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
//添加一个测试账号、和所拥有的角色(后面可以做成读取动态读取数据库)
simpleAccountRealm.addAccount("feng", "123456", "admin", "test");
// 设置Realm
securityManager.setRealm(simpleAccountRealm);
SecurityUtils.setSecurityManager(securityManager);
// 3. 获取主体
Subject subject = SecurityUtils.getSubject();
// 4. 用户名和密码(用户输入的用户名密码)生成token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("feng", "123456");
try {
// 5.进行登入(提交认证)
subject.login(usernamePasswordToken);
// 6. 检查是否有角色
subject.checkRoles("admin", "test");
// subject.checkRoles("admin", "user"); // 该用户没有权限访问
// subject.checkRoles("user"); // 该用户没有权限访问
} catch (UnknownAccountException e) {
//username wasn't in the system, show them an error message?
System.out.println("账号不存在");
} catch (IncorrectCredentialsException ice) {
System.out.println("用户名密码不匹配");
//password didn't match, try again?
} catch (LockedAccountException lae) {
//account for that username is locked - can't login. Show them a message?
System.out.println("账号已被禁用,请联系系统管理员");
} catch (AuthenticationException ae) {
//unexpected condition - error?
System.out.println("用户认证失败");
} catch (UnauthorizedException e) {
System.out.println("该用户没有权限访问");
}
System.out.println("---------->用户认证的状态:" + subject.isAuthenticated());
//登出logout
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("---------->用户认证的状态:" + subject.isAuthenticated());
}
}
3. 修改地方
修改一处,添加一处。
//添加一个测试账号、和所拥有的角色(后面可以做成读取动态读取数据库)
simpleAccountRealm.addAccount("feng", "123456", "admin", "test");
// 6. 检查是否有角色
subject.checkRoles("admin", "test");
//subject.checkRoles("admin", "user"); // 该用户没有权限访问
//subject.checkRoles("user"); // 该用户没有权限访问
4. 测试
- 当用户要
subject.checkRoles("admin","test");
检测是否拥有 admin、test角色的时候 程序输出 - 当用户要
subject.checkRoles("user","test");
检测是否拥有 test、user 角色的时候 程序输出 checkRoles ()
方法就是检测用户是否拥有传入的角色,即只要有一个不是用户所拥有的角色就会抛出异常。请看下列源码。
四、使用IniRealm进行认证授权
上面的两个demo中我们利用 SimpleAccountRealm 在程序中写死了用户安全数据,接下来我们使用 .ini
将数据移到配置文件中
1. 概述
IniRealm是Shiro提供一种Realm实现。用户、角色、权限等信息集中在一个.ini文件那里。
2. 配置 .ini 文件
在 resources
目录下创建一个 shiro.ini
文件
从文件中可以看出:用户admin 属于 admin 角色,拥有:所有的权限
用户test属于 test 角色,拥有:user:list,user:deleted,user:edit 三个权限
#账号信息
[users]
#账号=密码,角色
test=123456,test
admin=123456,admin
#角色信息
[roles]
test=user:list,user:deleted,user:edit,user:add
admin=*
3. 编写 Ch03_testIniRealm 测试类
流程与上不变,主要修改的是数据源的部分
package com.feng;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class Ch03_testIniRealm {
/**
* 读取 .ini配置文件 作为 数据源Realm
*/
@Test
public void testIniRealm() {
// 配置文件中用户权限信息,文件在类路径下
IniRealm initRealm = new IniRealm("classpath:shiro.ini");
// 1. 构建安全管理器环境
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 2. 设置Realm
securityManager.setRealm(initRealm);
SecurityUtils.setSecurityManager(securityManager);
// 3. 获取主体
Subject subject = SecurityUtils.getSubject();
// 4. 用户名和密码的token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("admin", "123456");
try {
// 5.主体提交认证请求
subject.login(usernamePasswordToken);
System.out.println("---------->用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
// 6. 检查是否有角色
subject.checkRoles("admin"); // test用户没有权限访问
// 检查是够有权限
subject.checkPermissions("user:add","user:userList");
System.out.println("---------->用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("---------->用户认证的状态:" + subject.isAuthenticated());
} catch (UnknownAccountException e) {
//username wasn't in the system, show them an error message?
System.out.println("账号不存在");
} catch (IncorrectCredentialsException ice) {
System.out.println("用户名密码不匹配");
//password didn't match, try again?
} catch (LockedAccountException lae) {
//account for that username is locked - can't login. Show them a message?
System.out.println("账号已被禁用,请联系系统管理员");
} catch (AuthenticationException ae) {
//unexpected condition - error?
System.out.println("用户认证失败");
} catch (UnauthorizedException e) {
// 关于是够有权限的 异常
System.out.println("该用户没有权限访问");
}
}
}
4. 测试
- 当
用户 test
进入到程序时候我们首先用 IniRealm 读取shiro.ini
配置获得 test 用户的一些安全信息。subject.checkRoles("test")
:即是判断该用户是否拥有 test 角色。subject.checkPermissions("user:add","user:list")
:即是检查用户是否拥有 user:list 的权限
很明显test 这个用户都满足这些条件,最后程序输出 - 假如把
subject.checkRoles("test") 改成 subject.checkRoles("amin") 即验证用户 test 是否拥有 admin 角色呢?
这个时候很明显会抛出异常,因为我们在 shiro.ini 配置里面只给 test 用户 配置了 test 的角色。 - 假如是用户 admin 登录进来呢?
很明显不会抛异常,因为用户 admin 拥有了 admin 这个角色,而 admin 这角色设置了 ‘*’ 的通配符即拥有所有的权限所以不会抛出异常。
程序输出
五、使用 JdbcRealm 进行认证授权
上一个案例demo我们主要讲了把用户安全信息(相应的角色/权限)配置在 .ini 文件,使用 IniRealm 去读取 .ini 文件获得用户的安全信息。也是有局限性的。因为我们得事先把所有用户信息配置在.ini 文件,这样显然是行不通的,我们的系统用户都是动态的不固定的,它的一些用户信息权限信息都是变化的,所以固定在.ini 配置文件显然是行不通的。这些数据通常我们都是把它存入到DB 中,那shiro 有没有提供直接从DB读取用户安全信息的域呢 ? (Realm)
shiro 作为一个优秀的开源框架,显然是可以的。
下面就该 JdbcRealm 出场了。
1. 新加依赖:数据库驱动和数据源
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--数据源-->
<!--http://localhost:8083/druid/index.html 通过这个网址 对 SQL进行监控-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
2. 数据库表结构
下面我们创建一个名为shiro
的数据库、分别创建三张表 users、user_roles、roles_permissions
创建数据库shiro
CREATE DATABASE IF NOT EXISTS shiro DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
创建users`表
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(25) DEFAULT NULL,
`password` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
创建user_roles
表
CREATE TABLE `user_roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_name` varchar(25) DEFAULT NULL,
`username` varchar(25) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
创建roles_permissions
表
CREATE TABLE `roles_permissions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`permission` varchar(255) DEFAULT NULL,
`role_name` varchar(25) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
往三个表中插入一些数据
INSERT INTO `shiro`.`users` (`id`, `username`, `password`) VALUES ('1', 'admin', '123456');
INSERT INTO `shiro`.`users` (`id`, `username`, `password`) VALUES ('2', 'test', '123456');
INSERT INTO `shiro`.`user_roles` (`id`, `role_name`, `username`) VALUES ('1', 'admin', 'admin');
INSERT INTO `shiro`.`user_roles` (`id`, `role_name`, `username`) VALUES ('2', 'test', 'test');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role_name`) VALUES ('1', 'user:deleted', 'test');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role_name`) VALUES ('2', 'user:list', 'test');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role_name`) VALUES ('3', '*', 'admin');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role_name`) VALUES ('4', 'user:edit', 'test');
3. 编写 Ch04_testJdbcRealm测试类
流程逻辑其实与上面几个案例一致。
主要修改的地儿 还是数据域这里,其余不变
package com.feng;
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class Ch04_testJdbcRealm {
/**
* 读取shiro创建好的 JdbcRealm ,sql语句也写好
*/
@Test
public void testJdbcRealm() {
// 配置数据源
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl("jdbc:mysql://192.168.131.168:3306/shiro");
druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
druidDataSource.setUsername("root");
druidDataSource.setPassword("Dataadt123!");
// 1. 构建安全管理器环境
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 2. 搭建数据源 Realm
// 配置文件中的用户权限信息,文件在类路径下
JdbcRealm jdbcRealm = new JdbcRealm();
jdbcRealm.setDataSource(druidDataSource);
// 使用 JdbcRealm 下面的值需要为 true 不然无法查询用户权限
jdbcRealm.setPermissionsLookupEnabled(true);
// 设置 Realm
securityManager.setRealm(jdbcRealm);
SecurityUtils.setSecurityManager(securityManager);
// 3. 获取主体
Subject subject = SecurityUtils.getSubject();
// 4. 用户名和密码的token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("test", "123456");
try {
// 5.主体提交认证
subject.login(usernamePasswordToken);
System.out.println("---------->用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
// 6. 检查是否有角色
subject.checkRoles("test");
System.out.println("---------->有 test 角色");
// 7. 检查是够有权限
subject.checkPermissions("user:update");
System.out.println("---------->有 user:update 权限");
System.out.println("---------->用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("---------->用户认证的状态:" + subject.isAuthenticated());
} catch (UnknownAccountException e) {
//username wasn't in the system, show them an error message?
System.out.println("账号不存在");
} catch (IncorrectCredentialsException ice) {
System.out.println("用户名密码不匹配");
//password didn't match, try again?
} catch (LockedAccountException lae) {
//account for that username is locked - can't login. Show them a message?
System.out.println("账号已被禁用,请联系系统管理员");
} catch (AuthenticationException ae) {
//unexpected condition - error?
System.out.println("用户认证失败");
} catch (UnauthorizedException e) {
// 关于是够有权限的 异常
System.out.println("该用户没有权限访问");
}
}
}
4. 测试
- 当用户 admin 登录进来的时候 程序输出
- 当用户 test 登录进来的时候 程序输出
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("test", "123456");
5. 源码分析
看了这个例子,为什么我们没有写 sql 怎么查询用户信息和角色,权限信息的呢?我们来看JdbcRealm源码
从以上源码得知其实 JdbcRealm 已经帮我们写好查询语句了,所以我们就要在数据库创建与之对应的表结构,这样才能查出数据,但是有的同学可能就有疑问了,这里只能使用他默认的 sql,在实际的开发中,我们不可能就简单的使用 JdbcRealm 默认的 sql 语句,而是自己自定义的 sql 语句,更多时候我们的数据库以及数据表都是根据业务需要自己创建的。而且自己创建的表中字段必须与上面源码中一致,才可以。
因此需要自定义写SQL,看如下案例。
六、自定义SQL认证授权
流程逻辑其实还是不变,都是一样的,改变的地儿还是数据域这里(Realm),看类即可,我不再测试了
1. 编写 Ch05_testNewJdbcRealm 类
package com.feng;
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class Ch05_testNewJdbcRealm {
/**
* 修改数据库的表,统一加前缀 sys_:: users=>sys_users,user_roles=>sys_user_roles, roles_permissions=>sys_roles_permissions
* 进行查询自定义的表
*/
@Test
public void testNewJdbcRealm() {
// 配置数据源
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl("jdbc:mysql://192.168.131.168:3306/shiro");
druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
druidDataSource.setUsername("root");
druidDataSource.setPassword("Dataadt123!");
// 1. 构建安全管理器环境
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 2. 配置数据源
// 配置文件中的用户权限信息,文件在类路径下
JdbcRealm jdbcRealm = new JdbcRealm();
jdbcRealm.setDataSource(druidDataSource);
// 使用 JdbcRealm 下面的值需要为 true 不然无法查询用户权限
jdbcRealm.setPermissionsLookupEnabled(true);
// 使用自定义的 SQL 查询用户权限
String sql = "select password from users where username =?";
jdbcRealm.setAuthenticationQuery(sql);
String roleSql = "select role_name from user_roles where username = ?";
jdbcRealm.setUserRolesQuery(roleSql);
String permissionSql = "select permission from roles_permissions where role_name = ?";
jdbcRealm.setPermissionsQuery(permissionSql);
// 设置 Realm
securityManager.setRealm(jdbcRealm);
SecurityUtils.setSecurityManager(securityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("admin", "123456");
try {
// 2.主体提交认证
subject.login(usernamePasswordToken);
System.out.println("---------->用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
// 检查是否有角色
subject.checkRoles("admin");
System.out.println("---------->有 admin 角色");
// 检查是够有权限
subject.checkPermissions("user:list");
System.out.println("---------->有 user:list 权限");
System.out.println("---------->用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("---------->用户认证的状态:" + subject.isAuthenticated());
} catch (UnknownAccountException e) {
//username wasn't in the system, show them an error message?
System.out.println("账号不存在");
} catch (IncorrectCredentialsException ice) {
System.out.println("用户名密码不匹配");
//password didn't match, try again?
} catch (LockedAccountException lae) {
//account for that username is locked - can't login. Show them a message?
System.out.println("账号已被禁用,请联系系统管理员");
} catch (AuthenticationException ae) {
//unexpected condition - error?
System.out.println("用户认证失败");
} catch (UnauthorizedException e) {
// 关于是够有权限的 异常
System.out.println("该用户没有权限访问");
}
}
}
七、自定义 Realm 进行认证授权(important)
1. 概述
虽然 jdbcRealm 已经实现了从数据库中获取用户的验证信息,但是 jdbcRealm灵活性也是稍差一些的,如果要实现自己的一些特殊应用时将不能支持,这个时候可以通过自定义realm来实现身份的认证功能。
通常自定义Realm只需要继承:AuthorizingRealm
重写 doGetAuthenticationInfo
(用户认证)和doGetAuthorizationInfo
(用户授权) 这两个方法即可。
2. 自定义 Realm
编写类 com.feng.shiro.CustomRealmCh06
。这个也就是上面案例中的数据域部分。继承AuthorizingRealm类,实现doGetAuthenticationInfo()和doGetAuthorizationInfo()方法。
里面的数据是做的模拟数据,按说应该从数据库中获取,此时这里不是重点哈。
package com.feng.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CustomRealmCh06 extends AuthorizingRealm {
/**
* 模拟数据库中的用户名和密码
*/
private Map<String, String> userMap =new HashMap<>();
/**
* 使用代码块初始化数据
*/
{
userMap.put("admin", "123456");
userMap.put("test","123456");
}
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("---------->开始执行 CustomRealm.doGetAuthorizationInfo 方法啦");
// SecurityUtils.getSubject().getPrincipal()用户名: 就是SimpleAuthenticationInfo(username,password,getName()); 第一个参数
String name= (String) SecurityUtils.getSubject().getPrincipal();
System.out.println("---------->(String)SecurityUtils.getSubject().getPrincipal():"+name);
String username = (String) principalCollection.getPrimaryPrincipal();
System.out.println("---------->(String) principalCollection.getPrimaryPrincipal():"+username);
//从数据库或者缓存中获取角色数据
List<String> roles = getRolesByUsername(username);
//从数据库或者缓存中获取权限数据
List<String> permissions = getPerminssionsByUsername(username);
//创建AuthorizationInfo,并设置角色和权限信息
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addStringPermissions(permissions);
authorizationInfo.addRoles(roles);
return authorizationInfo;
}
/**
* 认证
* 逻辑:获取用户输入的用户名。密码不用获取,已经在 shiro 中(密码是 HashedCredentialsMatcher 进行 md5 加密的)
* 根据用户名获取数据库的密码,然后进行MD5加密。
* 然后将 用户名、加密后密码、类名称 交给shiro去认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
/*
* AuthenticationToken 是 UsernamePasswordToken 顶级父类,getPrincipal() 和 getCredentials() 是其接口的两个方法,
* 被 UsernamePasswordToken实现重新,分别是 获取用户名和密码。
* 这里也就是获取用户输入的用户名和密码
* */
System.out.println("---------->开始执行 CustomRealm.doGetAuthenticationInfo 方法啦");
//获取登录用户名
String username = (String) authenticationToken.getPrincipal();
System.out.println("---------->根据authenticationToken获取到的用户名:"+username);
// 下面这行 执行报错,不能获取用户输入的密码, 上面是获取用户名,可以获取
// String pass = (String) authenticationToken.getCredentials();
// System.out.println("---------->根据authenticationToken获取到的密码:"+pass);
//通过用户名到数据库获取用户信息
String password = getPasswordByUsername(username);
System.out.println("---------->从数据库中获取的密码:"+password);
if (password == null) {
return null;
}
// 不加密
// 传入用户名、密码、name 。流转到下面去认证
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username, password, getName());
return authenticationInfo;
}
/**
* 模拟通过数据库获取权限数据
* @Author: 小冯
* @UpdateUser:
* @Version: 0.0.1
* @param username
* @return java.util.List<java.lang.String>
* @throws
*/
private List<String> getPerminssionsByUsername(String username) {
List<String> permissions = new ArrayList<>();
/**
* 只有是 admin 用户才有 新增、删除、编辑权限
* 用户只有 遍历 权限
*/
if(username.equals("admin")){
permissions.add("user:delete");
permissions.add("user:add");
permissions.add("user:edit");
}
permissions.add("user:list");
return permissions;
}
/**
* 模拟通过数据库获取用户角色信息
* @Author: 小冯
* @UpdateUser:
* @Version: 0.0.1
* @param username
* @return java.util.List<java.lang.String>
* @throws
*/
private List<String> getRolesByUsername(String username) {
List<String> roles = new ArrayList<>();
if(username.equals("admin")){
roles.add("admin");
}
roles.add("test");
return roles;
}
/**
* 通过用户名查询密码,模拟数据库查询
* @Author: 小冯
* @UpdateUser:
* @Version: 0.0.1
* @param username
* @return java.lang.String
* @throws
*/
private String getPasswordByUsername(String username) {
return userMap.get(username);
}
}
3. 编写 Ch06_testCustomRealm 测试类
这里的代码逻辑部分其实也是不变的。只改变了数据域的地方。
package com.feng;
import com.feng.shiro.CustomRealmCh06;
import com.feng.shiro.CustomRealmCh07;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class Ch06_testCustomRealm {
/**
* 自定义 数据源 CustomRealm 。
*/
@Test
public void testCustomRealm() {
// 1. 构建安全管理器环境
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 2. 构建数据源
CustomRealmCh06 customRealmCh06 = new CustomRealmCh06();
securityManager.setRealm(customRealmCh06);
SecurityUtils.setSecurityManager(securityManager);
// 3. 获取主体
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("admin", "123456");
System.out.println(usernamePasswordToken.getUsername());
System.out.println(usernamePasswordToken.getPassword());
System.out.println(usernamePasswordToken.getPrincipal());
System.out.println(usernamePasswordToken.getCredentials());
try {
// 2.主体提交认证
subject.login(usernamePasswordToken);
System.out.println("---------->用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
// 检查是否有角色
subject.checkRoles("admin");
System.out.println("---------->有 admin 角色");
// 检查是够有权限
subject.checkPermissions("user:delete", "user:add", "user:edit", "user:list");
System.out.println("---------->有 user:delete, user:add, user:edit 权限");
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("---------->用户认证的状态:" + subject.isAuthenticated());
} catch (UnknownAccountException e) {
//username wasn't in the system, show them an error message?
System.out.println("账号不存在");
} catch (IncorrectCredentialsException ice) {
System.out.println("用户名密码不匹配");
//password didn't match, try again?
} catch (LockedAccountException lae) {
//account for that username is locked - can't login. Show them a message?
System.out.println("账号已被禁用,请联系系统管理员");
} catch (AuthenticationException ae) {
//unexpected condition - error?
System.out.println("用户认证失败");
} catch (UnauthorizedException ue){
System.out.println("用户没有权限");
}
}
}
4. 测试
- 当用户 admin 登录进来的时候 程序输出
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("admin", "123456");
- 当用户 test 登录进来的时候 程序输出
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("test", "123456");
- 当用户 dev 登陆进来的时候 程序输出
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("dev", "123456");
通过上述验证过程我们可以发现,shiro 更多的是帮助我们完成验证过程。我们需要从数据库查询当前用户的角色、权限,把这些信息告诉 shiro 框架。当我们执行用户认证的时候首先调用 doGetAuthenticationInfo 进行用户认证,当我们要校验权限的时候 就会执行
doGetAuthorizationInfo 进行授权操作。