权限体系在现代软件应用中有着非常重要的地位。一个应用如果没有权限体系都会显着这个系统“特别不安全”,无论是传统的MIS系统还是互联网项目出于对业务数据和应用自身的安全,都会设置自己的安全策略。
目前市场上专门的Java权限框架有Apache Shiro 和 Spring Security。相较于Spring Security 来说 Shiro更加老牌。学习好Shiro对于以后市场上在出现新型权限框架的学习能带来很大便利。因为权限的概念是不变的,变得是框架的实现方式。当然了,对于第一次学习权限框架的人来说,相较于权限框架的应用,更难的就是权限方面的概念。
- Authentication 认证。主要用于处理用户的登录并且做认证。
- Authorization 授权。用户是否有权限访问指定URL等。
- Cryptography 密码学。如密码的加密。
- Session Management Session 管理。
- Web Integration Web集成。Shiro不依赖于容器。
主体。每个用户登录成功后都会对应一个Subject对象,所有用户信息都存放在Subject中。可以理解:Subject就是Shiro提供的用户实体类。
3 Security ManagerShiro最大的容器,此容器中包含了Shiro的绝大多数功能。在非Spring Boot项目中,获取Security Manager 是编写代码的第一步。而在Spring Boot中已经帮助我们自动化配置了。
4 Authenticator认证器。执行认证过程调用的组件。里面包含了认证策略。
5 Authorizer授权器。执行授权时调用的组件。
6 Session ManagerShiro被Web集成后,HttpSession对象会由Shiro的Session Manager进行管理。
7 Cache Manager缓存管理。Shiro执行很多第三方缓存技术。例如:EHCache等。
8 Session DAO操作Session内容的组件。
9 RealmsShiro框架实现权限控制不依赖于数据库,通过内置数据也可以实现权限控制。但是目前绝大多数应用的数据都存储在数据库中,所以Shiro提供了Realms组件,此组件的作用就是访问数据库。Shiro内置的访问数据库的代码,通过简单配置就可以访问数据库,也可以自定义Realms实现访问数据库逻辑(绝大多数都这么做)
理解这张图片
shiro的全局配置文件是ini格式的,ini中放置数据,比如用户、角色、权限等,由于我们会将这些数据放在数据库中而不是固定在ini文件中所以本篇文章不详细介绍全局配置文件了。
(四)shiro的过滤器理解shiro的过滤器是理解shiro的重中之重,shiro就是凭借这些过滤器来做权限或认证拦截的。
- anon:不认证也可以访问。例如:/admin
@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; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //1.获取身份信息 String pricipal = principalCollection.getPrimaryPrincipal().toString(); //2.根据身份获取、角色权限等信息 Listroles = service.getRolesService(pricipal); List permissions = service.getPermissionsService(pricipal); //3.将角色、权限添加到授权信息里面 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addRoles(roles); info.addStringPermissions(permissions); return info; } @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
我们在做权限认证的时候,每做一次权限认证就需要获取一次权限信息(也就是走数据库),这样是非常消耗资源并且用户体验不好的,我们可以在第一次获取完用户的权限信息,若用户在规定时间内再做授权认证时直接从缓冲池里拿数据而不是从数据库中拿。Ehcache就是一个非常方便我们使用的缓冲池。
Ehcache简介EHCache是sourceforge的开源缓存项目,现已经具有独立官网,网址:(http://www.ehcache.org)。其本身是纯JAVA实现的,所以可以和绝大多数Java项目无缝整合,例如:Hibernate的缓存就是基于EHCache实现的。
Ehcache的使用
EHCache支持内存和磁盘缓存,默认是存储在内存中的,当内存不够时允许把缓存数据同步到磁盘中,所以不需要担心内存不够问题。
EHCache支持基于Filter的Cache实现,同时也支持Gzip压缩算法提高响应速度。
ehcache直接在jvm虚拟机中缓存,速度快,效率高;但是缓存共享麻烦,集群分布式应用不方便。属性含义:
maxElementsInMemory:缓存中允许创建的最大对象数。
eternal:缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期。
timeToIdleSeconds:缓存数据的钝化时间,取值0表示无限长。
timeToLiveSeconds:缓存数据的生存时间,取值0表示无限长。
overflowToDisk:内存不足时,是否启用磁盘缓存。
memoryStoreEvictionPolicy:缓存满了之后的淘汰算法。api演示
public static void main(String[] args) { //根据配置文件创建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就可以了
代码片段@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的时候抛出异常告诉用户账号被锁定。@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; @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; } public Simplecookie mycookie(){ Simplecookie cookie = new Simplecookie("rememberMe"); cookie.setMaxAge(60*60*24*10); cookie.setPath("/"); return cookie; } public cookieRememberMeManager rememberMeManager(){ cookieRememberMeManager rememberMeManager = new cookieRememberMeManager(); rememberMeManager.setcookie(mycookie()); rememberMeManager.setCipherKey("1234567890987654".getBytes()); return rememberMeManager; } @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
- @RequiresAuthentication



