一、什么是Shiro
权限体系在现代软件应用中有着非常重要的地位。一个应用如果没有权限体系都会显着这个系统“特别不安全”,无论是传统的MIS系统还是互联网项目出于对业务数据和应用自身的安全,都会设置自己的安全策略。 目前市场上专门的Java权限框架有Apache Shiro 和 Spring Security。相较于Spring Security 来说 Shiro更加老牌。学习好Shiro对于以后市场上在出现新型权限框架的学习能带来很大便利。因为权限的概念是不变的,变得是框架的实现方式。当然了,对于第一次学习权限框架的人来说,相较于权限框架的应用,更难的就是权限方面的概念。
(一)核心功能
- Authentication 认证。主要用于处理用户的登录并且做认证。
- Authorization 授权。用户是否有权限访问指定URL等。
- Cryptography 密码学。如密码的加密。
- Session Management Session 管理。
- Web Integration Web集成。Shiro不依赖于容器。
(二) shiro架构
2 Subject
主体。每个用户登录成功后都会对应一个Subject对象,所有用户信息都存放在Subject中。可以理解:Subject就是Shiro提供的用户实体类。
3 Security Manager
Shiro最大的容器,此容器中包含了Shiro的绝大多数功能。在非Spring Boot项目中,获取Security Manager 是编写代码的第一步。而在Spring Boot中已经帮助我们自动化配置了。
4 Authenticator
认证器。执行认证过程调用的组件。里面包含了认证策略。
5 Authorizer
授权器。执行授权时调用的组件。
6 Session Manager
Shiro被Web集成后,HttpSession对象会由Shiro的Session Manager进行管理。
7 Cache Manager
缓存管理。Shiro执行很多第三方缓存技术。例如:EHCache等。
8 Session DAO
操作Session内容的组件。
9 Realms
Shiro框架实现权限控制不依赖于数据库,通过内置数据也可以实现权限控制。但是目前绝大多数应用的数据都存储在数据库中,所以Shiro提供了Realms组件,此组件的作用就是访问数据库。Shiro内置的访问数据库的代码,通过简单配置就可以访问数据库,也可以自定义Realms实现访问数据库逻辑(绝大多数都这么做)
理解这张图片
(三) shiro的全局配置文件
shiro的全局配置文件是ini格式的,ini中放置数据,比如用户、角色、权限等,由于我们会将这些数据放在数据库中而不是固定在ini文件中所以本篇文章不详细介绍全局配置文件了。
(四)shiro的过滤器
理解shiro的过滤器是理解shiro的重中之重,shiro就是凭借这些过滤器来做权限或认证拦截的。
- anon:不认证也可以访问。例如:/admin/**=anon /login=anon
- authc:必须认证。 /**=authc —所有的资源都认证
- authcBasic:没有参数时表示httpBasic认证(客户端认证方式)。
- logout:退出。
- noSessionCreation:新增Filter,表示没有Session创建。
- perms:判断是有具有指定权限。例如:/admin/user/**=perms[“per1”,”per2”]。必须同时具有给定权限才可以访问。如果只有一个权限可以省略双引号。
- port:限制端口。例如:/admin/**=port[8081]。只要请求不是8081端口就重新发送URL到8081端口。
- rest:请求方式和权限的简便写法。例如:/admin/**=rest[user],相当于/admin/** = perms[user:方式],方式是http请求的方式:post、get等。
- roles:判断是否具有指定角色。/admin/**=roles[role1]
- ssl:表示是安全的请求。协议为https
- user:表示必须存在用户。
二、shiro认证和授权以及自定义配置
在说这两个核心功能之前我想再次强调一件事:在springboot中使用shiro并不需要我们手动去配置securityManager对象,我们通常会为shiro做自定义配置,让项目启动时就将securityManager对象创建好,我们只需要用就可以了
(一)认证
我们一般是通过subject来使用shiro的功能的,比如在登录中我们需要为用户做认证,我们需要为用户名和密码创建一个userNamePasswordtoken对象,再将这个token对象传到subject的login方法中,shiro会为我们做完剩下的数据校验以及认证工作
@RequestMapping("userLogin")
public String userLogin(String uname,String pwd,@RequestParam(defaultValue = "false") Boolean rm){
//1.创建token对象
UsernamePasswordToken token = new UsernamePasswordToken(uname, pwd,rm);
//2.获取subject对象
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
session.setAttribute("user",uname);
//3.执行登录
subject.login(token);
//4.页面跳转到main
return "main";
}
这时候我们会产生疑问?shiro要做数据比对首先得有我们的真实数据才行啊,怎么可能只通过用户输入的用户名和密码就完成登录校验的功能呢?没错,我们还需要增加一点配置才能完成我们的登录功能。
Realm登场
Realm就是shiro中获取数据的模块,默认realm是从shiro的全局配置文件中获取数据,但是在项目中我们会将数据放在数据库中,为了实现登录功能,我们需要重写realm,让shiro从数据库中拿到用户数据。
@Component
public class MyRealm extends AuthorizingRealm {
@Autowired
UserService service;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 情景:当用户进行登录认证的时候shiro底层会使用该方法获取认证信息,但shiro默认向shiro.ini文件获取
* 目的:使得shiro向数据库获取认证信息,并且做一些自定义操作,比如设置加密的盐等等
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取用户的身份信息
String uname = authenticationToken.getPrincipal().toString();
//2.根据用户名在数据库中查找用户
User user = service.selUserService(uname);
//3.判断用户是否存在
if(user != null){
//得到数据库密码
String pwd = user.getPwd();
//将身份信息与数据库中的密码还有盐存放到认证信息中以便于shiro的认证模块做比对
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(authenticationToken.getPrincipal(), pwd, ByteSource.Util.bytes("codexie"), uname);
return info;
}
return null;
}
}
(二)授权
授权分为两个部分,第一个部分是权限信息的获取,shiro需要知道这个登录的用户具备哪些权限,第二部分是权限认证,一般是通过在单元方法上加注解来表示这个单元方法只能被具备特定角色或权限的用户访问。 权限信息的获取 根据之前对realm的描述我们可以知道权限信息的获取也是通过自定义realm来实现
@Component
public class MyRealm extends AuthorizingRealm {
@Autowired
UserService service;
/**
* 情景:当用户进行权限认证的时候shiro底层会使用该方法获取认证信息,但shiro默认向shiro.ini文件获取
* 目的:向数据库中选取该用户的权限信息,若没使用缓冲池则每次授权操作都需要执行一次该方法
* @param principalCollection
* @return AuthorizationInfo 授权信息对象
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//1.获取身份信息
String pricipal = principalCollection.getPrimaryPrincipal().toString();
//2.根据身份获取、角色权限等信息
List<String> roles = service.getRolesService(pricipal);
List<String> permissions = service.getPermissionsService(pricipal);
//3.将角色、权限添加到授权信息里面
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roles);
info.addStringPermissions(permissions);
return info;
}
/**
* 情景:当用户进行登录认证的时候shiro底层会使用该方法获取认证信息,但shiro默认向shiro.ini文件获取
* 目的:使得shiro向数据库获取认证信息,并且做一些自定义操作,比如设置加密的盐等等
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取用户的身份信息
String uname = authenticationToken.getPrincipal().toString();
//2.根据用户名查找用户
User user = service.selUserService(uname);
//3.判断用户是否存在
if(user != null){
//得到数据库密码
String pwd = user.getPwd();
//将身份信息与数据库中的密码还有盐存放到认证信息中以便于shiro的认证模块做比对
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(authenticationToken.getPrincipal(), pwd, ByteSource.Util.bytes("codexie"), uname);
return info;
}
return null;
}
}
权限认证
@RequestMapping("psCheck")
@RequiresPermissions({"user:add","user:delete","admin:delete"})
@ResponseBody
public String psCheck(){
return "权限认证成功";
}
@RequestMapping("adminCheck")
@RequiresRoles("admin")
@ResponseBody
public String adminCheck(){
return "角色认证成功";
}
常用注解
- @RequiresAuthentication
验证用户是否登录,等同于方法subject.isAuthenticated() 结果为true时。 - @RequiresUser
验证用户是否被记忆,user有两种含义: 一种是成功登录的(subject.isAuthenticated() 结果为true); 另外一种是被记忆的(subject.isRemembered()结果为true)。 - @RequiresGuest
验证是否是一个guest的请求,与@RequiresUser完全相反。 换言之,RequiresUser == !RequiresGuest。 此时subject.getPrincipal() 结果为null.也就是所谓的游客 - @RequiresRoles
例如:@RequiresRoles(“aRoleName”); void someMethod(); 如果subject中有aRoleName角色才可以访问方法someMethod。如果没有这个权限则会抛出异常AuthorizationException。 - @RequiresPermissions
例如: @RequiresPermissions({“file:read”, “write:aFile.txt”} ) void someMethod(); 要求subject中必须同时含有file:read和write:aFile.txt的权限才能执行方法someMethod()。否则抛出异常AuthorizationException
shiro整合Ehcache
我们在做权限认证的时候,每做一次权限认证就需要获取一次权限信息(也就是走数据库),这样是非常消耗资源并且用户体验不好的,我们可以在第一次获取完用户的权限信息,若用户在规定时间内再做授权认证时直接从缓冲池里拿数据而不是从数据库中拿。Ehcache就是一个非常方便我们使用的缓冲池。
Ehcache简介
EHCache是sourceforge的开源缓存项目,现已经具有独立官网,网址:(http://www.ehcache.org)。其本身是纯JAVA实现的,所以可以和绝大多数Java项目无缝整合,例如:Hibernate的缓存就是基于EHCache实现的。 EHCache支持内存和磁盘缓存,默认是存储在内存中的,当内存不够时允许把缓存数据同步到磁盘中,所以不需要担心内存不够问题。 EHCache支持基于Filter的Cache实现,同时也支持Gzip压缩算法提高响应速度。 ehcache直接在jvm虚拟机中缓存,速度快,效率高;但是缓存共享麻烦,集群分布式应用不方便。
Ehcache的使用
属性含义: maxElementsInMemory:缓存中允许创建的最大对象数。 eternal:缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期。 timeToIdleSeconds:缓存数据的钝化时间,取值0表示无限长。 timeToLiveSeconds:缓存数据的生存时间,取值0表示无限长。 overflowToDisk:内存不足时,是否启用磁盘缓存。 memoryStoreEvictionPolicy:缓存满了之后的淘汰算法。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="ehcache" updateCheck="false">
<!-- 磁盘缓存位置 -->
<diskStore path="java.io.tmpdir"/>
<!-- 默认缓存 -->
<defaultCache
maxEntriesLocalHeap="1000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="false">
</defaultCache>
<!-- 记录用户的授权信息的缓存:缓存用户的角色和权限 -->
<cache name="passwordRetryEhcache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="60"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
api演示
public static void main(String[] args) {
/* String property = System.getProperty("java.io.tmpdir");
System.out.println(property);*/
//根据配置文件创建cacheManager对象
CacheManager cacheManager=CacheManager.create("D:\\SXTCourse\\Testshiro\\uu\\src\\main\\resources\\each.xml");
//根据缓冲池名字获取缓冲池
Cache cache = cacheManager.getCache("HelloWorldCache");
//创建一个缓存对象
Element element=new Element("key","sxt");
//将该对象放入缓冲池
cache.put(element);
//通过键名向缓冲池中取对象
Element element1 = cache.get("key");
System.out.println(element1);
}
将Ehcache整合到shiro
我们只用在配置securityManager的bean的时候设置ehcacheManager为securityManager的cacheManager就可以了 代码片段
/**得到一个securityManager对象,该对象是全局单例的
* 我们要为它设置自定义realm模块、rememberMeManager模块
* @return
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
//1.创建securManager对象
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//2.创建matcher
myMatcher.setHashAlgorithmName("md5");
myMatcher.setHashIterations(3);
//3.将matcher设置到realm中
myRealm.setCredentialsMatcher(myMatcher);
//4.集中设置securityManager
securityManager.setRealm(myRealm);
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setCacheManager( ehCacheCacheManager());
return securityManager;
}
@Bean
public EhCacheManager ehCacheCacheManager(){
//1.创建ehCacheManager
EhCacheManager ehCacheManager = new EhCacheManager();
//2.读取配置文件
InputStream in = null;
try {
in = ResourceUtils.getInputStreamForPath("classpath:ehcache/ehcache-shiro.xml");
} catch (IOException e) {
e.printStackTrace();
}
//3.根据配置文件创建cacheManager对象
net.sf.ehcache.CacheManager cacheManager = new net.sf.ehcache.CacheManager(in);
//4.将cacheManager对象传给ehCacheManager对象
ehCacheManager.setCacheManager(cacheManager);
return ehCacheManager;
}
配置完后,shiro默认就会从我们的默认缓冲池中拿权限信息,如果没有则会调用service拿取数据
自定义配置matcher
(一)matcher使用的时机
matcher是在用户执行登录(认证操作)的时候,shiro的认证模块会调用realm中matcher的doCredentialsMatch方法做信息比对,通过该方法返回的布尔值来决定认证是否成功
使用场景
现在有如下场景,当我们登录失败时服务器要记住用户登录失败的次数,若用户连续登录失败三次,则告知用户账号被锁定十分钟,若用户在三次内输入正确密码则清空之前的失败次数。 思路:我们可以使用缓冲池解决此问题,若用户登录失败则将用户名作为键,失败次数作为值放入缓冲池中,若用户连续输错则将值+1,当值大于等于3的时候抛出异常告诉用户账号被锁定。
/**
* 该类的作用是对HashedCredentialsMatcher的功能进行封装,采用静态代理的设计模式
* 具体功能:若用户在十分钟内连续输错三次则用户将被锁定十分钟
* 思路:采用ehCacheManager的特点,设置一个数据钝化时间为10分钟的缓冲池
* 当用户登录时判断用户的输错次数是否大于等于3,若是则不用进行数据库的比对,直接抛出异常
* 若不是则跟数据库进行比对,比对成功则清空之前的记录,比对失败则将失败次数+1
*/
@Component
public class MyMatcher extends HashedCredentialsMatcher {
private Ehcache passwordEhcache;
public MyMatcher(EhCacheManager ehCacheCacheManager) {
Ehcache passwordRetryEhcache = ehCacheCacheManager.getCacheManager().getEhcache("passwordRetryEhcache");
passwordEhcache = passwordRetryEhcache;
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
int i = 0;
//1.得到uname
String uname = token.getPrincipal().toString();
//2.根据uname得到缓冲池中的密码
Element element = passwordEhcache.get(uname);
//3.若该用户名对应的元素为空则将其初始化并放入缓冲池
if(element == null){
element = new Element(uname,0);
passwordEhcache.put(element);
}else{ //4.否则比较该元素的值是否大于等于3,若是则抛异常
if((Integer) element.getObjectValue() >= 3){
throw new ExcessiveAttemptsException();
}
}
//5.调用父级方法做信息核验
boolean match = super.doCredentialsMatch(token, info);
//6.若核验成功则移除之前的记录
if(match){
passwordEhcache.remove(uname);
}else{
//否则将失败记录+1
Integer integer = (Integer) element.getObjectValue();
integer++;
passwordEhcache.put(new Element(uname,integer));
System.out.println("失败次数:"+integer);
}
return match;
}
}
shiro在springboot中的配置
@Configuration
public class ShiroConfig {
@Autowired
MyRealm myRealm;
@Autowired
MyMatcher myMatcher;
/**得到一个securityManager对象,该对象是全局单例的
* 我们要为它设置自定义realm模块、rememberMeManager模块
* @return
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
//1.创建securManager对象
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//2.创建matcher
myMatcher.setHashAlgorithmName("md5");
myMatcher.setHashIterations(3);
//3.将matcher设置到realm中
myRealm.setCredentialsMatcher(myMatcher);
//4.集中设置securityManager
securityManager.setRealm(myRealm);
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setCacheManager( ehCacheCacheManager());
return securityManager;
}
@Bean
public EhCacheManager ehCacheCacheManager(){
//1.创建ehCacheManager
EhCacheManager ehCacheManager = new EhCacheManager();
//2.读取配置文件
InputStream in = null;
try {
in = ResourceUtils.getInputStreamForPath("classpath:ehcache/ehcache-shiro.xml");
} catch (IOException e) {
e.printStackTrace();
}
//3.根据配置文件创建cacheManager对象
net.sf.ehcache.CacheManager cacheManager = new net.sf.ehcache.CacheManager(in);
//4.将cacheManager对象传给ehCacheManager对象
ehCacheManager.setCacheManager(cacheManager);
return ehCacheManager;
}
/**
* 目的:为Shiro的rememberMe模块设置自定义cookie
* 主要是设置cookie的范围、存在时间等
* @return
*/
public SimpleCookie myCookie(){
SimpleCookie cookie = new SimpleCookie("rememberMe");
cookie.setMaxAge(60*60*24*10);
cookie.setPath("/");
return cookie;
}
/**
* 创建CookieRememberMeManager对象,将自定义cookie传给它
* @return
*/
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
rememberMeManager.setCookie(myCookie());
rememberMeManager.setCipherKey("1234567890987654".getBytes());
return rememberMeManager;
}
/**
* 初始化shiro各权限过滤器的范围
* @return
*/
@Bean
public DefaultShiroFilterChainDefinition shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
//将登录页面的请求设置成不需要认证
chainDefinition.addPathDefinition("/userController/login","anon");
//将用户登录的请求设置成不需要认证
chainDefinition.addPathDefinition("/userController/userLogin","anon");
//设置退出的过滤器
chainDefinition.addPathDefinition("/loginOut","logout");
//其它的所有请求设置成需要用户
chainDefinition.addPathDefinition("/**","user");
return chainDefinition;
}
}
项目地址以及shiro工作流程
https://github.com/bigWhiteXie/Shiro-Example.git
|