Shiro的用法以及配置
shiro认证的流程(非常重要)
shiro认证流程.png认证流程.png
为什么要重写realm?如何重写realm?
在执行调用subject.login(token)方法后,会把subject以及token都传进去
(subject是从环境中取出的,也就是说subject是可以代表当前用户所处的上下文环境,也即是说可以拿到当前环境的realm的真实数据);
程序会先判断账号,再判断密码。
-
为什么要重写realm?
因为在web开发中,我们需要realm内的真实数据是我们自己查出来的数据库中的数据,
包含我们自己的逻辑,所以我们需要重写realm以存放我们的数据 -
如何去重写realm?
Realm的继承体系
使用AuthorizingRealm来继承
shrio进行认证的底层的逻辑主要在realm.doGetAuthenticationInfo(token)中,原生的方法里
(在一开始创建securityManager实例对象的时候,会将用户指定的realm(ini方式)加载进环境)
有个getUser->realm.getUser(upToken.getUsername())(realm在初始化securityManager的时候就加载进内存,所以这里的数据源是从环境中来的)方法来获得account(这就是一个AuthenticationInfo),如果此时account为空,那么就不再判断密码而是直接报错出来,
如果这个account有值,那么程序会拿着这个account继续往下判断密码
而我们的逻辑主要在于一开始的数据从哪来,怎么进行第一步的判断账号
所以 我们重写realm主要就是重写doGetAuthenticationInfo(token)方法,在该方法中使用service来从数据库获得数据,并进行初次判断。
因为原生的realm里doGetAuthenticationInfo(token)方法中只进行用户账号的判断,然后将account(info)返回,交给后续程序处理(realm.assertCredentialsMatch(token, info)),这个info是包含数据源中的信息的,相当于一个标准,用来被比较。)
所以在我们重写的只需要把一个包含我们doGetAuthenticationInfo(token)方法中,只需要把标准的密码封装到一个info对象即可,这里自己
new SimpleAuthenticationInfo(employee,employee.getPassword(),getName());将其返回就好,剩下的交给shrio。
@Autowired
private IEmployeeService employeeService;
/*通过注入的方式给realm设置凭证匹配器*/
@Autowired
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
super.setCredentialsMatcher(credentialsMatcher);
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
Employee employee = employeeService.getByUsername((String)token.getPrincipal());
if(employee!=null){
return new SimpleAuthenticationInfo(employee,employee.getPassword(), ByteSource.Util.bytes(employee.getName()),getName());
}
return null;
}
- 重写了realm,我们就需要让shiro知道使用我们自定义的数据源
JavaSE:通过ini配置文件告知
JavaEE: 通过spring的配置文件,在安全管理器中的bean配置
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myWebRealm"/>
</bean>
怎么使用Spring和shrio进行认证
在JavaSE中,我们只是在做简单的验证模拟,这时我们已经是在做登录操作了!
那么在JavaEE中,我们要怎么判断用户的哪些行为是登录操作?
- 通过过滤器判断用户的哪些行为是登录操作(即什么时候来进行登录认证)
所有的请求都需要过滤器,所以shrio使用的是过滤器(不要认为也是拦截器,拦截器是SpringMVC的东西)
那么就像我们以前写原生的servlet一样,我们要把filter(那么现在我们需要用到这个shiroFilter已经写好,我们直接用)交给tomcat来管理,此时在web.xml来配置filter(DelegatingFilterProxy),那么现在我们需要用到这个shiroFilter,显然,有了Spring,我们不可能自己创建,而且这个filter也就是单例就好
配置Spring配置文件
-
把这个filter交给Spring管理,通过ShiroFilterFactoryBean来创建这个ShiroFilter。里面可配置(anon,logout,authc)等多个过滤器。
-
配置好了过滤器,再不妨想想在JavaSE中我们是怎么进行登录认证的?
我们先是进行安全管理器的设置,告知当前环境用的是什么管理器,所以我们也需要在spring中配置安全管理器。上面讲到,通过源码分析发现自定义数据源是在创建securityManager实例对象时加载的,所以在spring配置
securityManager的时候,我们要告知安全管理器是用哪个数据源。配置property -
过滤中执行认证操作
过滤功能有了,认证功能也有了此时我们就是需要在过滤的时候进行认证操作,也就是结合这个两个功能,这是只需要将filter和securityManager配置关联即可
<!--注意:名字必须要和web.xml中配置的名字一致-->
<!-- 定义ShiroFilter -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login.html"/>
<property name="filterChainDefinitions">
<value>
/js/**=anon
/bootstrap-3.3.7-dist/**=anon
/jQuery/**=anon
/images/**=anon
/css/**=anon
/style/**=anon
/logout.do=logout
/**=authc
</value>
</property>
<property name="filters">
<map>
<entry key="authc" value-ref="myCRMFormFilter"/>
</map>
</property>
</bean>
<!-- 配置安全管理器SecurityManager 在web环境下使用默认web安全管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myWebRealm"/>
</bean>
登录表单提交的用户名和密码名字必须是username和passord,通过源码发现其底层就是req.getParameter("username")
授权操作
授权操作的整体实现和认证差不多,需要使用到我们自己的业务和数据源,在web环境下开发就需要使用我们自己定义的数据源,同样还是继承AuthorizingRealm,重写其中的doGetAuthorizationInfo方法
在这里我们不需要判断权限,只需要将用户的权限和角色查出,丢进info里(new SimpleAuthorizationInfo())即可
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//创建一个空的AuthorizationInfo
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//拿到当前用户
Employee employee = (Employee)principalCollection.getPrimaryPrincipal();
//判断是否是超级管理员
if(employee.getAdmin()){
//给管理员设置用户信息,并且可以查询所有权限
info.addRole("admin");
info.addStringPermission("*:*");
return info;
}else{
//从数据库中查到所对应的角色和权限
List<String> roles = employeeService.getRolesByEmployeeId(employee.getId());
Set<String> permissions = employeeService.getPermissionsByEmployeeId(employee.getId());
info.addRoles(roles);
info.addStringPermissions(permissions);
return info;
}
}
怎么去判断权限
就像我们之前做RBAC的一样,既然要有权限控制,那么资源所对应的权限是我们规定的。
- 加载权限
加载权限也是将Controller中贴有注解(@RequiresPermissions)的方法扫描出来存进数据库中,同样也是通过注入的方式拿到Spring容器对象
不同点在于:
-
使用Shiro时,使用容器对象的getBeansWithAnnotation()获得的controller是可以包括贴有@Controller注解的类的子类的 (这里的意思是说,Shrio会自动将贴有标签的Controller类动态生成相应的代理类,而@Controller这个注解是没有继承的,但是SpringMVC还是能查找到,并且如果有子类只会找子类而不会找其父类) 然后我们要判断这些字节码对象是否是属于cglib的代理类 (
AopUtils.isCglibProxy(controller)
) -
再用这些判断后的字节码对象获得其父类字节码(
controller.getClass().getSuperclass()
)(@RequiresPermissions标签不继承)找到贴有这些注解的方法,获取其方法体上的注解字节码然后拿到内容,存进数据库中
- 取出权限并存入作用域中
使用Shiro可以很方便的做到这一步,
就像上面说的,只需要通过principalCollection.getPrimaryPrincipal()拿到当前身份信息,也就是认证过的用户,然后把权限从数据库查到,丢到info即可
与原来RBAC的不同在于
RBAC需要是用RequestContextHolder.getRequestAttributes()
方法拿到session,然后再丢进session,这样其实很麻烦。
总的来说
Shiro是先进行认证,认证通过后再
进行授权,什么时候校验权限?访问方法的时候,代理类增强的方法会去检验
一些配置
- 在认证的时候会用到MD5加密,因为存进数据库的密码是通过加密的,而用户登录表单传过来的密码也要相同规则加密才行,这时我们在数据源中只将盐丢进info里面,让shiro后续进行加密即可,那么实行什么方式的加密是我们配置的,在shiro.xml中配置,然后配置给自定义中的
setCredentialsMatcher
方法
<!--配置凭证匹配器,并将其设置给realm(通过注入的方式)-->
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="md5"/>
</bean>
- 在使用shiro验证权限时,因为是shiro自动生成的动态代理类,使用的AoP织入,所以我们要加上AoP的配置(事务时已经加过了),又因为在功能加强中要知道当前的安全管理器,才能获取到数据源等信息,所以我们需要指明数据源
<!--配置权限AoP织入增强功能-->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
缓存管理器
使用shiro自带的缓存管理器,这时权限和角色信息(shrio只负责这两块,所以也只缓存这两块,不需另外指明)缓存到内存中,这样就不会刷新页面访问同样的资源时还要执行数据源中的授权方法,这样就不用再发SQL了
<!-- 缓存管理器开始 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManager" ref="ehCacheManager"/>
</bean>
<bean id="ehCacheManager" class ="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:shiro-ehcache.xml" />
<property name="shared" value="true"/>
</bean>
shiro和freemark的兼容配置(在FreeMark中使用Shiro标签)
要增强FreeMark的功能,而又要按照FreeMark的规范,这时我们可以继承FreeMark的类再重写自己的方法,这里我们需要继承FreeMark的配置类,拓展Shiro的便签类(记得加依赖)
public class MyCRMFreeMarkerConfig extends FreeMarkerConfigurer {
@Override
public void afterPropertiesSet() throws IOException, TemplateException {
super.afterPropertiesSet();
Configuration cfg = this.getConfiguration();
cfg.setSharedVariable("shiro", new ShiroTags());//shiro标签
}
}
此时配置文件中就要引入我们自己的FreeMark的配置
<!--配置freeMarker的模板路径 -->
<bean class="cn.kiring.crm.shiro.MyCRMFreeMarkerConfig">
<!-- 配置freemarker的文件编码 -->
<property name="defaultEncoding" value="UTF-8" />
<!-- 配置freemarker寻找模板的路径(相当于前缀) -->
<property name="templateLoaderPath" value="/WEB-INF/views/" />
</bean>
<!--freemarker视图解析器 -->
<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
<!-- 是否在model自动把session中的attribute导入进去; -->
<property name="exposeSessionAttributes" value="true" />
<!-- 配置逻辑视图自动添加的后缀名 -->
<property name="suffix" value=".ftl" />
<!-- 配置视图的输出HTML的contentType -->
<property name="contentType" value="text/html;charset=UTF-8" />
</bean>
关于登录
- 在过滤器的配置中配置loginURL就为我们自己的登录页面->login.html,而在我们的登录页面上的表单的action也是/login.html,那么为什么是这样?
通过查看认证过滤器中isLoginSubmission()方法就能发现,这个方法内部有进行判断这个请求是什么方式,如果是POST方式,就会进行登录验证操作,否则就是普通的访问这个资源,又因为我们在shiro.xml中的过滤器的配置上配置了loginUrl属性值,所以过滤器不会拦截这个请求。
- 我们的登录页面的表单的name一定是"username" 和 "password"
通过查看认证过滤器FormAuthenticationFilter里的getUsername()和getPassword()可以发现shiro底层就是使用request.getParameter("username")和request.getParameter("password")来获取用户登录数据并装进token的