springboot shiro安全框架使用
很长的前言(老司机忽略):
1.去百度/官方文档了解Subject、SecurityManager、Realm这些shiro的概念
2.分清authentication(认证)、authorization(授权)这两个单词
3.了解cookie和session完成的登陆认证。
(1)shiro的rememberMe能够把用户信息加密储存到cookie,并且在下次登陆时自动取回cookied并还原用户信息给我们
(2)获取用户输入的账户密码后,在shiro中调用login()
方法,用户信息将被自动存入session,而rememberMe相关的不会自动存入session。
4.需要了解:数据库、spring的各层术语、jpa、role(角色)与permission(权限)
5.建议前面说的多少了解些,不然后面太多东西集成,一大串不懂心很烦
下面跟着这个走:github上的一个例子
再推荐一个教程:教你 Shiro 整合 SpringBoot,避开各种坑
我这里是自底向上,根据已有的东西往上建设。
先完成登陆认证:
1.把DAO层、service层、jpa的Entity这些弄好,数据库自己弄好,能连上没问题后进入正题
2.写一个MyShiroRealm类继承AuthorizingRealm类。
有两个方法需要重写:doGetAuthorizationInfo
(权限授权相关,这里跳过)。
AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
(重点是这个),我们需要从这里完成从数据库中获取对应账户和密码的工作,这样后面就不用再管数据库了。
(1)参数token可以看作UsernamePasswordToken
(AuthenticationToken 的子类),一般我们后面都会传入这个。token.getPrincipal()
方法将返回我们“账户密码”中的“账户”,在例子中这个是一个代表用户名的字符串。根据这个去查询数据库,把该用户的信息取出来用。
(2)返回一个SimpleAuthenticationInfo
的对象。用该对象封装必要的登陆信息(这样就屏蔽了我们userinfo对象的差异)。四参数构造函数中,第一个参数代表用户,随便设置,后面SecurityUtils.getSubject().getPrincipal()
就返回这个,自己看需要;二、三参数分别代表加密后的密码和加密用的盐;第四个参数是如果后面我们需要指定哪个Realm来获取认证信息时才用到的。
3.Configuration中先写好3个必要的@Bean
myShiroRealm、securityManager、hashedCredentialsMatcher这三个。
(1)hashedCredentialsMatcher就是密码匹配器。它用于把密码加密后与密文对应,这个东西需要交给Realm使用。代码中myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
这里把密码匹配器交给Realm
(2)securityManager。了解过相关概念的话,登陆认证调用其实是SecurityUtils--->securityManager--->Realm的doGetAuthenticationInfo,因此在securityManager需要把我们的Realm交给它,代码中securityManager.setRealm(myShiroRealm());
这里就是把Realm交给securityManager。
至于securityManager自己,如果没有写Filter,则这里应当有一句SecurityUtils.setSecurityManager(securityManager);
来把其交给SecurityUtils。但例子中没有这行代码,是由于Filter中已经设置了,当Filter被注入后,SecurityUtils就自动获得了该securityManager
4.Configuration中其他的Bean先放下(注意第三点说的,如果没有写Filter,自行找个地方把securityManager交给SecurityUtils),上面的完成后,我们即可在web层Controller中操作了,先注入两个参数username和password,后面就可以
UsernamePasswordToken token=new UsernamePasswordToken(username,password);
Subject subject= SecurityUtils.getSubject();
subject.login(token);
这样就完成最基本登陆认证了。有了这个基础去找其他文章,看他们的例子补充rememberMe、session、验证码实现的相关知识。
这里贴一份实现rememberMe功能的:(rememberMe记住我功能)
授权相关
1.回头写MyShiroRealm类的doGetAuthorizationInfo
(1)其参数PrincipalCollection principals
代表一个身份集合。这个集合主要是由于可能存在多个Realm,比如你有两张表,一张常规用户表一张用户黑名单表,一个用户可能在两个表里都有,这样你就会写两个继承Realm的类,它们会放进两个不同的user(即上面2.2点中说到的第一个参数)。它的方法getPrimaryPrincipal()
就是返回主要的user对象(自行强制转换),如果只有一个Realm知道这些就足够了。如果有多个Realm,则用fromRealm(String realmName)
即可,其参数realmName对应上面2.2点中说的第四个参数。详情右转api文档
(2)返回一个SimpleAuthorizationInfo对象。你需要把这个用户拥有的角色和权限以字符串的形式封装进这个对象:addRole(String role)
和addStringPermission(String permission)
使用这两个方法(有重载方法参数直接为一个集合)。
(3)视乎你的代码设计,这里的代码差异较大。例子中的user对象可以直接获取role/permission对应的字符串,如果你user对象不是这个储存结构,可以在这里想办法再去查询一遍数据库来获取角色/权限数据,再封装进SimpleAuthorizationInfo对象。
2.关于这个Permission,它其实是shiro的一个接口,只不过为了使用方便,99.9%情况下都会以字符串的形式来表示一个Permission对象,shiro有能力把我们的字符串换成Permission对象,我们只管操作字符串表示Permission就行
关于Permission字符串的格式、权限和角色的使用:Shiro入门9
3.subject.hasRole("xxx")
和subject.checkPermission("xxx")
这些方法就是根据你doGetAuthorizationInfo返回的AuthorizationInfo对象来判断的
4..如果想在web层的Controller中使用shiro提供的角色/权限控制注解,参考例子中Configuration的authorizationAttributeSourceAdvisor这个Bean,就几行。写完这个Bean就可以开启shiro的注解了,当然注解的role/permission的判断同第3点
5.如果只使用上面3、4点的方法来进行授权管理,那么当规模很大时,你维护起来就要到处找,不直观不方便。因此这里还有最后的一个shirFilter这个Bean(spring自动注入到shiro,写了这个bean就行),你可以在这里进行集中管理。
(1)看代码看注释,不难理解大部分的东西
(2)Map<String,String> filterChainDefinitionMap
,用来配置拦截器链,其中key为url,value为对应的拦截器类(怎么对应看下面给的链接),不同的拦截器类有不同的拦截效果
举个非常简单的例子:
在这里写filterChainDefinitionMap.put("/hello", "perms[userInfo:del]");
和你在对应Controller上写@RequiresPermissions("userInfo:del")
注解是一样的效果;filterChainDefinitionMap.put("/hello","roles[admin]"
和@RequiresRoles("admin")
是一样的效果,其他看代码注释不难理解。
更多详细右转:Shiro几大拦截器
总结和坑
1.Subject的isRemembered()方法实现为:
public boolean isRemembered() {
PrincipalCollection principals = this.getPrincipals();
return principals != null && !principals.isEmpty() && !this.isAuthenticated();
}
看这里“!this.isAuthenticated()”,也就是说,如果当前用户这次浏览经过了账户密码认证,那么isRemembered一定返回false。也就是说“已认证”会覆盖“记住我”
2.chrome等浏览器中直接删除rememberMe这个cookie,F5刷新后cookie仍然生效。
shiro缓存有毒,即使此时浏览器不再发送cookie,此时isRemember仍然为true,摸不清这个缓存机制。此时重启浏览器/重启服务端即可。
3.shiroFilterFactoryBean.setSuccessUrl("/index");
无效的问题
(1)shiro过滤链跟servlet的过滤链可看作一样,shiro把自己的过滤链放在servlet的所有Filter之前而已。
(2)shiroFilterFactoryBean.setLoginUrl("/login");
作用是无权访问"authc"的页面时强制跳转到这里
(3)shiroFilterFactoryBean.setSuccessUrl("/index");
当且仅当:login页面的form提交方法为post且拦截包括"/login"时,一旦登陆成功就会跳转。
这个东西比较鸡肋,第一次看例子时一直看不懂Controller中"/login"的登陆逻辑,就是这个B。实际流程是shiro拦截访问并重定向到"login",在处理方法返回值前,shiro会从过滤链里获取request,再得到username、password、rememberMe这几个提交的值,由shiro帮你组织并调用subject.login来登陆,再进入处理方法,因此例子中login的处理方法主要就是处理有无错误/异常,最后由Filter更改其跳转,也就是此时会返回setSuccessUrl设置的url,而非控制器中处理方法的值。
而一旦我们设置了访问"/login"页面可以匿名访问时,那么登陆逻辑全部由我们自己来做,shiro不会做任何成功跳转之类的行为,也就是该情况下setSuccessUrl无效。
总结来说:"/login"受到shiro拦截时,可以偷懒不写登陆逻辑,控制器中对应的处理方法没什么用,并且成功登陆后跳转到setSuccessUrl设置的值。"/login"不受拦截可匿名访问时,需要在处理方法内自己完成登陆逻辑,此时setSuccessUrl不生效,符合常规控制器处理方法的mvc模式。