# LearnShiro **Repository Path**: pinefire/LearnShiro ## Basic Information - **Project Name**: LearnShiro - **Description**: Shiro学习笔记 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2023-03-23 - **Last Updated**: 2023-03-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [Shiro教学视频](https://www.bilibili.com/video/BV1UW41197qG) [TOC] ## 1. 简介 - Apache Shiro 是 Java 的一个安全(权限)框架。 - Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在 JavaEE 环境。 - Shiro 可以完成:认证、授权、加密、会话管理、与Web集成、缓存等。 - [Shiro官网]([http://shiro.apache.org/) ### 功能简介 ### ![](image/01.png) - **Authentication**:身份认证/登录,验证用户是不是拥有相应的身份; - **Authorization**:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限; - **Session Manager**:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中; 会话可以是普通JavaSE环境,也可以是 Web 环境的; - **Cryptography**:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储; - **Web Support**:Web 支持,可以非常容易的集成到Web环境; - **Caching**:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率; - **Concurrency**: Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能 把权限自动传播过去; - **Testing**:提供测试支持; - **Run As**: 允许一个用户假装为另一个用户(如果他们允许)的身份进行访问; - **Remember Me**: 记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了 > authentication /ɔːˌθentɪˈkeɪʃn/n. 身份验证;认证;鉴定 > > authorization /ˌɔːθərəˈzeɪʃn/n. 批准;授权;批准书;授权书 ### Shiro 架构 ### #### Shiro 架构(Shiro外部来看) #### 从外部来看Shiro,即从应用程序角度的来观察如何使用 Shiro 完成工作 ![](image/02.png) - **Subject**:应用代码直接交互的对象是Subject,也就是说 Shiro 的对外API核心就是Subject。Subject代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等; 与 Subject 的所有交互都会委托给SecurityManager;Subject 其实是一个门面, SecurityManager 才是实际的执行者; - **SecurityManager**:安全管理器;即所有与安全有关的操作都会与SecurityManager 交互;且其管理着所有 Subject;可以看出它是 Shiro的核心,它负责与 Shiro 的其他组件进行交互,它相当于 SpringMVC 中DispatcherServlet 的角色 - **Realm**:Shiro从Realm(领域、场所) 获取安全数据(如用户、角色、权限),就是说SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource #### Shiro 架构(Shiro内部来看) #### ![](image/03.png) ## 2. HelloWorld 1. 新键Maven工程。 2. [pom.xml](pom.xml)加入依赖: 1. org.apache.shiro : shiro-all : 1.3.2 2. org.slf4j : slf4j-log4j12 : 1.8.0-alpha2 3. 添加配置文件: 1. [shiro.ini](src/main/resources/shiro.ini) Shiro的配置文件 2. [log4j.properties](src/main/resources/log4j.properties) 日志配置 4. 添加Shiro的HelloWorld类——[Quickstart.java](src/main/java/com/lun/shiro/one/Quickstart.java) ### shiro.ini配置文件解析 [shiro.ini](src/main/resources/shiro.ini)配置文件主要内容为: 1. 用户=密码, 角色 2. 角色=角色对应的权限 ``` # ----------------------------------------------------------------------------- # 用户=密码, 角色 # Users and their assigned roles ----------------------------------------------------------------------------- [users] # 1. user 'root' with password 'secret' and the 'admin' role root = secret, admin # 2. user 'guest' with the password 'guest' and the 'guest' role guest = guest, guest # 3. user 'presidentskroob' with password '12345' ("That's the same combination on # my luggage!!!" ;)), and role 'president' presidentskroob = 12345, president # 4. user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz' darkhelmet = ludicrousspeed, darklord, schwartz # 5. user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz' lonestarr = vespa, goodguy, schwartz # ----------------------------------------------------------------------------- # Roles with assigned permissions # 角色以及它的权限 ----------------------------------------------------------------------------- [roles] # 1. 'admin' role has all permissions, indicated by the wildcard '*' admin = * # 2. The 'schwartz' role can do anything (*) with any lightsaber: schwartz = lightsaber:* # 3. The 'goodguy' role is allowed to 'delete' (action) the user (type) with # license plate 'zhangsan' (instance specific id) goodguy = user:delete:zhangsan ``` ### Quickstart.java解析 ### 从[Quickstart.java](src/main/java/com/lun/shiro/one/Quickstart.java)中,大概可看出如何使用Shiro进行认证、授权等安全操作。 ```java public class Quickstart { public static void main(String[] args) { //创建并安全管理器,需要用到上述的shiro.ini配置文件 Factory factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); //获取当前的Subject,这里Subject可理解为即将登陆的用户 Subject currentUser = SecurityUtils.getSubject(); // 测试使用Session读写键值对,这个Session无需Web容器就可使用 Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("---> Retrieved the correct value! [" + value + "]"); } // 测试当前的用户是否已经被认证,即是否已经登录 if (!currentUser.isAuthenticated()) { // 把用户名和密码封装为 UsernamePasswordToken 对象 UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); // rememberme token.setRememberMe(true); try { //执行登录. currentUser.login(token); } // 若没有指定的账户,则抛出 UnknownAccountException catch (UnknownAccountException uae) { log.info("----> There is no user with username of " + token.getPrincipal()); return; } // 若账户存在,但密码不匹配,则抛出 IncorrectCredentialsException 异常 catch (IncorrectCredentialsException ice) { log.info("----> Password for account " + token.getPrincipal() + " was incorrect!"); return; } // 用户被锁定的异常 LockedAccountException catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } // 所有认证时异常的父类AuthenticationException,也就是说LockedAccountException、IncorrectCredentialsException等继承该父类 catch (AuthenticationException ae) { //unexpected condition? error? } } //print their identifying principal (in this case, a username): log.info("----> User [" + currentUser.getPrincipal() + "] logged in successfully."); // 测试是否有某一个角色 if (currentUser.hasRole("schwartz")) { log.info("----> May the Schwartz be with you!"); } else { log.info("----> Hello, mere mortal."); return; } // 测试用户是否具备某一个行为 if (currentUser.isPermitted("lightsaber:weild")) { log.info("----> You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } //测试用户是否具备某一个更具体的行为: if (currentUser.isPermitted("user:delete:zhangsan")) { log.info("----> You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " + "Here are the keys - have fun!"); } else { log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!"); } // 执行登出 log.info("---->currentUser.isAuthenticated(): " + currentUser.isAuthenticated()); currentUser.logout(); log.info("---->currentUser.isAuthenticated(): " + currentUser.isAuthenticated()); System.exit(0); } } ``` > principal [ˈprɪnsəpl] > adj. 最重要的;主要的 > n. 大学校长;学院院长;本金;资本;主要演员;主角 #### 运行结果 ``` 2020-07-11 18:43:05,687 INFO [org.apache.shiro.session.mgt.AbstractValidatingSessionManager] - Enabling session validation scheduler... 2020-07-11 18:43:06,051 INFO [com.lun.shiro.one.Quickstart] - Retrieved the correct value! [aValue] 2020-07-11 18:43:06,054 INFO [com.lun.shiro.one.Quickstart] - User [lonestarr] logged in successfully. 2020-07-11 18:43:06,055 INFO [com.lun.shiro.one.Quickstart] - May the Schwartz be with you! 2020-07-11 18:43:06,056 INFO [com.lun.shiro.one.Quickstart] - You may use a lightsaber ring. Use it wisely. 2020-07-11 18:43:06,056 INFO [com.lun.shiro.one.Quickstart] - You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. Here are the keys - have fun! 2020-07-11 18:43:06,056 INFO [com.lun.shiro.one.Quickstart] - currentUser.isAuthenticated(): true 2020-07-11 18:43:06,059 INFO [com.lun.shiro.one.Quickstart] - currentUser.isAuthenticated(): false ``` ## 3. 集成Spring 1. 在已有的Maven工程的[pom.xml](pom.xml)加入Spring框架全家桶依赖 2. 添加配置文件 1. [web.xml](src/main/webapp/WEB-INF/web.xml) 2. [applicationContext.xml](src/main/resources/applicationContext.xml) 3. [spring-servlet.xml](src/main/webapp/WEB-INF/spring-servlet.xml) ### 配置web.xml ### 配置[web.xml](src/main/webapp/WEB-INF/web.xml)主要目的: 1. 引入Spring - `org.springframework.web.context.ContextLoaderListener` 2. 引入SpringMVC - `org.springframework.web.servlet.DispatcherServlet` 3. 引入Shiro ```xml org.springframework.web.context.ContextLoaderListener contextConfigLocation classpath:applicationContext.xml spring org.springframework.web.servlet.DispatcherServlet 1 spring / shiroFilter org.springframework.web.filter.DelegatingFilterProxy targetFilterLifecycle true shiroFilter /* ``` - Shiro 提供了与 Web 集成的支持,其通过一个ShiroFilter 入口来拦截需要安全控制的URL,然后进行相应的控制 - ShiroFilter 类似于如 Strut2/SpringMVC 这种web 框架的前端控制器,是安全控制的入口点,其负责读取配置(如ini 配置文件),然后判断URL是否需要登录/权限等工作。 ### 配置spring-servlet.xml ### 在[spring-servlet.xml](src/main/webapp/WEB-INF/spring-servlet.xml)配置SpringMVC信息 ```xml ``` ### 配置applicationContext.xml Shiro的配置信心主要在[applicationContext.xml](applicationContext.xml)中。 1. 配置 SecurityManager 2. 配置 CacheManager 3. 配置 Realm 4. 配置 LifecycleBeanPostProcessor. 可以自定的来调用配置在 Spring IOC 容器中 shiro bean 的生命周期方法 5. 启用 IOC 容器中使用 shiro 的注解. 但必须在配置了 LifecycleBeanPostProcessor 之后才可以使用 6. 配置 ShiroFilter ```xml /login.jsp = anon /shiro/login = anon /shiro/logout = logout /user.jsp = roles[user] /admin.jsp = roles[admin] # everything else requires authentication: /** = authc ``` ## 4. Shiro工作流程 ![](image/04.png) ## 5. DelegatingFilterProxy 在[web.xml](src/main/webapp/WEB-INF/web.xml)中 ```xml shiroFilter org.springframework.web.filter.DelegatingFilterProxy targetFilterLifecycle true ``` --- 在[applicationContext.xml](applicationContext.xml)中 ```xml ... ... ... ``` ## 6. 权限URL配置细节 [urls]部分的配置格式是:“url=拦截器[参数],拦截器[参数]”。 如果当前请求的 url 匹配 [urls] 部分的某个 url 模式,将会执行其配置的拦截器。 拦截器例子如下: - anon(anonymous)拦截器表示匿名访问(即不需要登录即可访问) - authc(authentication)拦截器表示需要身份认证通过后才能访问 ```xml /login.jsp = anon /shiro/login = anon /shiro/logout = logout /user.jsp = roles[user] /admin.jsp = roles[admin] # everything else requires authentication: /** = authc ``` ### URL 匹配模式 ### url 模式使用 Ant 风格模式 Ant 路径通配符支持 ?、 *、 **,注意通配符匹配不包括目录分隔符“/” - ?:匹配一个字符, 如 /admin? 将匹配 /admin1,但不匹配 /admin 或 /admin/; - *:匹配零个或多个字符串, 如 /admin 将匹配 /admin、/admin123,但不匹配 /admin/1; - \*\*:匹配路径中的零个或多个路径, 如 /admin/** 将匹配 /admin/a 或 /admin/a/b ### Shiro中默认的过滤器 [Default Filters](http://shiro.apache.org/web.html#default-filters) ### URL 匹配顺序 URL 权限采取第一次匹配优先的方式, 即从头开始使用第一个匹配的 url 模式对应的拦截器链。如: - /bb/**=filter1 - /bb/aa=filter2 - /**=filter3 **如果请求的url是“/bb/aa”,因为按照声明顺序进行匹配,那么将使用 filter1 进行拦截**。(先声夺人) ## 7. 认证思路分析 认证说白就是登陆 1. 获取当前的Subject,调用SecurityUtils.getSubject() 2. 测试当前的用户是否已经被认证,即是否已经登录。调用 Subject.isAuthenticated() 3. 若没有被认证,则把用户名和密码封装为 UsernamePasswordToken 对象 1. 创建一个表单页面 2. 把请求提交到SpringMVC的Handler 3. 获取用户名和密码. 4. 执行登录:调用Subject的login(AuthenticationToken)方法 5. 自定义Realm的方法,从数据库中获取对应的记录,返回给Shiro. 1. 实际上需要继承org.apache.shiro.realm.AuthenticatingRealm类 2. 实现doGetAuthenticationInfo(AuthenticationToken)方法. 6. 由Shiro完成对密码的比对。 ## 8. 实现认证流程 1. 创建表单页面[login.jsp](src/main/webapp/login.jsp) 2. 创建表单页面Controller - [ShiroHandler](src/main/java/com/lun/shiro/two/handlers/ShiroHandler.java) 3. 创建Realm - [ShiroRealm](src/main/java/com/lun/shiro/two/realms/ShiroRealm.java),从数据库中获取对应的记录,返回给 Shiro. 1. 实际上需要继承 org.apache.shiro.realm.AuthenticatingRealm 类 2. 实现 doGetAuthenticationInfo(AuthenticationToken) 方法,添加账号,密码验证等相关代码。 ## 9. 实现认证Realm 1. 把 AuthenticationToken 转换为 UsernamePasswordToken 2. 从 UsernamePasswordToken 中来获取 username 3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录 4. 若用户不存在, 则可以抛出 UnknownAccountException 异常 5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常. 6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回。通常使用的实现类为: SimpleAuthenticationInfo,以下信息是从数据库中获取的 1. principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象。 2. credentials: 密码 3. realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可 [ShiroRealm](src/main/java/com/lun/shiro/two/realms/ShiroRealm.java) ```java public class ShiroRealm extends AuthorizingRealm { @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { System.out.println("[FirstRealm] doGetAuthenticationInfo"); //1. 把 AuthenticationToken 转换为 UsernamePasswordToken UsernamePasswordToken upToken = (UsernamePasswordToken) token; //2. 从 UsernamePasswordToken 中来获取 username String username = upToken.getUsername(); //3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录 System.out.println("从数据库中获取 username: " + username + " 所对应的用户信息."); //4. 若用户不存在, 则可以抛出 UnknownAccountException 异常 if("unknown".equals(username)){ throw new UnknownAccountException("用户不存在!"); } //5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常. if("monster".equals(username)){ throw new LockedAccountException("用户被锁定"); } //6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo //以下信息是从数据库中获取的. //1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象. Object principal = username; //2). credentials: 密码. Object credentials = "123456"; //3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可 String realmName = getName(); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, realmName); return info; } } ``` ## 10. 密码的比对 密码的比对:通过 AuthenticatingRealm 的 credentialsMatcher 属性来进行的密码的比对。 ## 11. 密码的MD5加密 如何把一个字符串加密为 MD5 替换当前 Realm 的 credentialsMatcher 属性。直接使用 HashedCredentialsMatcher 对象, 并设置加密算法即可。 在[applicationContext.xml](src/main/resources/applicationContext.xml)中 ```xml ``` 既然对密码了,[ShiroRealm](src/main/java/com/lun/shiro/two/realms/ShiroRealm.java)中的部分代码稍作修改。 计算求出密码加密后的密码 ```java public static void main(String[] args) { String hashAlgorithmName = "MD5"; Object credentials = "123456"; Object salt = null;//盐值下一节将提到 int hashIterations = 1024; Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); System.out.println(result); } ``` 得出`fc1709d0a95a6be30bc5926fdb7f22f4` 然后在[ShiroRealm](src/main/java/com/lun/shiro/two/realms/ShiroRealm.java)中的 Object credentials = "123456"; 改为 Object credentials = "fc1709d0a95a6be30bc5926fdb7f22f4"; 便可实现密码加密,通过验证。 ## 12. 密码的MD5盐值加密 为什么使用 MD5 盐值加密: 因为有时希望即使两个原始密码一样,加密后的密码也不一样,这样做相对安全些。 盐是日常生活中的调料。 如何做到: 1. 在 doGetAuthenticationInfo 方法返回值创建 SimpleAuthenticationInfo 对象的时候,需要使用SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName)构造器 2. 使用ByteSource.Util.bytes()来计算盐值。 3. 盐值需要唯一:一般使用随机字符串或 user id 4. 使用 new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);来计算盐值加密后的密码的值。 然后计算密码加密加盐的值 ```java public static void main(String[] args) { String hashAlgorithmName = "MD5"; Object credentials = "123456"; Object salt = ByteSource.Util.bytes("user"); int hashIterations = 1024; Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); System.out.println(result); } ``` 在[ShiroRealm](src/main/java/com/lun/shiro/two/realms/ShiroRealm.java)进行对应的修改 ``` java @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { .... //2). credentials: 密码. Object credentials = null; //"fc1709d0a95a6be30bc5926fdb7f22f4"; if("admin".equals(username)){ credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";/*上面main()计算得出加密加盐的值*/ }else if("user".equals(username)){ credentials = "098d2c478e9c11555ce2823231e02ec1";/*上面main()计算得出加密加盐的值*/ } //3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可 String realmName = getName(); //4). 盐值. ByteSource credentialsSalt = ByteSource.Util.bytes(username); SimpleAuthenticationInfo info = null; info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName); return info; } ``` ## 13. 多Realm验证 > realm /relm/ n. 领域; 场所; 王国 可设置多个Realm设置 [applicationContext.xml](src/main/resources/applicationContext.xml) ```xml ``` 创建添加第二个Realm,[SecondRealm](src/main/java/com/lun/shiro/two/realms/SecondRealm.java) [applicationContext.xml](src/main/resources/applicationContext.xml)实现SecondRealm的Bean ```xml ``` ## 14. 认证策略 AuthenticationStrategy 接口的默认实现: - FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略; - AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可, 和FirstSuccessfulStrategy 不同,将返回所有Realm身份验证成功的认证信息; - AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了。 - ModularRealmAuthenticator 默认是 AtLeastOneSuccessfulStrategy策略 ## 15. 把realms配置给SecurityManager 中的property name="realms"部分可以移动到bean id="securityManager" 中,原因在查阅源码中,securityManager 的authenticator是ModularRealmAuthenticator类的实例,就将securityManager的realms注入authenticator当中。 这样做可以放心为下一节授权做铺垫。 ## 16. 权限配置 - **授权**,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体( Subject)、资源( Resource)、权限( Permission)、角色( Role)。 - **主体**(Subject):访问应用的用户, 在 Shiro 中使用 Subject代表该用户。用户只有授权后才允许访问相应的资源。 - **资源**(Resource):在应用中用户可以访问的 URL, 比如访问 JSP 页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。 - **权限**(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访问用户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控制)等。权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许。 - Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,即实例级别的) - **角色**(Role):权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。 ### Shiro 支持三种方式的授权 ### - 编程式:通过写if/else 授权代码块完成 - 注解式:通过在执行的Java方法上放置相应的注解完成,没有权限将抛出相应的异常 - JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成 ### 默认拦截器 ### Shiro 内置了很多默认的拦截器,比如身份验证、授权等相关的。默认拦截器可以参考org.apache.shiro.web.filter.mgt.DefaultFilter中的枚举拦截器: [Default Filters](http://shiro.apache.org/web.html#default-filters) #### 身份验证相关的拦截器 #### 默认拦截器名|拦截器类|说明(括号里的表示默认值) ---|---|--- authc|org.apache. shiro.web.filter.authc.FormAuthenticationFilter|基于表单的拦截器;如"/**=authc" ,如果没有登录会跳到相应的登录页面登录;主要属性:
usernameParam :表单提交的用户名参数名(username);
passwordParam :表单提交的密码参数名(password);
rememberMeParam :表单提交的密码参数名(rememberMe);
loginUrl :登录页面地址(/login.jsp);
successUrl :登录成功后的默认重定向地址;
failureKeyAttribute :登录失败后错误信息存储key( shirologinFailure);
authcBasic|org.apache. shiro.web.filter.authc.BasicHttpAuthenticationFilter|Basic HTTP身份验证拦截器,主要属性 :applicationName :弹出登录框显示的信息( application) ; logout|org.apache.shiro.web.filter.authc.LogoutFilter|退出拦截器,主要属性: redirectUrl :退出成功后重定向的地址( / ) ;示例:"/logout=logout" user|org.apache.shiro.web.filter.authc.UserFilter|用户拦截器,用户已经身份验证/记住我登录的都可;示例"/** =user" anon|org.apache.shiro.web.filter.authc.AnonymousFilter|匿名拦截器,即不需要登录即可访问; -般用于静态资源过滤;示例:/static/** = anon" #### 授权相关的拦截器 #### 默认拦截器名|拦截器类|说明(括号里的表示默认值) ---|---|--- roles|org.apache.shiro.web.filter.authz.RolesAuthorizationFilter|角色授权拦截器,验证用户是否拥有所有角色;主要属性:loginUrl :登录页面地址(/login.jsp);unauthorizedUrl :未授权后重定向的地址;示例: "/admin/** =roles[admin]" perms|org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter|权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例"/user/**=perms["user:create"]" port|org.apache.shiro.web.filter.authz.PortFilter|端口拦截器,主要属性: port(80) :可以通过的端口;示例"/test= port[80]如果用户访问该页面是非80 ,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样 rest|org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter|rest风格拦截器,自动根据请求方法构建权限字符串( GET=read,POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS =read, MKCOL=create )构建权限字符串;示例:/users=rest[user]” ,会自动拼出"user:read,user:create,user:update,user:delete"权限字符串进行权限匹配(所有都得匹配, isPermittedAll); ssl|org.apache.shiro.web.filter.authz.SslFilter|SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443) ;其他和port拦截器一样; #### 其他的拦截器 #### 默认拦截器名|拦截器类|说明(括号里的表示默认值) ---|---|--- noSessionCreation|org.apache.shiro.web.filter.session. NoSessionCreationFilter|不创建会话拦截器,调用subject.getSession(false)不会有什么问题,但是如果subject.getSession(true)将抛出 ### 实操 新建两页面 - admin.jsp - 只可角色admin访问的 - user.jsp - 只可角色user访问的 在[applicationContext.xml](src/main/resources/applicationContext.xml)中进行配置 ```xml /login.jsp = anon /shiro/login = anon /shiro/logout = logout /user.jsp = roles[user] /admin.jsp = roles[admin] # everything else requires authentication: /** = authc ``` 在[ShiroRealm](src/main/java/com/lun/shiro/two/realms/ShiroRealm.java)设置realmName字符串是用来设置角色 ```java public class ShiroRealm extends AuthorizingRealm { @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { //... info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);//realmName是设置角色的 return info; } } ``` ## 17. 授权流程分析 1. 授权需要继承 AuthorizingRealm 类, 并实现其 doGetAuthorizationInfo 方法 2. AuthorizingRealm 类继承自 AuthenticatingRealm, 但没有实现 AuthenticatingRealm 中的 doGetAuthenticationInfo, 所以认证和授权只需要继承 AuthorizingRealm 就可以了. 同时实现他的两个抽象方法. ```java public class TestRealm extends AuthorizingRealm { //用于授权的方法. @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { // TODO Auto-generated method stub return null; } //用于认证的方法 @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { // TODO Auto-generated method stub return null; } } ``` 调用详细流程可以通过Debug Quickstart的`currentUser.hasRole("schwartz")`进行了解 ## 18. 多Realm授权的通过标准 调用详细流程可以通过Debug Quickstart的`currentUser.hasRole("schwartz")`进行了解 ## 19. 实现授权Realm [ShiroRealm](src/main/java/com/lun/shiro/two/realms/ShiroRealm.java) ```java public class ShiroRealm extends AuthorizingRealm { //... //授权会被 shiro 回调的方法 @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { //1. 从 PrincipalCollection 中来获取登录用户的信息 Object principal = principals.getPrimaryPrincipal(); //2. 利用登录的用户的信息来用户当前用户的角色或权限(可能需要查询数据库) Set roles = new HashSet<>(); roles.add("user"); if("admin".equals(principal)){ roles.add("admin"); } //3. 创建 SimpleAuthorizationInfo, 并设置其 reles 属性. SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles); //4. 返回 SimpleAuthorizationInfo 对象. return info; } } ``` ## 20. 标签 Shiro 提供了 JSTL 标签用于在 JSP 页面进行权限控制,如根据登录用户显示相应的页面按钮。 在JSP文件中引入时 <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> --- guest 标签:用户没有身份验证时显示相应信息,即游客访问信息: 欢迎游客访问 --- user 标签:用户已经经过认证/记住我登录后显示相应的信息。 欢迎 --- authenticated 标签:用户已经身份验证通过,即Subject.login登录成功, 不是记住我登录的 --- notAuthenticated 标签: 用户未进行身份验证, 即没有调用Subject.login进行登录, 包括记住我自动登录的也属于未进行身份验证。 --- pincipal标签: 显示用户身份信息,默认调用Subject.getPrincipal() 获取, 即 Primary Principal。 --- hasRole 标签:如果当前 Subject 有角色将显示 body 体内容: Admin Page --- hasAnyRoles 标签:如果当前Subject有任意一个角色(或的关系)将显示body体内容 --- lacksRole:如果当前 Subject 没有角色将显示 body 体内容 --- hasPermission: 如果当前 Subject 有权限将显示 body 体内容 --- lacksPermission: 如果当前Subject没有权限将显示body体内容。 ## 21. 权限注解 - @RequiresAuthentication:表示当前Subject已经通过login进行了身份验证; 即 Subject. isAuthenticated() 返回 true - @RequiresUser:表示当前 Subject 已经身份验证或者通过记住我登录的。 - @RequiresGuest:表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。 - @RequiresRoles(value={“admin”, “user”}, logical=Logical.AND):表示当前 Subject 需要角色 admin 和user - @RequiresPermissions (value={“user:a”, “user:b”},logical= Logical.OR):表示当前 Subject 需要权限 user:a 或user:b。 [ShiroService](src/main/java/com/lun/shiro/two/services/ShiroService.java) ```java public class ShiroService { @RequiresRoles({"admin"})//这里使用到权限注解,这里RequiresRoles要用到admin角色权限 public void testMethod(){ System.out.println("testMethod, time: " + new Date()); Session session = SecurityUtils.getSubject().getSession(); Object val = session.getAttribute("key"); System.out.println("Service SessionVal: " + val); } } ``` 在applicaContext.xml实现Bean ```xml ``` ```java @Controller @RequestMapping("/shiro") public class ShiroHandler { @Autowired private ShiroService shiroService; @RequestMapping("/testShiroAnnotation") public String testShiroAnnotation(HttpSession session){ session.setAttribute("key", "value12345"); shiroService.testMethod();//该方法调用需要验证权限,用到admin角色权限,否则,就会报错 return "redirect:/list.jsp"; } //... } ``` ## 22. 从数据表中初始化资源和权限 在[applicationContext.xml](src/main/resources/applicationContext.xml)硬编码分配权限角色,这不太好。通常情况下,是需要从数据库中读取权限角色的。 将property name="filterChainDefinitions"注释掉,添加property name="filterChainDefinitionMap" ref="filterChainDefinitionMap" ```xml ... ... ``` 然后,在[applicationContext.xml](src/main/resources/applicationContext.xml)中注册工厂方法Bean: ```xml ``` 然后实现工厂类[FilterChainDefinitionMapBuilder.java](src/main/java/com/lun/shiro/two/factory/FilterChainDefinitionMapBuilder.java) ``` public class FilterChainDefinitionMapBuilder { public LinkedHashMap buildFilterChainDefinitionMap(){ LinkedHashMap map = new LinkedHashMap<>(); //模拟从数据库中取数据 map.put("/login.jsp", "anon"); map.put("/shiro/login", "anon"); map.put("/shiro/logout", "logout"); map.put("/user.jsp", "authc,roles[user]"); map.put("/admin.jsp", "authc,roles[admin]"); map.put("/list.jsp", "user"); map.put("/**", "authc"); return map; } } ``` ## 23. 会话管理 Shiro 提供了完整的企业级会话管理功能, **不依赖于底层容器(如web容器tomcat),不管JavaSE还是JavaEE 环境都可以使用**,提供了会话管理、会话事件监听、会话存储/持久化、容器无关的集群、失效/过期支持、对Web 的透明支持、 SSO 单点登录的支持等特性。 ### Session会话相关的 API ### - Subject.getSession():即可获取会话;其等价于Subject.getSession(true),即如果当前没有创建 Session 对象会创建一个; Subject.getSession(false),如果当前没有创建 Session 则返回null - session.getId():获取当前会话的唯一标识 - session.getHost():获取当前Subject的主机地址 - session.getTimeout() & session.setTimeout(毫秒):获取/设置当前Session的过期时间 - session.getStartTimestamp() & session.getLastAccessTime():获取会话的启动时间及最后访问时间;如果是 JavaSE 应用需要自己定期调用 session.touch() 去更新最后访问时间;如果是 Web 应用,每次进入 ShiroFilter 都会自动调用 session.touch() 来更新最后访问时间。 - session.touch() & session.stop():更新会话最后访问时间及销毁会话;当Subject.logout()时会自动调用 stop 方法来销毁会话。如果在web中, 调用 HttpSession. invalidate()也会自动调用Shiro Session.stop 方法进行销毁Shiro 的会话 - session.setAttribute(key, val) &session.getAttribute(key) &session.removeAttribute(key):设置/获取/删除会话属性;在整个会话范围内都可以对这些属性进行操作 [ShiroHandler](src/main/java/com/lun/shiro/two/handlers/ShiroHandler.java) ```java @Controller @RequestMapping("/shiro") public class ShiroHandler { @Autowired private ShiroService shiroService; @RequestMapping("/testShiroAnnotation") public String testShiroAnnotation(HttpSession session){ session.setAttribute("key", "value12345");//写入到Session shiroService.testMethod(); return "redirect:/list.jsp"; } ... } ``` --- [ShiroService](src/main/java/com/lun/shiro/two/services/ShiroService.java) ```java public class ShiroService { @RequiresRoles({"admin"}) public void testMethod(){ System.out.println("testMethod, time: " + new Date()); //从Session中读取 Session session = SecurityUtils.getSubject().getSession(); Object val = session.getAttribute("key"); System.out.println("Service SessionVal: " + val); } ... } ``` ## 24. SessionDao SessionDao可让Session在数据库中CRUD。 ![SessionDao族谱](image/05.png) - AbstractSessionDAO 提供了 SessionDAO 的基础实现,如生成会话ID等 - CachingSessionDAO 提供了对开发者透明的会话缓存的功能,需要设置相应的 CacheManager - MemorySessionDAO 直接在内存中进行会话维护 - EnterpriseCacheSessionDAO 提供了缓存功能的会话维护, 默认情况下使用 MapCache 实现,内部使用ConcurrentHashMap 保存缓存的会话。 **配置示例** ```xml ``` ```xml ``` ```sql create table sessions ( id varchar(200), session varchar(2000), constraint pk_ sessions primary key(id) ) charset=utf8 ENGINE= InnoDB; ``` Session Dao ```java @Autowired private JdbcTemplate jdbcTemplate = nu1l; @Override protected Serializable doCreate(Session session){ Serializable sessionId = generateSessionId(session); assignSessionId,(session, sessionId); String sql = "insert into sessions(id, session) values(?, ?)";| jdbcTemplate.update(sql,sessionId,serializableUtils. serialize (session); return session.getId(); } @Override protected Session doReadSession(Serializable sessionId){ String sql = "select session from sessions where id=?"; List sessionStrList = jdbcTemplate.queryForList(sql, String.class, sessionId); if (sessionStrList.size() == 0) return null; return SerializableUtils.deserialize(sessionStrList.get(0)); } @Override protected void doUpdate (Session session) { if (session instanceof ValidatingSession && ! ((ValidatingSession)session).isValid()) { return; String sql = "update sessions set session=? where id=?"; jdbcTemplate.update(sql, SerializableUtils.serialize(session), session.getId()); } @Override protected void doDelete (Session session) { String sql = "delete from sessions where id=?"; jdbcTemplate.update(sql, session.getId()); } ``` SerializableUtils ```java public static String serialize (Session session) { try ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectoutputStream oos = new objectOutputStream(bos); oos.writeobject(session); return Base64.encodeTostring(bos.toByteArray()); } catch(Exception e){ throw new RuntimeException("serialize session error", e) ; } } public static Session deserialize (String sessionStr) { try ByteArrayInputStream bis = new ByteArrayInputStream(); Base64.decode(sessionStr)); ObjectInputstream ois = new objectInputstream(bis); return (Session)ois.readobject(); } catch (Exception e) { throw new RuntimeException ("deserialize session error", e); } } ``` ## 25. 缓存 ### CacheManagerAware 接口 ### Shiro 内部相应的组件( DefaultSecurityManager)会自动检测相应的对象(如Realm)是否实现了CacheManagerAware 并自动注入相应的CacheManager ### Realm 缓存 ### Shiro 提供了 CachingRealm,其实现了CacheManagerAware 接口,提供了缓存的一些基础实现; AuthenticatingRealm 及 AuthorizingRealm 也分别提供了对AuthenticationInfo 和 AuthorizationInfo 信息的缓存。 ### Session 缓存 ### - 如 SecurityManager 实现了 SessionSecurityManager,其会判断 SessionManager 是否实现了CacheManagerAware 接口,如果实现了会把CacheManager 设置给它。 - SessionManager 也会判断相应的 SessionDAO(如继承自CachingSessionDAO)是否实现了CacheManagerAware,如果实现了会把 CacheManager设置给它 - 设置了缓存的 SessionManager,查询时会先查缓存,如果找不到才查数据库。 ```xml ... ``` [ehcache.xml](src/main/resources/ehcache.xml) ```xml ... ``` 具体怎么使用的,视频没说,还需查阅更多资料 ## 26. 认证和记住我的区别 Shiro 提供了记住我( RememberMe)的功能,比如访问如淘宝等一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下: 1. 首先在登录页面选中 RememberMe 然后登录成功;如果是浏览器登录,一般会把 RememberMe 的Cookie 写到客户端并保存下来; 2. 关闭浏览器再重新打开,会发现浏览器还是记住你的; 3. 访问一般的网页服务器端还是知道你是谁,且能正常访问; 4. 但是比如我们访问淘宝时,如果要查看我的订单或进行支付时,此时还是需要再进行身份认证的,以确保当前用户还是你。 ## 27. 实现Remember me UsernamePasswordToken类中setRememberMe() [ShiroHandler](src/main/java/com/lun/shiro/two/handlers/ShiroHandler.java) ``` @Controller @RequestMapping("/shiro") public class ShiroHandler { ... @RequestMapping("/login") public String login(@RequestParam("username") String username, @RequestParam("password") String password){ Subject currentUser = SecurityUtils.getSubject(); if (!currentUser.isAuthenticated()) { // 把用户名和密码封装为 UsernamePasswordToken 对象 UsernamePasswordToken token = new UsernamePasswordToken(username, password); // rememberme token.setRememberMe(true);//设置remeberMe try { System.out.println("1. " + token.hashCode()); ... } } ``` 在[applicationContext.xml](src/main/resources/applicationContext.xml)中设置rememberMe的寿命 ```xml ... ... ... ```