前言:断断续续更新了一两周才写完,写文章只要是为了总结自己所学所用,次要目的便是可以帮助他人。这里就对最近Shiro的使用与学习做个总结。
一.项目的构建
这里是总结在SpringBoot项目中我们该如何使用Shiro的,怎么样一步步将项目构建出来,并在每个步骤注意些什么。附上一张Shiro的架构图:
1.导入依赖
需要两项依赖,一个是shiro的一个是redis的,因为使用redis作缓存管理,所以这里也需要redis的依赖。
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2.自定义Realm
通过实现AuthorizationRealm来实现自定义Realm,这样自定义Realm就可以拥有doGetAuthenticationInfo、doGetAuthorizationInfo两个方法了,然后分别在两个方法中去获取用户信息与权限信息,两个方法的返回对象分别是AuthenticationInfo与AuthorizationInfo的实现类。通常我们都是使用SimpleAuthenticationIno、SimpleAuthorizationInfo这两个实现类。
模板如下:
public class FirstRealm extends AuthorizingRealm { @Autowired ShiroUserService shiroUserService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("触发授权操作了"); String username = (String)principalCollection.getPrimaryPrincipal(); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); List<ShiroUserPermission> shiroUserPermissionList = shiroUserService.queryShiroUserPermissionByUsername(username); shiroUserPermissionList.stream().forEach( shiroUserPermission -> { if(shiroUserPermission.getRole()!=null){ simpleAuthorizationInfo.addRole(shiroUserPermission.getRole()); } if(shiroUserPermission.getPermission()!=null){ simpleAuthorizationInfo.addStringPermission(shiroUserPermission.getPermission()); } } ); return simpleAuthorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)authenticationToken; ShiroUser shiroUser = shiroUserService.queryUser(usernamePasswordToken.getUsername()); if(shiroUser!=null){ // return new SimpleAuthenticationInfo(shiroUser.getUsername(),shiroUser.getPassword(),ByteSource.Util.bytes(shiroUser.getSalt()),this.getName()); return new SimpleAuthenticationInfo(shiroUser.getUsername(),shiroUser.getPassword(), new MyByteSource(shiroUser.getSalt().toString()),this.getName()); } return null; } }
3.自定义缓存管理器、缓存管理类
缓存管理器一般通过实现CacheManager来获得,缓存管理器的唯一目的就是用来获取缓存管理类的,缓存管理类一般通过实现Cache接口来获得,真正对缓存的操作都是缓存管理类来实现的。
缓存管理器模板如下:
public class MyRedisCacheManager implements CacheManager { /** * 只要加入了缓存管理器,配置了缓存管理类,系统就会默认在查询完认证和授权后将信息放入到缓存中 * 且下次需要认证和授权时,都是优先去查询缓存中的内容,查询不到,才会去查询数据库,这里也验证了 * 这一点,与之前的画的加入缓存后的授权信息的获取图是一样的。 */ @Override public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException { System.out.println("进入到了自定义缓存管理器,传入参数cacheName:"+ cacheName); return new RedisCache<K,V>(cacheName); } }
这些代码注释了一些解析,其实这个不是自动化过程,我们是在注入Realm时显示的为Realm创建了自己定义的缓存管理器(在安全管理器中也可以设置,效果一样),所以系统可以通过Realm操作缓存,可以总结的一句话,Shiro的所有数据操作都要和Realm有关系。包括Session管理器。
缓存管理类模板如下:
@NoArgsConstructor public class RedisCache<k,v> implements Cache<k,v> { private String cacheName; @Autowired public RedisTemplate redisTemplate; public static RedisTemplate redisTemplateSelf; @PostConstruct public void getRedisTemplateSelf(){ this.redisTemplateSelf = redisTemplate; this.redisTemplateSelf.setKeySerializer(new StringRedisSerializer()); this.redisTemplateSelf.setHashKeySerializer(new StringRedisSerializer()); this.redisTemplateSelf.setValueSerializer(new StringRedisSerializer()); } public RedisCache (String cacheName1){ this.cacheName = cacheName1; } @Override public v get(k k) throws CacheException { System.out.println(cacheName+":获取缓存方法,传入参数:" + k+",此时的redisTemplate:"+redisTemplateSelf); //获取缓存中数据时一定要为k加toStirng方法,否则会报错序列化的错 return (v)redisTemplateSelf.opsForHash().get(cacheName.toString(),k.toString()); } @Override public v put(k k, v v) throws CacheException { System.out.println("加入缓存方法,传入参数 K:" + k+",V:"+v); //放入redis中的值,一定要是序列化的对象 redisTemplateSelf.opsForHash().put(cacheName.toString(),k.toString(),v); return null; } @Override public v remove(k k) throws CacheException { System.out.println("调用了remove方法,传入参数:"+k.toString()); redisTemplateSelf.opsForHash().delete(cacheName.toString(),k.toString()); return null; } @Override public void clear() throws CacheException { System.out.println("调用了clear方法"); redisTemplateSelf.delete(cacheName); } @Override public int size() { return redisTemplateSelf.opsForHash().size(cacheName).intValue(); } @Override public Set<k> keys() { return redisTemplateSelf.opsForHash().keys(cacheName); } @Override public Collection<v> values() { return redisTemplateSelf.opsForHash().values(cacheName); } }
4.将shiro的过滤器、安全管理器、Realm等注入到Spring容器中
除了上述几个意外,我们还需要额外注入一个RedisCache这个缓存管理类。因为我们在这个类中使用Autowired注解获取Spring容器中的对象了。所以这个类也要交给容器。
1.注入RedisTemplate
这是自己定义的RedisTemplate的模板,注入进去后会覆盖掉系统默认注入的。
@Bean public RedisTemplate getRedisTemplate(RedisConnectionFactory connectionFactory){ RedisTemplate redisTemplate = new RedisTemplate(); redisTemplate.setConnectionFactory(connectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; }
2.注入缓存管理类:
这个是非必须,只是为了可以在该类中可以注入RedisTemplate。
@Bean public RedisCache getRedisCache(){ RedisCache redisCache = new RedisCache(); return redisCache; }
3.注入Realm
注入Realm时,我们还需要告诉Realm,使用的密码匹配器,是否开启缓存,使用哪种缓存管理器等。此处需要注意的地方有两点,第一授权的缓存管理默认是开启的,可以直接设置缓存管理器就可以使用,第二点,授权与认证的缓存名称一般自己设定,因为在redis中通常是通过hash结构来保存的,即Map<kW ,Map<kN,v>>,kW便是我们设置的授权与认证的缓存名称。
@Bean public FirstRealm getRealm(){ FirstRealm firstRealm = new FirstRealm(); HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("MD5"); hashedCredentialsMatcher.setHashIterations(2048); firstRealm.setCredentialsMatcher(hashedCredentialsMatcher); //开启缓存 firstRealm.setCachingEnabled(true);//开启全局的缓存管理 firstRealm.setAuthenticationCachingEnabled(true);//开启认证缓存 firstRealm.setAuthorizationCachingEnabled(true);//开启授权缓存 //缓存名称很有必要设置,因为若是只使用k,v的形式设置redis缓存,认证和授权默认的k都是用户名,所以我们 //需要使用k,map的形式存储,k就可以是这个设置的缓存名称,缓存管理器中传入的k就是这个值。 firstRealm.setAuthenticationCacheName("authenticationCache");//设置缓存名称--认证 firstRealm.setAuthorizationCacheName("authorizationCache");//设置缓存名称--授权 firstRealm.setCacheManager(new MyRedisCacheManager()); return firstRealm; }
4.注入安全管理器
我们发现无论是认证和授权,都是通过Realm来实现的,甚至是缓存,那么安全管理器负责干什么呢,事实上Realm这些操作也需要安全管理器来管理。那么自然是需要将Realm交给安全管理器了。
@Bean public DefaultWebSecurityManager getDefaultWebSecurityManager(FirstRealm firstReaml){ DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); defaultWebSecurityManager.setRealm(firstReaml); return defaultWebSecurityManager; }
5.注入ShiroFilter
ShiroFilter这里使用的是ShiroFilterFactoryBean。这个过滤器负责拦截一切请求,需要将安全管理器交给他,我们还可以在这里设置认证与授权失败的返回页。
@Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); Map<String,String> map = new HashMap<>(); map.put("/user/*","anon");//表示该资源无需认证授权,无需授权的应该写在上面 map.put("/user/logout","anon");//表示该资源无需认证授权 map.put("/register.jsp","anon");//表示该资源无需认证授权 map.put("/test","anon");//表示该资源无需认证授权 map.put("/redis","anon");//表示该资源无需认证授权 map.put("/login.jsp","anon");//表示该资源无需认证授权,此处不写是不能正常访问到登录页面的, //但是看的课程上是可以访问到,并且无其他配置,这块如果不加,我这里访问不到登录页,会陷入循环的重定向。 map.put("/**","authc");//表示所有资源都需要经过认证授权 shiroFilterFactoryBean.setFilterChainDefinitionMap(map); //设置授权失败返回的页面 shiroFilterFactoryBean.setLoginUrl("login.jsp");//这也是默认值 shiroFilterFactoryBean.setUnauthorizedUrl("error.jsp");//认证失败返回页面 return shiroFilterFactoryBean; }
这里最主要的功能还是拦截用户的请求,对于未认证的用护将其定位到登录页,通过anon与authc来配置不过滤与过滤的地址。
5.使用Shiro完成登录、授权
这里并没有提供数据库与Mybatis相关的实现,这部分与Shiro不是硬相关,这里就不展示了。假设在这些都准备的情况下,我们就可以开始用Shiro了。
1.使用Shiro完成登录
前面已经将Shiro所需要的各种对象注入到了Spring中,我们可以直接通过SucurityUtils来获取Subject的对象,然后登录就行。
@PostMapping("/login") public String login(String username,String password){ UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password); Subject subject = SecurityUtils.getSubject(); try { subject.login(usernamePasswordToken); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误"); return "redirect:/login.jsp"; } catch(IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("密码错误"); return "redirect:/login.jsp"; } return "redirect:/index.jsp"; }
2.使用Shiro实现退出
这一过程与登录没有什么区别,这里也未封装什么工具类,直接写原始代码。
@RequestMapping("logout") public String logout(){ Subject subject = SecurityUtils.getSubject(); subject.logout(); return "redirect:/login.jsp"; }
3.使用Shiro实现授权
这里只是总结了后端的授权实现,前端中jsp与此处并无二致,可以看到我们通常都是通过注解@RequiresPermissions、@RequiresRoles来实现权限的校验(都是使用一种,要么基于角色,要么基于资源,这里只是展示两种)。
@RequiresPermissions("admin:context:delete") @RequiresRoles("admin") @RequestMapping("/context") public String goContext(){ System.out.println("进入内容管理后端接口"); return "redirect:/context.jsp"; }
6.测试授权的效果
完成以上部分,这是我们Shiro的配置以及开发就基本都完成了(Session管理器这里并没有写)。我们测试下缓存是否好用:
经过登录后多次刷新我们可以看到权限信息的获取走的都是缓存而不是doGetAuthorizationInfo方法,这说明缓存是生效的,并且查看redis我们也发现缓存的存储都是正常的。这说明Shiro的使用也是正常的了。
二.总结
1.总结认证流程
下面是笔者总结的破产版流程图,只是总结了数据流向,如图,用户携带用户名密码会被ShiroFilter拦截,查询缓存是否已经拥有了该用户的认证数据,有的话直接进入系统,没有的话,则会进入登录接口,登录接口底层会去调用Realm,Realm再调用数据库,查询用户信息,然后比对,比对通过则将认证信息放入缓存,然后进入系统,比对不通过会提示相应的错误。
2.总结授权流程
下图就是笔者整理的破产版授权流程图,一个请求进入系统最先会被ShiroFilter拦截,然后判断请求的是否是公共资源,是的话就会进入授权过程,授权过程会先进入缓存查询,查不到才会去数据库查,查询到以后则会将授权信息加入缓存,然后进入接口内部。若是请求的是受限资源则必须先必须判断是否登录了。登录了才可以继续从新请求,如果已登录也会进入授权流程。就像图中所描述的那样。
我们可以发现其实Shiro的使用其实很简单,他的架子搭起来也很容易,这是一款简单易用的权限架构,也是目前被使用最广泛的一种权限管理框架之一,Shiro的总结系列到这里就结束了。希望自己的总结也可以帮到路过的你。
(审核说这篇文章无意义?不是it文章?你知道什么是shiro?)