什么是shiro?
一款apache公司出品的Java安全框架,主要用于设计针对应用程序的保护,使用shiro可以完成认证、授权、加密、会话管理等。保证系统稳定性、数据安全性
优势:易于使用、易于理解、兼容性强(可以与其他框架集成)
什么是认证?
认证是指身份认证,即判断该用户身份是否合法是否符合规定的处理过程。比如用户登录:根据用户提供的用户名和密码与系统中存储的是否一致
shiro与SpringSecurity
Spring Security基于spring框架开发,上手难度更大一些,需要的配置文件也较复杂。但是SpringSecurity搭配spring框架更加灵活。而使用shiro则需要与spring框架进行整合
在功能性方面,SpringSecurity要比shiro更加丰富,功能更加强大。但是shiro易于理解,上手速度快,不依赖于任何框架和容器。因为有良好的兼容性,可以与不同的框架整合开发
导入依赖资源
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
shiro功能了解
如上图所示,shiro支持认证、授权、会话管理、加密操作,以及对web开发的支持
Caching缓存(用户通过验证后,将其信息与权限存入缓存,避免每次都操作数据库
Testing支持对test单元测试的操作
Run As 允许一个用户用另一个用户的身份登录
Remember Me 记住我,一次登录下次可以直接进入
Concurrency支持对并发验证,可以通过线程将权限传播
shiro核心组件
subject主体:subject记录了当前进行操作的用户,外部程序通过subject进行相关校验和认证授权等,而subject是通过SecurityManager安全管理器进行认证授权
SecurityManager安全管理器:对外部subject进行安全管理,SecurityManager针对subject进行认证、授权等擦欧总,但是SecurityManager通过认证器Authenticator进行认证,通过Authorizer授权器控制授权
Authenticator认证器对用户身份进行认证
Authorizer授权器,用户认证通过后所执行的某些操作需要通过授权器授权才会获得执行权
Realm用户域,securityManager对用户进行安全认证时需要获取用户数据,如果数据在数据库那么realm就会去数据库中访问该用户数据从而执行校验。可以把realm理解为一个数据源,但是realm不仅仅是一个数据源,它还包含了一些认证授权的操作
简单实现shrio登录验证
创建普通maven项目,导入上述依赖资源。利用ini存放一些简单数据供测试使用
[users]
zhangsan=z3,role1,role2
lisi=l4
[roles]
role1=user:insert,user:select
编写测试类:
{
// 1. 获取安全管理实例
// 该方法已经被废弃,此处用作演示使用
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 2. 获取subject
Subject subject = SecurityUtils.getSubject();
// 3. 创建token对象
AuthenticationToken token = new UsernamePasswordToken("zhangsan","z3");
// 4. 完成登录
try {
subject.login(token);
System.out.println("登陆成功!");
// 5. 判断角色
boolean hasRole = subject.hasRole("role1");
System.out.println("该角色是否存在="+hasRole);
// 6. 判断权限
boolean permitted = subject.isPermitted("user:insert");
System.out.println("是否拥有此权限="+permitted);
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户不存在");
} catch (AuthenticationException e) {
e.printStackTrace();
}
}
shiro加密
shiro提供了多种不同的加密算法来对用户信息进行加密,同样的加密方式也有多种。比如利用MD5类进行加密,那么通过MD5不同的构造器就可以实现不同的加密方式以及迭代加密次数
String pwd = "z3";
// 使用shrio提供方法加密
Md5Hash md5Hash = new Md5Hash(pwd);
System.out.println("简单加密="+md5Hash.toHex());
// 密码拼接后加密 添加干扰信息
Md5Hash md5Hash1 = new Md5Hash(pwd,"ppt");
System.out.println("干扰加密="+md5Hash1.toHex());
// 也可以继续进行迭代加密
Md5Hash md5Hash2 = new Md5Hash(pwd, "salt", 3);
System.out.println("迭代加密="+md5Hash2.toHex());
// 使用父类加密
SimpleHash simpleHash = new SimpleHash("MD5",pwd,"pilipala",3);
System.out.println("父类加密="+simpleHash.toHex());
shiro自定义登录认证
shiro提供的登录认证默认是不带有加密操作的,如果需要加密则需要开发时自定义认证操作
- 继承AuthenticatingRealm类,并且实现doGetAuthenticationInfo方法,有一个参数AuthenticationToken身份验证令牌,该令牌会拥有用户的realm信息,可以通过它获取用户输入的账号和密码
- 查询数据库中对应用户输入用户名的信息,存在该用户名则获取其密码,不存在直接返回null表示用户不存在
- 获取到数据库中信息之后,将用户名、数据库获取的加密后密码、干扰信息“盐”、用户名string类型封装给AuthenticationInfo身份验证信息对象
- 然后在底层做出判断
ini文件
# shiro加密配置器
[main]
md5CredentialsMatcher=org.apache.shiro.authc.credential.Md5CredentialsMatcher
md5CredentialsMatcher.hashIterations=3
myrealm=com.yuqu.test.MyRealm
myrealm.credentialsMatcher=$md5CredentialsMatcher
securityManager.realms=$myrealm
[users]
zhangsan=09d772642c9848153b119fc016580411,role1,role2
lisi=l4
[roles]
role1=user:insert,user:select
自定义MyRealm类
{
// 1. 获取身份信息
String principal = token.getPrincipal().toString();
System.out.println("用户信息="+principal);
System.out.println("tokrn.getPrincipal="+token.getPrincipal());
// 2. 获取凭证信息 比如 密码
String pwd = new String((char[]) token.getCredentials());
System.out.println("token.getCredentials()="+new String((char[]) token.getCredentials()));
// 3. 访问数据库获取该用户所存储的信息
if (principal.equals("zhangsan")){
// 数据库中存储的加密后的密码
String pwdSql = "09d772642c9848153b119fc016580411";
// 4. 信息封装给校验逻辑对象 返回封装数据
AuthenticationInfo info = new SimpleAuthenticationInfo(
token.getPrincipal(), // 用户输入的用户名
pwdSql, // 数据库拿到的加密后密码
ByteSource.Util.bytes("pilipala"),// 干扰信息
token.getPrincipal().toString()
);
// 可以发现用户输入的密码z3没有封装过去,
// 所以我盲猜底层实现是将数据库中加密后的密码进行恢复然后再比较
System.out.println("干扰信息="+ByteSource.Util.bytes("pilipala"));
return info;
}
return null;
}
自定义realm类需要继承AuthorizingRealm类,然后实现两个doGet方法
- doGetAuthenticationInfo读写身份验证信息
- doGetAuthorizationInfo读写授权信息
上述示例仅展示读写身份验证信息
测试类
{
// 1. 获取安全管理实例
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 2. 获取subject
Subject subject = SecurityUtils.getSubject();
// 3. 创建token对象
AuthenticationToken token = new UsernamePasswordToken("zhangsan","z3");
// 4. 完成登录
try {
subject.login(token);
System.out.println("登陆成功!");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户不存在");
} catch (AuthenticationException e) {
e.printStackTrace();
}
}
通过上述示例,有一点值得注意:在封装身份验证信息时并未将用户输入的密码也就是加密之前的密码封装过去,但是封装了加密之后的信息。所以我盲猜底层是获取了数据库中加密的数据进行解密后对比,得空了看看源码到底是不是
分析一下执行过程:
首先获取到用户的输入,并将用户输入的信息封装为身份验证令牌也就是AuthenticationToken,然后通过subject主体调用login方法,并将该令牌入参。由此开始执行我们的自定义realm登录认证。
首先在ini文件中已经配置好了认证配置信息比如自定义realm类的未知,加密方式以及迭代次数。回到realm类,doGetAuthenticationInfo方法由底层自动调用,其形参就是我们先前传入的令牌,通过令牌获取用户输入的用户名密码等信息,然后通过用户名进入数据库进行查找,找不到就返回null表示该用户不存在。找到之后就获取其密码,最后将数据库中获取的密码以及用户输入的用户名和干扰信息也就是盐共同封装给身份验证信息对象并返回。
shiro授权
自定义realm类实现的doGetAuthorizationInfo读写授权信息方法
// doGetAuthorizationInfo 读写授权信息
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String principal = (String) principals.getPrimaryPrincipal();// 获取用户名
// 查询数据库 该用户都具备什么权限
List<String> permissions = new ArrayList<String>();
permissions .add("user:insert");
permissions .add("user:select");
// 权限交给校验逻辑对象
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addStringPermissions(permissions);
return authorizationInfo;
}
测试
// 6. 判断权限
boolean permitted = subject.isPermitted("user:insert");
System.out.println("是否拥有insert权限="+permitted);
shiro整合SpringBoot+MyBatis+Druid数据源
导入shiro支持的springBoot依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.8.0</version>
</dependency>
在application.yaml文件中配置mybatis和Druid
spring:
thymeleaf:
cache: false
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sqltest?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
# springBoot默认不会配置以下属性值 需要手动绑定
# druid 数据源专有配置
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
#配置监控统计拦截的filters stat:监控统计、log4j日志记录 wall防御sql注入
filters: stat,wall,log4j
max-pool-prepared-statement-per-connection-size: 20
use-global-data-source-stat: true
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
mybatis:
type-aliases-package: com.yuqu.pojo
mapper-locations: classpath:mybatis/mapper/*.xml
编写controller层
@RequestMapping("/login")
public String loginProcess(String username,String password,Model model){
System.out.println("获取到前端传回的账号密码=="+username+"------"+password);
// 获取当前用户
Subject subject = SecurityUtils.getSubject();
// 加密
// Md5Hash md5Hash = new Md5Hash(password);
// System.out.println("加密后的密码="+md5Hash.toHex());
// 获取用户信息
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
token.setRememberMe(true);
try {
// 登录
subject.login(token);
System.out.println("登陆成功!");
return "/index";
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
model.addAttribute("message","密码错误");
return "login";
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户不存在");
model.addAttribute("message", "用户不存在");
return "login";
}
}
controller层获取用户subject后调用登录方法会走我们自定义的realm用户域进行认证和授权,此处只展示认证操作
自定义UserRealm
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("经过用户认证----------");
Books books = bookService.selectBookByName(token.getPrincipal().toString());
if (books.getBookName().equals(token.getPrincipal().toString())){
// 用户名存在 校验密码
return new SimpleAuthenticationInfo("",books.getBookCounts().toString(),"");
}
return null;
}
我这里使用的是之前的Book表,bookName作为用户名,bookCounts作为密码
shiro整合thymeleaf
导入依赖
<!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
在视图上配置命名空间
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"