Shiro
是一个权限认证的框架,Web安全性包括用户认证和用户授权
用户认证:验证某个用户是否是一个合法的用户,通常用来通过用户名和密码验证这个用户是否能登录
用户授权:验证某个用户是否有权限执行某个操作
核心架构
最重要的是安全管理器
-
authenticator是用来认证使用的
-
authorizer是用来实现授权的
-
session manager是在web中使用的,用来管理web的web会话
-
session dao对session进行增删改查的
-
cache manager 用来缓存认证和授权的数据
-
pluggable realms用来获取认证、授权的数据,完成授权操作
-
cryptography是算法生成器,提供了密码加密的算法
认证
shiro
认证的关键对象:
subject
- 中文为课程、题目、主题,读音为
səbˈdʒekt
- 在这里代指为主体,通常主体为:用户(当前访问系统的用户)、程序等
- 中文为课程、题目、主题,读音为
principal
- 中文为最主要的、主要的,读音为
ˈprɪnsəpl
- 是主体subject进行身份认证的表示,这个标识必须具有唯一性
- 可以有多个身份,但必须要有一个主身份
- 例如用户名、手机号码、邮箱等
- 中文为最主要的、主要的,读音为
credential
- 中文为资质、凭据,读音为
krəˈdenʃl
- 通常称为凭证信息,通常密码、口令等
- 中文为资质、凭据,读音为
引入依赖:
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.9.0</version>
</dependency>
配置文件
配置文件使用.ini
结尾的文件,可以将权限数据暂时写死到这个文件中
例如以下是3个用户:
[users]
admin=123456
user=user123
username=password
认证的实现
- 创建安全管理器对象
SecurityManager
接口,可以使用实现类DefaultSecurityManager
- 给安全管理器提供用户的数据
- realm中文为领域,读音为
relm
- 可以使用
DefaultSecurityManager
类提供的setRealm
设置数据
- realm中文为领域,读音为
- 使用全局安全工具类
SecurityUtils
完成认证- 给这个工具类设置安全管理器
- 获取
Subject
主体 - 创建一个令牌(
UsernamePasswordToken
类),通过在令牌中设置用户名和密码 - 通过
Subject
主体中的login(令牌)
的方式进行尝试登录- 如果登录失败,并且密码错误,将会抛出
org.apache.shiro.authc.IncorrectCredentialsException
异常(不正确的凭据异常) - 如果用户名不存在,将会抛出
org.apache.shiro.authc.UnknownAccountException
异常 subject.isAuthenticated()
方法可以判断认证状态
- 如果登录失败,并且密码错误,将会抛出
- 以上流程可以概括为:
- 创建一个安全管理器
- 在安全管理器中设置已存在的账号密码
- 为全部安全工具类设置一个安全管理器
- 设置一个令牌
- 通过安全管理器获取一个主体
- 通过主体验证令牌
public static void main(String[] args) {
// 创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 设置数据
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
// 设置安全工具类的安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 获取一个主体
Subject subject = SecurityUtils.getSubject();
// 设置一个令牌
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
// 尝试登录
try {
subject.login(token);
} catch (AuthenticationException e) {
System.out.println("登录失败");
e.printStackTrace();
}
System.out.println("认证状态:" + subject.isAuthenticated());
}
底层源码实现
底层调用的是安全管理器中的login()
方法
-
向下执行,会发现先判断令牌是不是空的,如果是空的就直接抛异常,如果不是继续往下
-
进行断言
realms
是不是空的,如果是空的就直接抛出异常,如果不是空的,继续向下执行 -
获取所有的
realms
并放到List
集合中,判断一下List
集合的大小,如果大小是1,就直接在集合中取出这个realm
- 当大小是1时,先判断当前的
realm
是否支持这个token
(令牌),如果不支持就打个日志,并抛出异常,如果支持 继续向下执行 - 首先在缓存中取出认证信息,如果是空的就通过用户名拿到这个用户信息,拿到之后判断一下用户是不是锁定了或者密码是不是过期了,如果有相应的情况就抛出相应的异常,如果没有相应的情况直接返回这个用户信息,需要注意这个时候并没有进行密码的校验
- 如果取出的用户信息不为空,那么将返回的用户信息放入到缓存中
- 这个时候会进行断言密码是否匹配
- 如果不匹配就抛出异常
- 当想要进行从数据库中读取用户名和密码时,可以将
SimpleAccountRealm
类中的doGetAuthenticationInfo(AuthenticationToken token)
方法换成自己的实现即可,又由于SimpleAccountRealm
类继承自AuthorizingRealm
,所以自定义的类可以继承AuthorizingRealm
- 当大小是1时,先判断当前的
-
总结:
自定义Realm实现
-
自定义类继承
AuthorizingRealm
类,实现抽象方法 -
主要重写
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
方法- 通过调用
token.getPrincipal()
将其值强制转换为String
,此值将作为用户名 - 连接数据库,通过用户名获取用户,如果用户为空,直接返回空即可
- 如果不为空,需要返回
AuthenticationInfo
实例,可以使用实现类SimpleAuthenticationInfo
的实例进行返回,构造方法有3个参数,分别为:- 参数1为用户名
- 参数2为密码(口令)
- 参数3为一个名字,可以用父类的
getName()
获取名称
- 通过调用
-
将
SecurityManager
的realm
设置为自定义类的实例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.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; public class MyRealm extends AuthorizingRealm { // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal(); System.out.println("token中的用户名为:" + principal); // 接下来可以访问数据库通过用户名获取整个用户信息 if (!"hello".equals(principal)) { // 代表此时不存在这个用户 return null; } // 参数1为用户名;参数2为密码(口令);参数3为一个名字,可以用父类的getName()获取名称 return new SimpleAuthenticationInfo("hello", "123456", getName()); } }
// 创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 设置数据
securityManager.setRealm(new MyRealm());
// 设置安全工具类的安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 获取一个主体
Subject subject = SecurityUtils.getSubject();
// 设置一个令牌
UsernamePasswordToken token = new UsernamePasswordToken("hello", "1234567");
// 尝试登录
try {
subject.login(token);
} catch (AuthenticationException e) {
System.out.println("登录失败");
e.printStackTrace();
}
System.out.println("认证状态:" + subject.isAuthenticated());
加密
MD5
:
- 一般用来加密、签名
- 算法是不可逆的,同一条数据无论加密多少次,得到的结果都是一致的
- 如果想要求出原始字符串,那就只能通过穷举法
- 生成结果始终是16进制的32位的字符串
- 一般在
Service
层完成密码的加密
Salt
:
- 如果用户输入的密码是一个比较简单的密码,那么使用穷举进行求出原始串时也会变的比较容易,所以为了密码的进一步安全性,通常在注册时需要在密码前边或者后边加入一个随机的字符串,然后在对这个密码做
MD5
加密,随机加的字符串称为Salt
,在设计数据库时还需要将Salt
作为一个字段进行存储 - 在登录时,需要在数据库中取出这个
salt
,将其拼接到密码中,在对这段密码进行MD5
加密,将加密后的结果再与数据库中的进行比对
在Shiro中对密码进行加密
错误的用法如下:
Md5Hash md5Hash = new Md5Hash();
md5Hash.setBytes("admin".getBytes());
System.out.println("md5Hash.toHex() = " + md5Hash.toHex());
结果为md5Hash.toHex() = 61646d696e
正确的用法:
使用构造方法
Md5Hash md5Hash = new Md5Hash("admin");
System.out.println("md5Hash.toHex() = " + md5Hash.toHex());
使用salt
:
String uuid = UUID.randomUUID().toString();
Md5Hash md5Hash = new Md5Hash("admin", uuid);
System.out.println("uuid = " + uuid);
System.out.println("md5Hash.toHex() = " + md5Hash.toHex());}
第二个参数为salt
也可以对结果进行再次散列:
构造方法的第3个参数为对结果进行散列的次数
Md5Hash md5Hash = new Md5Hash("admin", uuid, 10240);
如果想要在Realm
中实现通过MD5
进行比较,还需要进行切换凭据匹配器
可以在自定义的realm
类中通过:
-
getCredentialsMatcher();
获取一个凭据匹配器 -
setCredentialsMatcher(CredentialsMatcher credentialsMatcher);
设置一个凭据匹配器-
CredentialsMatcher
是一个接口,可以使用HashedCredentialsMatcher
作为凭据匹配器实现MD5
比较 -
在
Realm
实现类的构造方法中设置凭据匹配器为setCredentialsMatcher(new HashedCredentialsMatcher("md5"));
-
其他地方代码保持不变,只是返回的密码变成了
MD5
-
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.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; public class MyRealm extends AuthorizingRealm { public MyRealm() { setCredentialsMatcher(new HashedCredentialsMatcher("md5")); } // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal(); System.out.println("token中的用户名为:" + principal); // 接下来可以访问数据库通过用户名获取整个用户信息 if (!"hello".equals(principal)) { // 代表此时不存在这个用户 return null; } // 参数1为用户名;参数2为密码(口令);参数3为一个名字,可以用父类的getName()获取名称 return new SimpleAuthenticationInfo("hello", "e10adc3949ba59abbe56e057f20f883e", getName()); } }
-
如果采用了Salt
对密码进行加密,需要在返回SimpleAuthenticationInfo
值的构造方法上进行添加:
ByteSource.Util.bytes("Salt字符串")
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
System.out.println("token中的用户名为:" + principal);
// 接下来可以访问数据库通过用户名获取整个用户信息
if (!"hello".equals(principal)) {
// 代表此时不存在这个用户
return null;
}
// 参数1为用户名;参数2为密码(口令);参数3为通过ByteSource转换的salt;参数4为一个名字,可以用父类的getName()获取名称
return new SimpleAuthenticationInfo("hello", "d83dd1679625748d33a1ce05de8aa1aa", ByteSource.Util.bytes("00e1adff-cb31-4f83-8dc0-93d1101ffdef"), getName());
}
如果密码散列了多次,还需要在自定义Relam
类的构造方法设置凭据匹配器时设置散列次数:
-
public MyRealm() { HashedCredentialsMatcher md5 = new HashedCredentialsMatcher("md5"); md5.setHashIterations(1024); setCredentialsMatcher(md5); }
授权
控制谁能访问哪些资源,对于某些资源没有权限是无法访问的
授权可以理解为who
对what
进行how
操作
who
:主体subject
,需要访问系统中的资源what
:为资源例如商品信息、某个类方法等how
:主体最资源的操作许可、操作权限,权限不能离开资源
授权一般是在认证成功后进行授权
授权的方式:
-
基于角色访问控制
-
RBAC
:Role-Based Access Control
-
检查某个用户是否有相应权限
if(subject.hasRole("用户")) { .... }
-
-
基于资源访问控制
-
RBAC
:Resource-Based Access Control
-
检查这个资源是否有权限,具体含义为检查检查某个模块是否有操作某个资源的权限
if(subject.isPermission("资源标识符:操作名称:具体的资源实例标识")) { ... }
-
权限字符串:资源标识符:操作:资源实例标识符
- 权限字符串可以用
*
标识,*
代表所有权限
授权的实现
实现方案有两种:
-
编程式
-
if (subject.hasRole("admin")) { } else { }
-
-
注解式
-
@RequiresRoles("admin") public void method() { } @RequiresPermissions("*:*:*") public void method() { }
-
只有认证通过的用户才能够判断是否有某个权限,否则进行判断是否有某个权限时,结果使用为false
基于单角色的权限访问控制
即只需要判断一个用户只需要有其中的一种角色就可以
进行判断权限时,将会调用protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
方法,每进行一个权限判断,都会执行一次这个方法
-
在这个方法的参数中,可以调用
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
获取主身份(用户名) -
通过主身份可以查询数据库中的用户的权限,并将查到的权限放入到一个
Set<String>
集合中 -
通过
return new SimpleAuthorizationInfo(Set<String> roles)
将权限列表返回 -
如果返回的这个对象中没有任何的内容或者
null
,代表此时无任何权限 -
public static void main(String[] args) { SecurityUtils.setSecurityManager(new DefaultSecurityManager(new MyRealm2())); Subject subject = SecurityUtils.getSubject(); System.out.println(subject.hasRole("管理员")); subject.login(new UsernamePasswordToken("hello", "123456")); if (subject.isAuthenticated()) { System.out.println(subject.hasRole("管理员")); } }
-
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String primaryPrincipal = (String) principals.getPrimaryPrincipal(); // 查询数据库语句 return new SimpleAuthorizationInfo(Set.of("管理员", "用户")); }
基于多角色的权限访问控制
需要判断一个用户有多个角色时,只需要修改授权判断就可以
if (subject.isAuthenticated()) {
System.out.println(subject.hasAllRoles(List.of("管理员", "用户")));
}
以上代码中,只有这个用户同时拥有管理员和用户权限时,结果为true
也可以通过subject.hasRoles(List.of("管理员", "用户", "老师"))
判断这个主体针对多个角色是否有相应的权限,结果返回一个boolean[]
基于权限字符串的权限访问控制
权限字符串:资源标识符:操作:资源实例标识符
,例如:
- 返回权限如果最后是
*
,那么可以省略,但必须要保证至少有一个- 例如
user:update:*
可以写为user:update
user:*:*
可以写为user
- 但当某个角色拥有全部权限时,不能写为
*
- 例如
- 返回的权限:
user:update
,那么此时user:update:*
、user:update:1
、user:update:akfasdkfja
等都是合法的,但user:*:1
、user:insert:3
等都是不合法的 - 需要判断的权限只要比返回的权限小或者相等,那么这时候都是合法的
可以通过subject.isPermitted("权限字符串")
进行判断是否有某项权限
在protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
方法中的返回值中可以添加一些权限的字符串
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
// 查询数据库语句
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(Set.of("管理员", "用户"));
// 添加权限字符串
simpleAuthorizationInfo.addStringPermission("user:update:*");
return simpleAuthorizationInfo;
}
System.out.println("=============基于权限字符串==============");
if (subject.isAuthenticated()) {
// 判断有单个权限
System.out.println("subject.isPermitted(\"user:update:1\") = " + subject.isPermitted("user:update:1"));
// 判断是否拥有一下全部权限
System.out.println("subject.isPermittedAll(\"user:update:2\", \"user:update:3\", \"user:insert:3\", \"user:update:*\", \"user:update\") = " + subject.isPermittedAll("user:update:2", "user:update:3", "user:insert:3", "user:update:*", "user:update"));
// 判断对各项权限的拥有情况
System.out.println("Arrays.toString(subject.isPermitted(\"user:update:2\", \"user:update:3\", \"user:insert:3\", \"user:update:*\", \"user:update\")) = " + Arrays.toString(subject.isPermitted("user:update:2", "user:update:3", "user:insert:3", "user:update:*", "user:update")));
}
Spring Boot整合Shiro
将所有的请求都交给ShiroFilter
处理
引入依赖:
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-starter -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.9.0</version>
</dependency>
需要一个配置类:
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
shiroFilterFactoryBean.setLoginUrl("/main/login");
shiroFilterFactoryBean.setFilterChainDefinitionMap(new HashMap<>(){
{
put("/", "authc");
}
});
return shiroFilterFactoryBean;
}
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(MyRealm2 myRealm2) {
return new DefaultWebSecurityManager(myRealm2);
}
@Bean
public MyRealm2 myRealm2() {
return new MyRealm2();
}
}
以上代码的第11行有authc
字样:
- 这是一个过滤器的简称,shiro提供了多个过滤器
- Spring Boot会自定将安全管理器注入到
SecurityUtils
中 subject.logout
可以退出登录
前后端分离的项目中,在配置ShiroFilterFactoryBean
时,无需再进行配置拦截路径
是通过JSESSIONID
判断登录状态的
注解式权限控制:
-
以下代码为简单模拟的用户名之后带有
n
的用户才会被赋予角色n
和权限admin:*:*
-
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String primaryPrincipal = (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); if (primaryPrincipal.endsWith("n")) { simpleAuthorizationInfo.addRole("n"); simpleAuthorizationInfo.addStringPermission("admin:*:*"); } return simpleAuthorizationInfo; }
-
@GetMapping("/status") @RequiresRoles("n") @RequiresPermissions("admin:add:user") public String status() { return "有权限"; }
如果没有权限,将会被控制器执行这个方法时抛出
org.apache.shiro.authz.UnauthorizedException
(没有权限)异常,如果在其他有权限的方法中调用这个方法,会正常执行,将不会抛出异常!!例如
admina
用户是没有这个权限的,所以admina
请求/status
时会抛出没有权限的异常,但admina
有权限访问/login
,/login
的处理方法中的第6
行调用了status
方法,此时将会正常的执行@PostMapping("/login") public String login(@RequestParam String username, @RequestParam String password) { Subject subject = SecurityUtils.getSubject(); subject.login(new UsernamePasswordToken(username, password)); System.out.println("subject.isAuthenticated() = " + subject.isAuthenticated()); status(); return "登录"; } @GetMapping("/status") @RequiresRoles("n") @RequiresPermissions("admin:add:user") public String status() { System.out.println("执行了"); return "有权限"; } @ExceptionHandler({AuthenticationException.class, Exception.class}) public String processException(Exception e) { e.printStackTrace(); if (e instanceof AuthenticationException) { return "用户名不存在或者密码错误"; } return "没有权限"; }
数据库设计
用户->角色->权限->资源
用户和角色、角色和权限是多对多的关系
在设计实体类时,将角色的List
类型的集合放入到用户类中,将权限的List
类型的集合放入到角色类中
Shiro中的缓存
如果一个用户经过了权限认证,那么可以将这个数据暂时的缓存起来,以减轻数据库的压力,一般放入缓存的数据都是增删改比较少的数据
Shiro
提供了CacheManager
,以用来管理缓存
可以使用ehCache
作为缓存管理器:
-
在配置类中返回自定义
Realm
实例的方法中进行配置 -
引入依赖
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-ehcache --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.9.0</version> </dependency>
-
配置信息大致如下:
@Bean public MyRealm myRealm() { MyRealm myRealm = new MyRealm(); myRealm.setCacheManager(new EhCacheManager()); // 开启全局缓存 myRealm.setCachingEnabled(true); // 开启认证缓存 myRealm.setAuthorizationCachingEnabled(true); // 开启授权缓存 myRealm.setAuthorizationCachingEnabled(true); myRealm.setAuthenticationCacheName("设置认证缓存的名字"); myRealm.setAuthorizationCacheName("设置授权缓存的名字"); return myRealm; }
整合Redis
- 引入
Spring Boot
操作Redis
的依赖 - 自定义一个类实现
CacheManager
接口,并实现相应方法- 将返回值设置为
new 下一步的实现类
- 将返回值设置为
- 再使用自定义类实现
Cache
接口
@Bean
public MyRealm myRealm() {
MyRealm myRealm = new MyRealm();
// 设为自己的缓存管理器
myRealm.setCacheManager(new MyCacheManager());
// 开启全局缓存
myRealm.setCachingEnabled(true);
// 开启认证缓存
myRealm.setAuthorizationCachingEnabled(true);
// 开启授权缓存
myRealm.setAuthorizationCachingEnabled(true);
myRealm.setAuthenticationCacheName("设置认证缓存的名字");
myRealm.setAuthorizationCacheName("设置授权缓存的名字");
return myRealm;
}
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
public class MyCacheManager implements CacheManager {
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
System.out.println("getCache:" + s);
return new MyRedisCache<>();
}
}
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import java.util.Collection;
import java.util.Set;
public class MyRedisCache<K, V> implements Cache<K, V> {
@Override
public V get(K k) throws CacheException {
return null;
}
@Override
public V put(K k, V v) throws CacheException {
return null;
}
@Override
public V remove(K k) throws CacheException {
return null;
}
@Override
public void clear() throws CacheException {
}
@Override
public int size() {
return 0;
}
@Override
public Set<K> keys() {
return null;
}
@Override
public Collection<V> values() {
return null;
}
}
- 此时需要重写以上的方法,
K
的中的值通常为用户名 V
类型为SimpleAuthorizationInfo
,可以通过getRoles()
获取到所有的角色信息,通过getObjectPermissions()
获取到所有的权限信息- 可以使用Redis存储二进制信息,通过反序列化将对象取出来
Q.E.D.