- Shiro基础

>> Shiro 基础

官网地址

~ Shiro 简介

Apache Shiro是Java的一个安全(权限)框架,对比Spring Security,没有Spring Security做的功能强大,但小而简单;

Shiro可以完成:认证授权加密会话管理与Web集成缓存等;


~ Shiro 结构与功能

- Shiro基础_第1张图片
- Shiro基础_第2张图片
- Shiro基础_第3张图片

1、Subject:任何可以与应用程序交互的“用户”

应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject;
Subject 代表了当前“用户”, 这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;与 Subject 的所有交互都会委托给 SecurityManager; Subject 其实是一个门面,SecurityManager 才是实际的执行者;

2、SecurityManager:安全管理器

所有与安全有关的操作都会与 SecurityManager 交互,且其管理着所有 Subject

它负责与 Shiro 的其他组件进行交互,相当于SpringMVC 中的 DispatcherServlet,是 Shiro 的核心; 所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证、授权、会话及缓存的管理;

SecurityManager 接口继承了 Authenticator,另外还有一个 ModularRealmAuthenticator实现,其委托给多个Realm 进行验证,验证规则通过 AuthenticationStrategy 接口指定;

3、Authenticator/Authentication:认证器 / 身份认证(登录)

Authenticator:负责 Subject 认证,是一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

Authenticator 的职责是验证用户帐号,是 Shiro API 中身份验证核心的入口点:如果验证成功,将返回AuthenticationInfo 验证信息,此信息中包含了身份及凭证;如果验证失败将抛出相应的 AuthenticationException 异常;

Authentication身份认证/登录,验证用户是不是拥有相应的身份;
使用Shiro完成用户的密码匹配以此来完成用户的登录;

4、Authorizer/Authorization:授权器/权限验证

Authorizer:授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

Authorization授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作;
如:验证某个用户是否拥有某个角色;或者细粒度的验证某个用户对某个资源是否具有某个权限;

5、Realm

ShiroRealm 获取安全数据(如用户、角色、权限);认证或授权的时候,可以有 1 个或多个 Realm,用来访问持久化的数据;
就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/ 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource;
可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC 实现,也可以是内存实现等等;由用户提供;所以一般在应用中都需要实现自己的 Realm;

Realm接口常用方法:

String getName(); // 返回一个唯一的Realm名字;
boolean supports(AuthenticationToken token); // 判断此Realm是否支持此token;
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token); // 根据token获取认证信息;

一般继承 AuthorizingRealm(授权)即可;其继承了 AuthenticatingRealm(即身份验证),而且也间接继承了 CachingRealm(带有缓存实现);
- Shiro基础_第4张图片

6、SessionManager:Session管理器

SessionManager:Session管理器,管理 Session 生命周期的组件,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境;
Session可以是普通 JavaSE 环境的Shiro提供的Session,也可以是 Web 环境的HttpSession;

7、CacheManager:缓存控制器

CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能;
**Caching**缓存
比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

8、Cryptography:加密

Cryptography加密,保护数据的安全性;
如密码盐值加密存储到数据库,而不是明文存储;
Shiro 提高了一些常见的加密组件用于如密码加密/解密;

9、Web Support:Web 支持

可以非常容易的集成到Web 环境;

10、Concurrency:在多线程的情况下进行授权认证

Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

11、Testing:提供测试支持

12、Run As:让已经登录的用户以其他用户的身份来操作当前项目和系统;

13、Remember Me:记住我

这个是非常常见的功能,即一次登录后,下次再来的话不用登录了;


~ Shiro 入门代码

public static void main(String[] args) {
    // 获取当前的Subject:使用SecurityUtils的方法;
    Subject currentUser = SecurityUtils.getSubject();
    // 获取Session:使用Subject的方法;
    Session session = currentUser.getSession();
    // 设置Session属性
    session.setAttribute("name", "zxj");
    // 从Session中获取数据
    String name = session.getAttribute("name");
    // 测试当前用户是否已经被认证:是否已经登录;使用Subject实例的方法;
    if(!currentUser.isAuthenticated()){
        // 把用户名和密码封装为UsernamePasswordToken对象;用户名、密码从页面表单传入;
        UsernamePasswordToken token = new UsernamePasswordToken("zxj", "123456");
        token.setRememberMe(true);
        try{
            // 使用Subject实例的login方法,传入token,执行登录
            // 能都登录成功取决于配置文件中是否配置了这个用户,以及密码是否正确;
            currentUser.login(token);
        }catch (UnknownAccountException e){        // 账户不存在
            log.info("There is no user with username of " + token.getPrincipal());
            return;
        }catch (IncorrectCrendentialsException e){ // 密码错误
            log.info("Password for account " + token.getPrincipal() + " was incorrect!");
            return ;
        }catch (LockedAccountException e){         // 用户被锁定
            log.info("用户被锁定,请联系管理员解锁!");
            return ;
        }catch (AuthenticationException e){ 
            // 所有认证时异常的父类
        }
    }
    // 测试用户是否有某个角色;
    if(currentUser.hasRole("root")){...}
    // 测试用户是否有某个权限;
    if(currentUser.isPermitted("某个权限:..")){...}
    // 用户可以对某个用户执行某个操作;
    if(currentUser.isPermitted("user:delete:zhangsan")){...}

    // 执行登出
    currentUser.logout();
}

~ Shiro 集成Spring

1、创建Maven项目,配置好Spring和SpringMVC;

2、在web.xml中配置spring容器的监听器和SpringMVC前端控制器:


    <context-param>
        <param-name>contextConfigLocationparam-name>
        <param-value>classpath:applicationContext.xmlparam-value>
    context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
    listener>
    
    <servlet>
        <servlet-name>springmvcservlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
        
        <init-param>
            <param-name>contextConfigLocationparam-name>
            <param-value>classpath:springmvc.xmlparam-value>
        init-param>
    servlet>
    <servlet-mapping>
        <servlet-name>springmvcservlet-name>
        
        
        <url-pattern>/url-pattern>
    servlet-mapping>
    
    <filter>
        <filter-name>characterEncodingFilterfilter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
        <init-param>
            <param-name>encodingparam-name>
            <param-value>UTF-8param-value>
        init-param>
    filter>
    <filter-mapping>
        <filter-name>characterEncodingFilterfilter-name>
        <url-pattern>*.htmlurl-pattern>
    filter-mapping>
    <filter-mapping>
        <filter-name>characterEncodingFilterfilter-name>
        <url-pattern>*.jspurl-pattern>
    filter-mapping>

3、springmvc.xml:配置视图解析器、批量扫描包…


<beans  xmlns="......"
    
    <mvc:annotation-driven/>
    <mvc:default-servlet-handler/>
    
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/"/>
        <property name="suffix" value=".jsp"/>
    bean>
beans>

4、在web.xml中配置ShiroFilter
DelegatingFilterProxy 实际上是Filter的一个代理对象,作用是自动到 Spring 的IOC容器中查找查找和 对应的 filter bean 并把所有 Filter 的操作委托给它;也可以通过 targetBeanName 的初始化参数来配置 filter bean 的 id;

<filter>
    <filter-name>shiroFilterfilter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
    <init-param>
        <param-name>targetFilterLifecycleparam-name>
        <param-value>trueparam-value>
    init-param>
filter>
<filter-mapping>
    <filter-name>shiroFilterfilter-name>
    <url-pattern>/*url-pattern>
filter-mapping>

3、在Spring的配置文件applicationContext.xml中配置Shiro:

4、创建自定义ShiroRealm类实现Realm接口:


~ Shiro 工作流程

Shiro 提供了与 Web 集成的支持,其通过一个 ShiroFilter 入口来拦截需要安全控制的URL,然后进行相应的控制;
ShiroFilter 类似于如 Strut2/SpringMVC 这种 web 框架的前端控制器,是安全控制的入口点,其负责读取配置(如ini 配置文件),然后判断URL 是否需要登录/权限等工作;

1、在web.xml文件中配置的shiroFilter是一个入口,它可以拦截所有请求资源;
有两种页面可以直接访问:在applicationContext.xml的shiroFilter的filterChainDefinitions属性中配置的过滤器是anon的页面,和在这里面没有进行配置的页面;

2、请求不能直接访问的资源,会被重定向到shiroFilter的loginUrl属性定义的页面中;

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login.jsp"/>
        <property name="successUrl" value="/list.jsp"/>
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/> 
        <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap">property>
        <property name="filterChainDefinitions">
            <value>
                /login.jsp = anon
                /shiro/login = anon
                /shiro/logout = logout
                /user.jsp = roles[user]
                /admin.jsp = roles[admin]
                # everything else requires authentication:
                /** = authc
            value>
        property>
    bean>

~ Shiro 中的默认过滤器

org.apache.shiro.web.filter.mgt.DefaultFilter这时shiro的一个枚举类,里面列举了shiro的默认过滤器;

在spring的配置文件applicationContext.xml的配置的的属性filterChainDefinitions用来配置哪些页面需要受保护,以及访问这些页面需要的权限;

1、配置的格式是:url = 拦截器[参数],如果当前请求的 url 匹配某个 url 模式,将会执行其配置的拦截器;

2、匹配模式: url 模式使用 Ant 风格模式,Ant 路径通配符支持?(匹配一个字符)* (匹配零个或多个字符串)** (匹配多重路径)

3、URL匹配顺序: URL权限采取第一次匹配优先的方式,即从头开始,使用第一个匹配的 url 模式对应的拦截器链;

4、 身份认证相关的过滤器

  • anon(anonymous): 拦截器表示匿名访问(即不需要登 录即可访问);
  • authc (authentication):认证过滤器;拦截器表示需要身份认证通过后才能访问对应的资源;
  • logout:登出过滤器;

5、授权相关的过滤器:

  • roles:角色授权过滤器;有对应的权限,才能访问对应的资源;
    /user.jsp = roles[user]
    /admin.jsp = roles[admin]
  • perms:权限授权过滤器;验证用户是否拥有所有权限;

6、Permissions配置:
(1)规则资源标识符:操作:对象实例ID,即对哪个资源的哪个实例可以进行什么操作;默认支持通配符权限字符串:冒号表示资源/操作/实例的分割,逗号表示操作的分割,星号*表示任意资源/操作/实例;

(2)多层次管理

  • 例如:user:queryuser:edit
  • 冒号是一个特殊字符,它用来分隔权限字符串的下一部件:第一部分是权限被操作的领域(打印机),第二部分是被执行的操作;
  • 多个值:每个部件能够保护多个值,因此,除了授予用户user:queryuser:edit 权限外,也可以简单地授予他们一个:user:query, edit
  • 还可以用 * 号代替所有的值,如:user:* , 也可以写:*:query,表示某个用户在所有的领域都有 query 的权限;

(3)实例级访问控制

  • 这种情况通常会使用三个部件:域、操作、被付诸实施的实例,如:user:edit:manager
  • 也可以使用通配符来定义,如:user:edit:*user:*:*user:*:manager
  • 部分省略通配符:缺少的部件意味着用户可以访问所有与之匹配的值,比如:user:edit 等价于 user:edit :*user 等价于 user:*:*
  • 注意:通配符只能从字符串的结尾处省略部件,也就是说 user:edit 并不等价于 user:*:edit

- Shiro基础_第5张图片


>> Shiro 认证

~ Shiro 身份验证

**身份验证:**一般需要提供如身份 ID 等一些标识信息来表明登录者的身份,如提供 email,用户名/密码来证明;

在 shiro 中,用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能验证用户身份;最常见的 principalscredentials 组合就是用户名/密码;

  • principals:身份,即主体的标识属性,可以是任何属性,如用户名、 邮箱等,唯一即可;一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名/邮箱/手机号;
  • credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等;

~ Shiro 认证流程步骤

【前三步在handler中完成;】
1、调用 SecurityUtils.getSubject()方法获取当前的Subject;
2、调用 subject.isAuthenticated()方法测试当前的用户是否已经被认证,即是否已经登录;
3、若返回false,表示没有被认证, 则把从页面表单传来的 用户名和密码封装为 UsernamePasswordToken 对象;
创建一个表单页面,把请求提交到 SpringMVC 的 Handler,然后获取用户名和密码;
4、执行登录: 调用 Subject的login(AuthenticationToken) 方法; UsernamePasswordToken类是接口AuthenticationToken的实现类;
5、自定义Realm的方法, 从数据库中获取对应的记录, 返回给Shiro;
自定义Realm需要继承 org.apache.shiro.realm.AuthenticatingRealm 类,实现 doGetAuthenticationInfo(AuthenticationToken) 方法;
6、由 shiro 完成对密码的比对;

  • 1、首先调用 Subject.login(token) 进行登录,其会自动委托给 SecurityManager
  • 2、SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
  • 3、Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
  • 4、Authenticator 可能会委托给相应的 AuthenticationStrategy 进 行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
  • 5、Authenticator 会把相应的 token 传入 Realm,从 Realm 获取 身份验证信息,如果没有返回/抛出异常表示身份验证失败了;此处可以配置多个Realm,将按照相应的顺序及策略进行访问;

~ Shiro 认证流程实现

1、创建一个表单页面;

<form action="shiro/login" method="POST">
	username: <input type="text" name="username"/><br><br>
	password: <input type="password" name="password"/><br><br>
	<input type="submit" value="Submit"/>
form>

2、在applicationContext.xml中的shiroFilter Bean处添加配置/shiro/login = anon,使表单提交的请求可以匿名访问;

3、创建handler接收表单提交请求,获取用户名、密码,判断是否登录;

@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);
            token.setRememberMe(true);
            try {
            	System.out.println("1. " + token.hashCode());
                currentUser.login(token); // 这个token传给了自定义Realm的doGetAuthenticationInfo方法入参;
            }catch (AuthenticationException ae) {
            	System.out.println("登录失败: " + ae.getMessage());
            }
        }
		return "redirect:/list.jsp";
	}
}

4、创建自定义Realm继承AuthorizingRealm类,实现doGetAuthenticationInfo方法:

public class ShiroRealm extends AuthorizingRealm {
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		// 1、把 AuthenticationToken 转换为 UsernamePasswordToken; 
		UsernamePasswordToken upToken = (UsernamePasswordToken) token;
		// 2、从 UsernamePasswordToken 中来获取 username;(表单传入的)
		String username = upToken.getUsername();
		// 3、根据用户名从数据库中获取用户信息,然后根据用户信息的获取情况决定是否要抛异常;
		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;
		Object principal = upToken.getPrincipal(); // 认证的实体信息;可以是username,也可以是数据表对应的用户的实体类对象;从数据库中获取的;
		Object credentials = "从数据库中获取的密码";   // 密码,从数据库中获取的;
		String realmName = getName();			   // 当前realm对象的name,调用父类的getName()方法即可;
		ByteSource credentialsSalt = ByteSource.Util.bytes(username); // 盐值:防止相同的密码加密之后的值相同;
		SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
		return info;
	}
}

5、 Shiro 比对密码:
UsernamePasswordToken保存的是从表单获取的用户名和密码,SimpleAuthenticationInfo保存的是从数据库中查询的密码,shiro 通过 AuthenticatingRealmcredentialsMatcher 属性来完成这两个对象中保存的密码的比对;

6、登出:直接在applicationContext.xml中的shiroFilter Bean处添加配置/shiro/logout = logout


~ Shiro 密码MD5盐值加密

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);
	}

1、如何把一个字符串加密为 MD5 :
applicationContext.xml中配置的自定义Realm的Bean,替换Bean的 credentialsMatcher 属性,直接使用 HashedCredentialsMatcher 对象,指定凭证匹配器,给凭证匹配器指定使用MD5加密算法;指定凭证匹配器之后,shiro会自动将表单输入的密码进行MD5加密;

<bean id="jdbcRealm" class="com.zxj.shiro.realms.ShiroRealm">
	<property name="credentialsMatcher">
		<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
			<property name="hashAlgorithmName" value="MD5"/>
			<property name="hashIterations" value="1024"/> // 加密次数
	bean>
	property>
bean>

2、为什么使用 MD5 盐值加密: 防止相同的密码加密之后的值相同;

3、如何做到盐值加密:
(1)在自定义Realm的 doGetAuthenticationInfo 方法返回值创建 SimpleAuthenticationInfo 对象的时候,使用
SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName) 构造器;
(2)使用 ByteSource.Util.bytes() 来计算盐值;
(3)盐值需要唯一:一般使用随机字符串或 user id;
(4)使用 new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); 来计算盐值加密后的密码的值;

~ Shiro 多Realm和认证策略

在某些特定的场合,可能将安全数据放在不同的数据库中,比如:MySQL中使用MD5加密算法,Oracle数据库使用SHA1加密算法;这时候进行数据认证的时候就要同时访问这两个数据库,就需要使用多个Realm;

1、创建多个自定义Realm,继承AuthorizingRealm类,实现doGetAuthenticationInfo方法:

2、在applicationContext.xml配置文件中配置Bean:

<bean id="jdbcRealm" class="com.zxj.shiro.realms.ShiroRealm">
	<property name="credentialsMatcher">
      	<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
          	<property name="hashAlgorithmName" value="MD5"/>
          	<property name="hashIterations" value="1024"/>   // 加密次数
      	bean>
  	property>
bean>
<bean id="secondRealm" class="com.atguigu.shiro.realms.SecondRealm">
  	<property name="credentialsMatcher">
      	<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
          	<property name="hashAlgorithmName" value="SHA1"/>
          	<property name="hashIterations" value="1024"/> 
      	bean>
 	property>
bean>

3、在applicationContext.xml配置文件中配置认证器:

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
	<property name="cacheManager" ref="cacheManager"/>
	<property name="authenticator" ref="authenticator"/>
	<property name="realms">
	    <list>
	        <ref bean="jdbcRealm"/>
	        <ref bean="secondRealm"/>
	    list>
	property>
	<property name="rememberMeManager.cookie.maxAge" value="10"/>
	bean>
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
	<property name="authenticationStrategy">
		<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"/>
    property>
bean>

多Realm认证策略 AuthenticationStrategy

定义多个Realm,指定认证策略,来决定怎么样才叫认证成功;

认证策略接口AuthenticationStrategy有三个默认实现:

  • FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第 一个 Realm 身份验证成功的认证信息,其他的忽略;
  • AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和 FirstSuccessfulStrategy 不同,将返回所有Realm身份验证成功的认证信 息;
  • AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有 Realm身份验证成功的认证信息,如果有一个失败就失败了;

ModularRealmAuthenticator 认证器默认是 AtLeastOneSuccessfulStrategy 策略;


>> Shiro 授权

~ Shiro 概念

授权,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据/页面操作等);在授权中需了解的几个关键对象:

  • 主体(Subject)访问应用的用户,在 Shiro 中使用 Subject 代表该用户;用户只有授权后才允许访问相应的资源;

  • 资源(Resource)在web应用中用户可以访问的 URL,比如访问 JSP 页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源;用户只要授权后才能访问

  • 权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力;即权限表示在应用中用户能不能访问某个资源,如:访问用户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控制)等;
    权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许;
    Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限, 即实例级别的);

  • 角色(Role):权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便;典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限;


~ Shiro 授权方式

编程式:通过写if/else 授权代码块完成;
注解式:通过在执行的Java方法上放置相应的注解完成,没有权限将抛出相 应的异常;
JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成;(隐藏没有访问权限的资源)

- Shiro基础_第6张图片


~ Shiro 授权类、方法

1、授权需要继承 AuthorizingRealm 类,并实现其 doGetAuthorizationInfo 方法;
2、AuthorizingRealm 类继承自 AuthenticatingRealm,但没有实现 AuthenticatingRealm 中的
doGetAuthenticationInfo,所以认证和授权只需要继承 AuthorizingRealm 就可以了,同时实现他的两个抽象方法;

public class ShiroRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //1、从 PrincipalCollection 中来获取登录用户的信息;
        Object principal = principals.getPrimaryPrincipal();
        //2、利用登录的用户的信息来获取当前用户的角色或权限(可能需要查询数据库)
        Set<String> roles = new HashSet<>();
        roles.add("user");
        if("admin".equals(principal)){
            roles.add("admin");
        }
        //3、创建 SimpleAuthorizationInfo,并设置其 roles 属性;
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
        //4、返回 SimpleAuthorizationInfo 对象;
        return info;
    }
}

~ Shiro 授权流程

  • 1、首先调用 Subject.isPermitted*/hasRole* 接口,其会委托给SecurityManager,而 SecurityManager 接着会委托给 Authorizer;
  • 2、Authorizer是真正的授权者,如果调用如 isPermitted(“user:view”),其首先会通过PermissionResolver 把字符串转换成相应的 Permission 实例;
  • 3、在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角色/权限用于匹配传入的角色/权限;
  • 4、Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果有多个Realm,会委托给 ModularRealmAuthorizer 进行循环判断, 如果匹配如 isPermitted*/hasRole* 会返回true,否则返回false表示,授权失败;
  • ModularRealmAuthorizer 进行多 Realm 匹配流程:首先检查相应的 Realm 是否实现了实现了Authorizer;如果实现了 Authorizer,那么接着调用其相应的 isPermitted*/hasRole* 接口进行匹配;如果有一个Realm匹配那么将返回 true,否则返回 false;
    - Shiro基础_第7张图片

~ Shiro 权限标签

Shiro 提供了 JSTL 标签用于在 JSP 页面进行权限控制,如:根据登录用户显示相应的页面按钮;
在jsp页面引入shiro标签:<%@ 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体内容;
    用户拥有user或admin角色
  • lacksRole:如果当前 Subject 没有角色将显 示 body 体内容;
    用户没有admin角色
  • hasPermission:如果当前 Subject 有权限 将显示 body 体内容;
    用户拥有权限user:create
  • lacksPermission:如果当前Subject没有权限将显示body体内容;
    用户没有权限user:create

~ Shiro 权限注解

可以写在Controller层,也可以写在service层;
注意:若service的类上添加的事务的注解,那么就不能再在该类上添加权限注解,添加了也无效,这时需要将权限注解加到Controller上;

  • @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:auser:b
@Controller
@RequestMapping("/shiro")
public class ShiroHandler {
    @Autowired
    private ShiroService shiroService;
    @RequestMapping("/testShiroAnnotation")
    public String testShiroAnnotation(HttpSession session){
        session.setAttribute("key", "value12345");
        shiroService.testMethod();
        return "redirect:/list.jsp";
    }
}
public class ShiroService {
    @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);
    }
}



~ Shiro 权限配置

受保护的资源跟对应的权限关系,有两种配置方式;
1、在applicationContext.xml文件中配置shiroFilter Bean,在Bean中配置filterChainDefinitions属性,属性的值配置资源与权限的对应关系;

2、资源与权限的对应关系写到数据库中,通过sql获取;
(1)shiroFilter Bean使用filterChainDefinitionMap属性,引用一个Map类型的Bean;
(2)构建一个实例工厂类,类中方法返回LinkHashMap类型的集合;
(3)通过实例工厂类的Bean,配置实例Bean;

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
	<property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login.jsp"/>
    <property name="successUrl" value="/list.jsp"/>
    <property name="unauthorizedUrl" value="/unauthorized.jsp"/> 
    <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"/>
    
bean>

<bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"/>
<bean id="filterChainDefinitionMapBuilder" class="com.atguigu.shiro.factory.FilterChainDefinitionMapBuilder">bean>
public class FilterChainDefinitionMapBuilder {
    public LinkedHashMap<String, String> buildFilterChainDefinitionMap(){
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        map.put("/login.jsp", "anon");
        map.put("/shiro/login", "anon");
        map.put("/**", "authc");
        return map;
    }
}

>> Shiro 会话管理

~ Shiro 概述

Shiro的Session和Http的Session是一致的,都表示是一个客户端和服务器的一次会话;

Shiro提供了完整的企业级会话管理功能,不依赖于底层的容器,就是说它没有web服务器,没有tomcat也可以使用Session;So不管是JavaSE、JavaEE环境下都可以使用Shiro的Session;提供了会话管理会话事件监听会话存储/ 持久化、容器无关的集群失效/过期支持对Web 的透明支持SSO 单点登录的支持等特性;


~ Shiro 会话相关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):设置/获取/删除会话属性:在整个会话范围内都可以对这些属性进行操作;


~ Shiro 会话监听器

会话监听器用于监听会话创建onStart(Session)、过期onExpiration(Session)及停止onStop(Session)事件;和Http的Session监听器类似;

在Web层建议使用Http的Session,但是在传统的web应用中,是无法在Service中访问HttpSession的,但是可以使用Shiro的Session在Service中访问session中属性值;


~ Shiro SessionDao

- Shiro基础_第8张图片
SessionDao可以将Session写到数据库中,然后就可以对Session进行增删改查操作;开发时一般继承EnterpriseCacheSessionDao

AbstractSessionDAO 提供了 SessionDAO 的基础实现,如生成会话ID等;
CachingSessionDAO 提供了对开发者透明的会话缓存的功能,需要设置相应CacheManager
MemorySessionDAO 直接在内存中进行会话维护;
EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用 MapCache 实现,内部使用 ConcurrentHashMap 保存缓存的会话;
该类提供了doCreate()doReadSession()doUpdate()doDelete四个方法;

doCreate()方法创建SessionDao时需要获取sessionId,generateSessionId方法返回sessionID时中需要用到sessionIdGenerator属性,所以在配置文件中配置EnterpriseCacheSessionDAO的时候,要设置sessionIdGenerator这个属性;

protected Serializable doCreate(Session session) {
	Serializable sessionId = generateSessionId(session);
    assignSessionId(session, sessionId);
    return sessionId;
}
 protected Serializable generateSessionId(Session session) {
 	if (this.sessionIdGenerator == null) {
        String msg = "sessionIdGenerator attribute has not been configured.";
        throw new IllegalStateException(msg);
    }
    return this.sessionIdGenerator.generateId(session);
}

SessionIdGenerator接口有两个实现类:
- Shiro基础_第9张图片

配置:
- Shiro基础_第10张图片

配置好SessionDao以后,就可以对Session进行增删改查操作了;需要使用对象输入/输出流进行读写操作;

步骤:
创建类继承EnterpriseCacheSessionDAO类,实现四个方法;需要读写、更新的时候,需要对进行Session序列化/反序列化操作,利用对象输入输出流进行读写,对象输入输出流需要包装成字节数组输入输出流;

- Shiro基础_第11张图片
- Shiro基础_第12张图片
- Shiro基础_第13张图片
- Shiro基础_第14张图片
- Shiro基础_第15张图片


~ Shiro 会话验证调度器

Shiro 提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将停止会话;

出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的;但是如在 web 环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,Shiro 提供了会话验证调度器 SessionValidationScheduler

Shiro 也提供了使用Quartz会话验证调度器: QuartzSessionValidationScheduler


~ Shiro 缓存

Shiro 内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如Realm)是否实现了 CacheManagerAware 接口,并自动注入相应的 CacheManager;就是说如果实现了这个接口的话,Shiro会自动将CacheManager注入给这个对象,这时这个对象就可以使用缓存了;

Realm实现的接口中,就实现了CacheManager这个接口了,所以Realm是有缓存的;缓存的实际意义是:用户登录的时候,会进入到授权的方法,再点登录的时候,如果使用了缓存,就不再进授权的方法了,不使用缓存的话,每次点击登录都会授权一次;

Shiro 提供了 CachingRealm,其实现了 CacheManagerAware 接口,提供了缓存的一些基础实现;
• AuthenticatingRealm 及 AuthorizingRealm 也分别提供了对AuthenticationInfo 和 AuthorizationInfo 信息的缓 存;

在配置Realm的时候,可以设置一些缓存的属性:指定缓存的名字、指定是否使用缓存等;可以在ehcache.xml中分别为授权、认证设置不同的缓存策略;
- Shiro基础_第16张图片


~ Shiro 的 RememberMe

Shiro 提供了记住我(RememberMe)的功能,比如访问如淘宝等一些网站时,关闭了浏览器,下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下:
1、首先在登录页面选中 RememberMe 然后登录成功;如果是浏览器登录,一般会把RememberMe 的Cookie 写到客户端并保存下来;
2、关闭浏览器再重新打开,会发现浏览器还是记住你的;
3、访问一般的网页服务器端还是知道你是谁,且能正常访问;
4、但是比如我们访问淘宝时,如果要查看我的订单或进行支付时,此时还是需要再进行身份认证的,以确保当前用户还是你;

认证和记住我 区别

subject.isAuthenticated() 表示用户进行了身份验证登录的,即使有 Subject.login 进行了登录;
subject.isRemembered() 表示用户是通过记住我登录的, 此时可能并不是真正的你在访问的;(如你的朋友使用你的电脑,或者你的cookie 被窃取)
两者二选一,即 subject.isAuthenticated()==true,则 subject.isRemembered()==false;反之一样;

建议:
访问一般网页:如个人在主页之类的,我们使用user 拦截器即可,user 拦截器只要用户登录
(isRemembered() || isAuthenticated())过即可访问成功;

访问特殊网页:如我的订单,提交订单页面,我们使用 authc 拦截器即可,authc 拦截器会判断用户是否是通过 Subject.login(isAuthenticated()==true)登录的,如 果是才放行,否则会跳转到登录页面叫你重新登录;

实现:
user过滤器:用户拦截器,用户已经身份认证,或是通过记住我登录的,都可以,/** = user

(1)在登录页面添加“记住我”checkbox,checkbox的值会传给Controller,在Controller的方法中判断传过来的值是否为空,若不为空,则设置token.setRememberMe(true);,若为空,就不用设置这个了;

(2)设置“记住我”的时间:


        
        
        ......
        
    

如果要自己做RememeberMe,需要在登录之前这样创建Token: UsernamePasswordToken(用户名,密码,是否记住我),且调用 UsernamePasswordToken 的token.setRememberMe(true); 方法;


代码


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
							http://www.springframework.org/schema/beans/spring-beans-4.0.xsd ">

    
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="cacheManager" ref="cacheManager"/>
        <property name="authenticator" ref="authenticator"/>
        <property name="realms">
            <list>
                <ref bean="jdbcRealm"/>
                <ref bean="secondRealm"/>
            list>
        property>
        <property name="sessionManager" ref="sessionManager">property>
        // <property name="rememberMeManager.cookie.maxAge" value="10"/>
        <property name="rememberMeManager" ref="rememberMeManager"/>
    bean>
    
	<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
		<property name="globalSessionTimeout" value="1800000"/>
		<property name="deleteInvalidSessions" value="true"/>
		<property name="sessionValidationSchedulerEnabled" value="true"/>
		<property name="sessionDAO" ref="sessionDAO"/>
	bean>
	<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
		<constructor-arg value="rememberMe"/>
		<property name="httpOnly" value="true"/>
		<property name="maxAge" value="2592000"/>
	bean>
	
	<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
		<property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
		<property name="cookie" ref="rememberMeCookie"/>
	bean>

	
	<bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>
	
	<bean id="sessionDAO" class="com.atguigu.shiro.realms.MySessionDao">
		<property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
		<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
	bean>
	
    
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    bean>

    <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
        <property name="authenticationStrategy">
            <bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"/>
        property>
    bean>
    
    <bean id="jdbcRealm" class="com.zxj.shiro.realms.ShiroRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="MD5"/>
                <property name="hashIterations" value="1024"/>   // 加密次数
            bean>
        property>
    bean>
    <bean id="secondRealm" class="com.atguigu.shiro.realms.SecondRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <property name="hashAlgorithmName" value="SHA1"/>
                <property name="hashIterations" value="1024"/> 
            bean>
        property>
    bean>
    
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    bean>

    
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login.jsp"/>
        <property name="successUrl" value="/list.jsp"/>
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/> 
        <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"/>

        
        <property name="filterChainDefinitions">
            <value>
                /login.jsp = anon
                /shiro/login = anon
                /shiro/logout = logout
                /user.jsp = roles[user]
                /admin.jsp = roles[admin]
                # everything else requires authentication:
                /** = authc
            value>
        property>
    bean>

    
    <bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap"/>
    <bean id="filterChainDefinitionMapBuilder" class="com.atguigu.shiro.factory.FilterChainDefinitionMapBuilder">bean>
    <bean id="shiroService" class="com.atguigu.shiro.services.ShiroService">bean>
beans>
public class MySessionDao extends EnterpriseCacheSessionDAO {
	@Autowired
	private JdbcTemplate jdbcTemplate = null;
	@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<String> 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());
	}
}

public class SerializableUtils {
	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);
		}
	}
}

你可能感兴趣的:(Java,-,框架)