30 分钟学会如何使用 Shiro
云栖号资讯:【点击查看更多行业资讯】
在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来!
一、架构
要学习如何使用Shiro必须先从它的架构谈起,作为一款安全框架Shiro的设计相当精妙。Shiro的应用不依赖任何容器,它也可以在JavaSE下使用。但是最常用的环境还是JavaEE。下面以用户登录为例:
1、使用用户的登录信息创建令牌
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token可以理解为用户令牌,登录的过程被抽象为Shiro验证令牌是否具有合法身份以及相关权限。
2、执行登陆动作
SecurityUtils.setSecurityManager(securityManager); // 注入SecurityManager Subject subject = SecurityUtils.getSubject(); // 获取Subject单例对象 subject.login(token); // 登陆
Shiro的核心部分是SecurityManager,它负责安全认证与授权。Shiro本身已经实现了所有的细节,用户可以完全把它当做一个黑盒来使用。SecurityUtils对象,本质上就是一个工厂类似Spring中的ApplicationContext。
Subject是初学者比较难于理解的对象,很多人以为它可以等同于User,其实不然。Subject中文翻译:项目,而正确的理解也恰恰如此。它是你目前所设计的需要通过Shiro保护的项目的一个抽象概念。通过令牌(token)与项目(subject)的登陆(login)关系,Shiro保证了项目整体的安全。
我整理了架构师系列视频教程 ,关注微信公众号「互联网架构师」回复 2T 下载。
3、判断用户
Shiro本身无法知道所持有令牌的用户是否合法,因为除了项目的设计人员恐怕谁都无法得知。因此Realm是整个框架中为数不多的必须由设计者自行实现的模块,当然Shiro提供了多种实现的途径,本文只介绍最常见也最重要的一种实现方式——数据库查询。
4、两条重要的英文
我在学习Shiro的过程中遇到的第一个障碍就是这两个对象的英文名称:AuthorizationInfo,AuthenticationInfo。不用怀疑自己的眼睛,它们确实长的很像,不但长的像,就连意思都十分近似。
在解释它们前首先必须要描述一下Shiro对于安全用户的界定:和大多数操作系统一样。用户具有角色和权限两种最基本的属性。例如,我的Windows登陆名称是learnhow,它的角色是administrator,而administrator具有所有系统权限。这样learnhow自然就拥有了所有系统权限。那么其他人需要登录我的电脑怎么办,我可以开放一个guest角色,任何无法提供正确用户名与密码的未知用户都可以通过guest来登录,而系统对于guest角色开放的权限极其有限。
同理,Shiro对用户的约束也采用了这样的方式。AuthenticationInfo代表了用户的角色信息集合,AuthorizationInfo代表了角色的权限信息集合。如此一来,当设计人员对项目中的某一个url路径设置了只允许某个角色或具有某种权限才可以访问的控制约束的时候,Shiro就可以通过以上两个对象来判断。说到这里,大家可能还比较困惑。先不要着急,继续往后看就自然会明白了。
二、实现Realm
如何实现Realm是本文的重头戏,也是比较费事的部分。这里大家会接触到几个新鲜的概念:缓存机制、散列算法、加密算法。由于本文不会专门介绍这些概念,所以这里仅仅抛砖引玉的谈几点,能帮助大家更好的理解Shiro即可。
1、缓存机制
Ehcache是很多Java项目中使用的缓存框架,Hibernate就是其中之一。它的本质就是将原本只能存储在内存中的数据通过算法保存到硬盘上,再根据需求依次取出。你可以把Ehcache理解为一个Map对象,通过put保存对象,再通过get取回对象。
<?xml version="1.0" encoding="UTF-8"?> <ehcache name="shirocache"> <diskStore path="java.io.tmpdir" /> <cache name="passwordRetryCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="1800" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> </ehcache>
以上是ehcache.xml文件的基础配置,timeToLiveSeconds为缓存的最大生存时间,timeToIdleSeconds为缓存的最大空闲时间,当eternal为false时ttl和tti才可以生效。更多配置的含义大家可以去网上查询。
2、散列算法与加密算法
md5是本文会使用的散列算法,加密算法本文不会涉及。散列和加密本质上都是将一个Object变成一串无意义的字符串,不同点是经过散列的对象无法复原,是一个单向的过程。例如,对密码的加密通常就是使用散列算法,因此用户如果忘记密码只能通过修改而无法获取原始密码。但是对于信息的加密则是正规的加密算法,经过加密的信息是可以通过秘钥解密和还原。
3、用户注册
请注意,虽然我们一直在谈论用户登录的安全性问题,但是说到用户登录首先就是用户注册。如何保证用户注册的信息不丢失,不泄密也是项目设计的重点。
public class PasswordHelper { private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator(); private String algorithmName = "md5"; private final int hashIterations = 2; public void encryptPassword(User user) { // User对象包含最基本的字段Username和Password user.setSalt(randomNumberGenerator.nextBytes().toHex()); // 将用户的注册密码经过散列算法替换成一个不可逆的新密码保存进数据,散列过程使用了盐 String newPassword = new SimpleHash(algorithmName, user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), hashIterations).toHex(); user.setPassword(newPassword); } }
如果你不清楚什么叫加盐可以忽略散列的过程,只要明白存储在数据库中的密码是根据户注册时填写的密码所产生的一个新字符串就可以了。经过散列后的密码替换用户注册时的密码,然后将User保存进数据库。剩下的工作就丢给UserService来处理。
那么这样就带来了一个新问题,既然散列算法是无法复原的,当用户登录的时候使用当初注册时的密码,我们又应该如何判断?答案就是需要对用户密码再次以相同的算法散列运算一次,再同数据库中保存的字符串比较。
4、匹配
CredentialsMatcher是一个接口,功能就是用来匹配用户登录使用的令牌和数据库中保存的用户信息是否匹配。当然它的功能不仅如此。本文要介绍的是这个接口的一个实现类:HashedCredentialsMatcher
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher { // 声明一个缓存接口,这个接口是Shiro缓存管理的一部分,它的具体实现可以通过外部容器注入 private Cache<String, AtomicInteger> passwordRetryCache; public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) { passwordRetryCache = cacheManager.getCache("passwordRetryCache"); } @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String username = (String) token.getPrincipal(); AtomicInteger retryCount = passwordRetryCache.get(username); if (retryCount == null) { retryCount = new AtomicInteger(0); passwordRetryCache.put(username, retryCount); } // 自定义一个验证过程:当用户连续输入密码错误5次以上禁止用户登录一段时间 if (retryCount.incrementAndGet() > 5) { throw new ExcessiveAttemptsException(); } boolean match = super.doCredentialsMatch(token, info); if (match) { passwordRetryCache.remove(username); } return match; } }
可以看到,这个实现里设计人员仅仅是增加了一个不允许连续错误登录的判断。真正匹配的过程还是交给它的直接父类去完成。连续登录错误的判断依靠Ehcache缓存来实现。显然match返回true为匹配成功。
5、获取用户的角色和权限信息
说了这么多才到我们的重点Realm,如果你已经理解了Shiro对于用户匹配和注册加密的全过程,真正理解Realm的实现反而比较简单。我们还得回到上文提及的两个非常类似的对象AuthorizationInfo和AuthenticationInfo。因为Realm就是提供这两个对象的地方。
public class UserRealm extends AuthorizingRealm { // 用户对应的角色信息与权限信息都保存在数据库中,通过UserService获取数据 private UserService userService = new UserServiceImpl(); /** * 提供用户信息返回权限信息 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 根据用户名查询当前用户拥有的角色 Set<Role> roles = userService.findRoles(username); Set<String> roleNames = new HashSet<String>(); for (Role role : roles) { roleNames.add(role.getRole()); } // 将角色名称提供给info authorizationInfo.setRoles(roleNames); // 根据用户名查询当前用户权限 Set<Permission> permissions = userService.findPermissions(username); Set<String> permissionNames = new HashSet<String>(); for (Permission permission : permissions) { permissionNames.add(permission.getPermission()); } // 将权限名称提供给info authorizationInfo.setStringPermissions(permissionNames); return authorizationInfo; } /** * 提供账户信息返回认证信息 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); User user = userService.findByUsername(username); if (user == null) { // 用户名不存在抛出异常 throw new UnknownAccountException(); } if (user.getLocked() == 0) { // 用户被管理员锁定抛出异常 throw new LockedAccountException(); } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), getName()); return authenticationInfo; } }
根据Shiro的设计思路,用户与角色之前的关系为多对多,角色与权限之间的关系也是多对多。在数据库中需要因此建立5张表,分别是:
用户表(存储用户名,密码,盐等)
角色表(角色名称,相关描述等)
权限表(权限名称,相关描述等)
用户-角色对应中间表(以用户ID和角色ID作为联合主键)
角色-权限对应中间表(以角色ID和权限ID作为联合主键)
具体dao与service的实现本文不提供。总之结论就是,Shiro需要根据用户名和密码首先判断登录的用户是否合法,然后再对合法用户授权。而这个过程就是Realm的实现过程。
6、会话
用户的一次登录即为一次会话,Shiro也可以代替Tomcat等容器管理会话。目的是当用户停留在某个页面长时间无动作的时候,再次对任何链接的访问都会被重定向到登录页面要求重新输入用户名和密码而不需要程序员在Servlet中不停的判断Session中是否包含User对象。
启用Shiro会话管理的另一个用途是可以针对不同的模块采取不同的会话处理。以淘宝为例,用户注册淘宝以后可以选择记住用户名和密码。之后再次访问就无需登陆。但是如果你要访问支付宝或购物车等链接依然需要用户确认身份。当然,Shiro也可以创建使用容器提供的Session最为实现。
三、与SpringMVC集成
有了注册模块和Realm模块的支持,下面就是如何与SpringMVC集成开发。有过框架集成经验的同学一定知道,所谓的集成基本都是一堆xml文件的配置,Shiro也不例外。
1、配置前端过滤器
先说一个题外话,Filter是过滤器,interceptor是拦截器。前者基于回调函数实现,必须依靠容器支持。因为需要容器装配好整条FilterChain并逐个调用。后者基于代理实现,属于AOP的范畴。
如果希望在WEB环境中使用Shiro必须首先在web.xml文件中配置
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <display-name>Shiro_Project</display-name> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <servlet> <servlet-name>SpringMVC</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> <async-supported>true</async-supported> </servlet> <servlet-mapping> <servlet-name>SpringMVC</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <!-- 将Shiro的配置文件交给Spring监听器初始化 --> <param-value>classpath:spring.xml,classpath:spring-shiro-web.xml</param-value> </context-param> <context-param> <param-name>log4jConfigLoaction</param-name> <param-value>classpath:log4j.properties</param-value> </context-param> <!-- shiro配置 开始 --> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <async-supported>true</async-supported> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- shiro配置 结束 --> </web-app>
熟悉Spring配置的同学可以重点看有绿字注释的部分,这里是使Shiro生效的关键。由于项目通过Spring管理,因此所有的配置原则上都是交给Spring。DelegatingFilterProxy的功能是通知Spring将所有的Filter交给ShiroFilter管理。
接着在classpath路径下配置spring-shiro-web.xml文件
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 缓存管理器 使用Ehcache实现 --> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:ehcache.xml" /> </bean> <!-- 凭证匹配器 --> <bean id="credentialsMatcher" class="utils.RetryLimitHashedCredentialsMatcher"> <constructor-arg ref="cacheManager" /> <property name="hashAlgorithmName" value="md5" /> <property name="hashIterations" value="2" /> <property name="storedCredentialsHexEncoded" value="true" /> </bean> <!-- Realm实现 --> <bean id="userRealm" class="utils.UserRealm"> <property name="credentialsMatcher" ref="credentialsMatcher" /> </bean> <!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="userRealm" /> </bean> <!-- Shiro的Web过滤器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/" /> <property name="unauthorizedUrl" value="/" /> <property name="filterChainDefinitions"> <value> /authc/admin = roles[admin] /authc/** = authc /** = anon </value> </property> </bean> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> </beans>
需要注意filterChainDefinitions过滤器中对于路径的配置是有顺序的,当找到匹
器不会再继续寻找。因此带有通配符的路径要放在后面。三条配置的含义是:
/authc/admin需要用户有用admin权限
/authc/**用户必须登录才能访问
/**其他所有路径任何人都可以访问
说了这么多,大家一定关心在Spring中引入Shiro之后到底如何编写登录代码呢。
@Controller public class LoginController { @Autowired private UserService userService; @RequestMapping("login") public ModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password) { UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject subject = SecurityUtils.getSubject(); try { subject.login(token); } catch (IncorrectCredentialsException ice) { // 捕获密码错误异常 ModelAndView mv = new ModelAndView("error"); mv.addObject("message", "password error!"); return mv; } catch (UnknownAccountException uae) { // 捕获未知用户名异常 ModelAndView mv = new ModelAndView("error"); mv.addObject("message", "username error!"); return mv; } catch (ExcessiveAttemptsException eae) { // 捕获错误登录过多的异常 ModelAndView mv = new ModelAndView("error"); mv.addObject("message", "times error"); return mv; } User user = userService.findByUsername(username); subject.getSession().setAttribute("user", user); return new ModelAndView("success"); } }
登录完成以后,当前用户信息被保存进Session。这个Session是通过Shiro管理的会话对象,要
获取依然必须通过Shiro。传统的Session中不存在User对象。
@Controller @RequestMapping("authc") public class AuthcController { // /authc/** = authc 任何通过表单登录的用户都可以访问 @RequestMapping("anyuser") public ModelAndView anyuser() { Subject subject = SecurityUtils.getSubject(); User user = (User) subject.getSession().getAttribute("user"); System.out.println(user); return new ModelAndView("inner"); } // /authc/admin = user[admin] 只有具备admin角色的用户才可以访问,否则请求将被重定向至登录界面 @RequestMapping("admin") public ModelAndView admin() { Subject subject = SecurityUtils.getSubject(); User user = (User) subject.getSession().getAttribute("user"); System.out.println(user); return new ModelAndView("inner"); } }
本篇内容大多总结自张开涛的《跟我学Shiro》原文地址:
http://jinnianshilongnian.iteye.com/blog/2018936
【云栖号在线课堂】每天都有产品技术专家分享!
课程地址:https://yqh.aliyun.com/zhibo立即加入社群,与专家面对面,及时了解课程最新动态!
【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvnK
原文发布时间:2020-05-29
本文作者: 冷豪
本文来自:“互联网架构师 微信公众号”,了解相关信息可以关注“互联网架构师”
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
完美日记:实现高弹性高稳定电商架构
公司简介 完美日记(Perfect Diary)是广州市“独角兽”创新企业——广州逸仙电子商务有限公司旗下首个美妆品牌,创立于2017年,用心为新生代女性开发高品质、精设计、易上手的彩妆及护肤产品,立志于打造有国际影响力的Chinese Beauty Icon。 完美日记上线不到两年即成为天猫彩妆销冠,2019年成为11年来第一个登上天猫双十一彩妆榜首的国货品牌,包揽天猫2019全年彩妆销冠;2020年4月成为首个亮相天猫超级品牌日的国货彩妆品牌,同时勇破彩妆品牌销售纪录。 另外,完美日记已在全国各地开设了数十家线下店,计划至2022年底开店超600家。 截至2020年4月,品牌SKU超过700个,全网用户粉丝数量超过2500万,月曝光量10亿+。 业务痛点 系统开发迭代快,线上问题比较多,定位问题比较耗时。频繁大促,系统稳定性保障压力很大,第三方接口和一些慢SQL就可能导致严重的线上故障。压测与系统容量评估的工作非常频繁,需要做常态化的机制来支撑。系统大促时资源与日常资源相差较大,需要频繁扩缩容。 解决方案 图 1. 解决方案架构图 方案细节: 为了支撑业务快速发展,完美日记采用了阿...
- 下一篇
互联制造,数据分析如何重塑制造业
云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 就制造业而言,无论是“工业4.0”的技术浪潮,还是工业互联网作为“新基建”重点方向之一,制造业转向数据驱动的大潮早已汹涌澎湃。工业物联网将实体资产与系统及人员衔接起来,让数据在工厂中自由流动,帮助企业提升生产效率,这也就是现代化生产中所追寻的“互联制造”。 根据IDC预测,到2021年全球将有七成以上的制造企业使用数据来增强自动化和运营。正如数据已成为各个行业的驱动力一样,制造业的下一次进化也将通过数据和分析提高运营效率。制造企业将利用新的数据源,并以创新的方式运用这些信息,来实现更高效的产品开发、供应链和生产力。“数据驱动型组织获得客户的可能性会提高23倍,保留客户的可能性会提高6倍,盈利的可能性则会提高19倍”,麦肯锡近日发布的这一报告让制造业转向数据驱动显得更加迫在眉睫。 互联制造的兴起 目前,制造业已开始采用实时数据流来改善运营模式。实时捕获的数据可对设备维护、需求波动以及供应链中断做出响应,从而提高生产效率并降低维护成本。为了充分把握实时数据流带来的商业机会,制造企业必须重新考...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS8编译安装MySQL8.0.19
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路