名词
------------------------------------------…

  • subject - 主体、起因 - (登录到==》登录成功后 , 全部的记录)
  • principal - 当事人 - (登录成功后,存储用户信息)
  • token - 令牌 - (用户名 、 密码 。。。。)
  • credential - 凭证 - (密码、salt)
  • SecurityManager - 安全管理者 - (处理 认证、授权。。。等业务)
  • authenticator - 认证者 - (is a part of manager)
  • authorizer - 授权者- (is a part of manager)
  • realm - 领域 - (与 Dao 交互)
  • matcher - 匹配器 - (如加密匹配器-指定加密算法)

http://shiro.apache.org/web.html#default-filters
用户名 admin
密码 123456


1 Shiro安全框架简介

1.1 Shiro概述

Shiroapache 旗下一个<mark>开源安全框架</mark>(http://shiro.apache.org/)

(Shiro发音为“shee-roh”,日语“堡垒(Castle)”的意思)

它将软件系统的安全认证相关的功能抽取出来,实现:

  • 用户身份认证
  • 权限授权
  • 加密
  • 会话管理
  • 等功能

组成了一个通用的安全认证框架。

使用 shiro 就可以非常快速的完成认证、授权等功能的开发,降低系统成本。

用户在进行资源访问时,要求系统要对用户进行权限控制
其具体流程如图-1所示:


图-1

1.2 Shiro概要架构

在概念层面, Shiro 架构包含三个主要的理念,如图-2所示:


图-2

其中:

  1. Subject :主体对象,负责提交<mark>用户</mark>认证和授权信息。
  2. SecurityManager :安全管理器,负责认证,授权等业务实现。( 核心 )
  3. Realm :领域对象,负责从<mark>数据层</mark>获取业务数据。

1.3 Shiro详细架构

Shiro 框架进行权限管理时,要涉及到的一些核心对象,主要包括:

  • 认证管理对象
  • 授权管理对象
  • 会话管理对象
  • 缓存管理对象
  • 加密管理对象
  • 以及 Realm 管理对象
    (领域对象:负责处理认证和授权领域的数据访问题)

其具体架构如图-3所示:


图-3

其中:

  1. Subject (主体):与软件交互的一个特定的实体(用户、<mark>第三方</mark>服务等)。
  2. SecurityManager (安全管理器) :Shiro 的<mark>核心</mark>,用来协调管理组件工作。
  3. Authenticator (认证管理器):负责执行<mark>认证</mark>操作。[authenticate/ɔːˈθentɪkeɪt/]
  4. Authorizer (授权管理器):负责<mark>授权</mark>检测。[authorize /ˈɔːθəraɪz/]
  5. SessionManager (会话管理):负责创建并<mark>管理用户 Session 生命周期</mark>,提供一个强有力的 Session 体验。
  6. SessionDAO :代表 SessionManager 执行 Session <mark>持久</mark>(CRUD)动作,它允许任何存储的数据挂接到 session 管理基础上。
  7. CacheManager(缓存管理器):提供创建缓存实例和管理<mark>缓存</mark>生命周期的功能。
  8. Cryptography (加密管理器):提供了<mark>加密方式</mark>的设计及管理。
  9. Realms (领域对象):是<mark>shiro</mark>和你的应用程序安全<mark>数据</mark>之间的<mark>桥梁</mark>。


2 Shiro框架认证拦截实现(filter)

2.1 Shiro基本环境配置

2.1.1 添加shiro依赖

实用 spring 整合 shiro 时,需要在 pom.xml 中添加如下依赖:

<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-spring</artifactId>
   <version>1.4.1</version>
</dependency>

2.1.2 Shiro核心对象配置

第一步:创建 SpringShiroConfig 类。关键代码如下:

package com.cy.pj.common.config;
/** * @Configuration 注解描述的类为一个配置对象, * 此对象也会交给spring管理 */
@Configuration //bean
public class SpringShiroConfig {

}

第二步:在 Shiro 配置类中添加 SecurityManager 配置,关键代码如下:

(注意:要选择 shiro 的包 import org.apache.shiro.mgt.SecurityManager - 默认会导错)


@Configuration //bean
public class SpringShiroConfig {
	/** * @Bean 注解一般用于描述@Configuration注解修饰的类中的方法。 * 目的是将方法的调用交给Spring框架 * 并且Spring框架管理这个方法返回值的对象 * (例如,将次对象存储到Spring容器,并给定作用域。 存储时默认的key为方法名) * * @return SecurityManager (此对象是shiro框架的核心) * * 有抽象,尽量写抽象 */
	@Bean("sManager")
	public SecurityManager securityManager() {
		 DefaultWebSecurityManager sManager = new DefaultWebSecurityManager();
		 return sManager;

	}


}

第三步: 在 Shiro 配置类中添加 ShiroFilterFactoryBean 对象的配置。

通过此对象设置<mark>资源匿名访问</mark>、<mark>认证访问</mark>。

关键代码如下:


@Configuration //bean
public class SpringShiroConfig {
	@Bean
	public SecurityManager securityManager() {
			.....
	}
	/** * 初始化过滤器工厂bean对象(底层要通过此对象创建过滤器工厂,然后通过工厂创建过滤器) * 思考: * 1)为什么要创建过滤器呢?(通过过滤器对象请求数据进行校验,通过过滤操作,例如检查用户是否已认证) * 2)过滤器的请求或响应数据进行过滤时,要指定规则吗?(哪些请求要过滤,哪些不需要过滤?) * 3)对于要通过过滤器的请求,我们通过谁对数据进行校验呢?(例如可以在SecurityManager中进行认证检测) * @param securityManager * @return */
	@Bean
	@Autowired //如何找到形参的实参? @Autowired + @Qualifier
	public ShiroFilterFactoryBean shiroFilterFactory (
		@Qualifier("sManager")  SecurityManager securityManager  <-----------------上面的Bean
		) {
		 ShiroFilterFactoryBean sfBean = new ShiroFilterFactoryBean();
		 sfBean.setSecurityManager(securityManager);
		 //定义map指定请求过滤规则(哪些资源允许匿名访问,哪些必须认证访问)
		 LinkedHashMap<String,String> map =
				 new LinkedHashMap<>(); <------- 保证配置顺序	
				 
				 这里要记住,需要认证访问的资源,要写在下面 (也是为什么要 linkedHashMap)
				 	↓
		 //静态资源允许匿名访问:"anon"
		 map.put("/bower_components/**","anon");
		 map.put("/build/**","anon");
		 map.put("/dist/**","anon");
		 map.put("/plugins/**","anon");
		 //除了匿名访问的资源,其它都要认证("authc")后访问 
		 //  `Authenticator` (认证管理器):负责执行==认证==操作。[`authenticate/ɔːˈθentɪkeɪt/ `]
		 map.put("/**","authc");  
		 sfBean.setFilterChainDefinitionMap(map);
		 return sfBean;
	 }
 }

其配置过程中,对象关系如下图所示:

静态资源默认配置 在这个类里面查看 org.springframework.boot.autoconfigure.web;ResourceProperties

2.2 Shiro登陆页面呈现

2.2.1 服务端Controller实现

业务描述及设计实现

  • 当服务端拦截到用户请求以后,判定此请求是否已经被认证
  • 假如没有认证应该先跳转到登录页面

关键代码分析及实现.
第一步:在 PageController 中添加一个呈现登录页面的方法,关键代码如下:

@RequestMapping("doLoginUI")
public String doLoginUI(){
		return "login";
}

第二步:修改 SpringShiroConfig 类中 shiroFilterFactorybean 的配置,添加登陆url的设置。

@Bean
public ShiroFilterFactoryBean shiroFilterFactory (
		@Autowired SecurityManager securityManager) {
		 ShiroFilterFactoryBean sfBean=
		 new ShiroFilterFactoryBean();
		 sfBean.setSecurityManager(securityManager);
		 
		sfBean.setLoginUrl("/doLoginUI");  <------------------------设置 认证前 页面
		
		 //定义map指定请求过滤规则(哪些资源允许匿名访问,哪些必须认证访问)
		 LinkedHashMap<String,String> map=
				 new LinkedHashMap<>();
		 //静态资源允许匿名访问:"anon"
		 map.put("/bower_components/**","anon");  <----- 这里 “/” 要写,不然找不到
		 map.put("/build/**","anon");
		 map.put("/dist/**","anon");
		 map.put("/plugins/**","anon");
		 //除了匿名访问的资源,其它都要认证("authc")后访问
		 map.put("/**","authc");
		 sfBean.setFilterChainDefinitionMap(map);
		 return sfBean;
}

2.2.2 客户端页面实现

业务描述及设计实现。
/templates/pages/ 添加一个 login.html 页面,然后将项目部署到 web 服务器,并启动测试运行。
关键代码分析及实现。
具体代码见项目中 login.html


3 Shiro框架认证业务实现

3.1 认证流程分析

身份认证即判定用户是否是系统的合法用户,用户访问系统资源时的认证(对用户身份信息的认证)流程图-5所示:


图-5

其中认证流程分析如下:

  1. 系统调用 subjectlogin 方法将用户信息提交给 SecurityManager
  2. SecurityManager 将认证操作委托给认证器对象 Authenticator
    .
    SecurityManager is a Authenticator and a Authorizer
    SecurityManager 自身只有登入、登出、创建 subject 的功能)

    .
  3. Authenticator 将用户输入的身份信息传递给 Realm
    .
    Realm is not a Authenticator
    .
  4. Realm 访问数据库获取用户信息然后对信息进行封装并返回。realm/relm/
  5. Authenticatorrealm 返回的信息进行身份认证。

思考:不使用shiro框架如何完成认证操作?filter,intercetor。

3.2 认证服务端实现

3.2.1 核心业务分析

认证业务 API 处理流程分析,如图-6所示:


图-6

3.2.2 DAO接口定义

业务描述及设计实现。
在用户数据层对象 SysUserDao 中,按特定条件查询用户信息,并对其进行封装。
关键代码分析及实现。
SysUserDao 接口中,添加根据用户名获取用户对象的方法,关键代码如下:

基于用户名(此用户名来自于客户端的输入)查找用户对象
	↓
select * from sys_user where user =#{username }
	↓
SysUser findUserByUserName(String username)

3.2.3 Mapper元素定义

业务描述及设计实现。
根据 SysUserDao 中定义的方法,在 SysUserMapper 文件中添加元素定义。
关键代码分析及实现。
基于用户名获取用户对象的方法,关键代码如下:

  <select id="findUserByUserName" resultType="com.cy.pj.sys.entity.SysUser">
      select *
      from sys_users  
      where username=#{username}
   </select>

3.2.4 Service接口及实现

业务描述及设计实现。
本模块的业务在 Realm 类型的对象中进行实现,我们编写 realm 时,要继承
AuthorizingRealm 并重写相关方法,完成认证及授权业务数据的获取及封装。
关键代码分析及实现。

第一步:定义 ShiroUserRealm 类,关键代码如下:

不想修改 原有的 SysUserServiceImpl
因此,在
service 包下创建 一个新的类

package com.cy.pj.sys.service.realm;


				 我们写的realm必须符合shiro中Ream的一定规范
				 		↓
				1) 只要认证 ==> extends AthenticatingRealm
				2) 又要认证,又要授权 ⇒ extends  AuthorizingRealm
					|
@Servicepublic class ShiroUserRealm extends AuthorizingRealm { 

		暂时不处理
		↓
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		return null;
	}

	@Autowired
	private SysUserDao sysUserDao;
		

										 * 通过此方法完成认证数据的获取及封装,系统
										 * 底层会将认证数据传递认证管理器,由认证
										 * 管理器完成认证操作。
										 	↓
	@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(
			AuthenticationToken token) 
			throws AuthenticationException {

		1.获取用户名(用户页面输入)
					↓	
		UsernamePasswordToken upToken=
		(UsernamePasswordToken)token;    ← 当前应用,这里的token类型 可以通过断点 查看到
				↑
			不转换为UsernamePasswordToken,无法调用下面的方法。
												↓
					String username=upToken.getUsername();

		2.基于用户名查询用户信息
			↓
		SysUser user=sysUserDao.findUserByUserName(username);
		

		/* * 在 Authenticator 中可以看到可以抛出的异常 * @see ExpiredCredentialsException * @see IncorrectCredentialsException * @see ExcessiveAttemptsException * @see LockedAccountException * @see ConcurrentAccessException * @see UnknownAccountException */

		3.判定用户是否存在
			↓
		if(user==null)throw new UnknownAccountException();
		
		4.判定用户是否已被禁用。
			↓
		if(user.getValid()==0)throw new LockedAccountException();
		
		5.封装用户信息 
		(credential/krəˈdenʃl/n. 证书;凭据;国书)
			↓
		ByteSource credentialsSalt =
		ByteSource.Util.bytes(user.getSalt());
		
		SimpleAuthenticationInfo info=
		new SimpleAuthenticationInfo(---- 记住:构建什么对象要看方法的返回值
				user, <---- 数据库 查到的 数据 --- principal (身份)
				user.getPassword(),//hashedCredentials 已加密的密码
				credentialsSalt, //credentialsSalt 对登录密码进行加密时,使用的 盐(Salt)
				user.getName());//realName
						↑
					这里加密算法在下面指定 setCredentialsMatcher

		//6.返回封装结果
		return info;    ← 返回值会传递给认证管理器
									↑
		(后续认证管理器会通过此信息完成认证操作)  
	}
    ....
}



	
	 * 设置凭证匹配器(与用户添加操作使用相同的加密算法)
	 * 这里也可以重写 get xxx 方法
	 */
	@Override
	public void setCredentialsMatcher(  < --- matcher 匹配器
	    CredentialsMatcher credentialsMatcher) {

		//构建凭证匹配对象
		HashedCredentialsMatcher cMatcher=
		new HashedCredentialsMatcher();
		
		//设置加密算法
		cMatcher.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
		
		//设置加密次数
		cMatcher.setHashIterations(1);
		
		super.setCredentialsMatcher(cMatcher); //注意此处的值
	}
	




Authenticator 里面的认证异常

package org.apache.shiro.authc;
public interface Authenticator {
		......
     * @see ExpiredCredentialsException   ← 密码(验证)过期
     * @see IncorrectCredentialsException   ←  密码错误
     * @see ExcessiveAttemptsException
     * @see LockedAccountException   ←  用户被锁定
     * @see ConcurrentAccessException
     * @see UnknownAccountException  ←  用户不存在
     */
    public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
            throws AuthenticationException;

第二步:对此 realm ,需要在 SpringShiroConfig 类中,注入给 SecurityManager 对象
例如:

										添加
@Beanpublic SecurityManager securityManager(Realm realm) {
		 DefaultWebSecurityManager sManager= new DefaultWebSecurityManager();
		 
		 sManager.setRealm(realm);  <----------------添加
		 
		 return sManager;
}

3.2.5 Controller 类实现

业务描述及设计实现。
在此对象中定义相关方法,处理客户端的登陆请求,例如获取用户名,密码等然后提交该 shiro 框架进行认证。
关键代码分析及实现。
第一步:在 SysUserController 中添加处理登陆的方法。

关键代码如下:

	   @RequestMapping("doLogin")
	   @ResponseBody
	   public JsonResult doLogin(String username,String password){
		   //1.获取Subject对象
		   Subject subject=SecurityUtils.getSubject();
		   //2.通过Subject提交用户信息,交给shiro框架进行认证操作
		   //2.1对用户进行封装
		   UsernamePasswordToken token=
		   new UsernamePasswordToken(
				   username,//身份信息
				   password);//凭证信息
		   //2.2对用户信息进行身份认证
		   subject.login(token);
		   //分析:
		   //1)token会传给shiro的SecurityManager
		   //2)SecurityManager将token传递给认证管理器
		   //3)认证管理器会将token传递给realm
		   return new JsonResult("login ok");
	   }

第二步:修改 shiroFilterFactory 的配置,对 /user/doLogin.do 这个路径进行匿名访问的配置

@Bean
public ShiroFilterFactoryBean shiroFilterFactory (
			 @Autowired SecurityManager securityManager) {
		 ShiroFilterFactoryBean sfBean=
		 new ShiroFilterFactoryBean();
		 sfBean.setSecurityManager(securityManager);
		 //假如没有认证请求先访问此认证的url
		 sfBean.setLoginUrl("/doLoginUI");
		 //定义map指定请求过滤规则(哪些资源允许匿名访问,哪些必须认证访问)
		 LinkedHashMap<String,String> map=
				 new LinkedHashMap<>();
		 //静态资源允许匿名访问:"anon"
		 map.put("/bower_components/**","anon");
		 map.put("/build/**","anon");
		 map.put("/dist/**","anon");
		 map.put("/plugins/**","anon");
		map.put("/user/doLogin","anon");   <--------
								具体可以改成什么 ,看官网 http://shiro.apache.org/web.html#default-filters
		 //除了匿名访问的资源,其它都要认证("authc")后访问
		 map.put("/**","authc");
		 sfBean.setFilterChainDefinitionMap(map);
		 return sfBean;
	 }

第三步:当我们在执行登录操作时,为了提高用户体验,可对系统中的异常信息进行处理

例如,在统一异常处理类中添加如下方法:

	@ExceptionHandler(ShiroException.class)
	@ResponseBody
	public JsonResult doHandleShiroException(
			ShiroException e) {
		JsonResult r=new JsonResult();
		r.setState(0);
		if(e instanceof UnknownAccountException) {
			r.setMessage("账户不存在");
		}else if(e instanceof LockedAccountException) {
			r.setMessage("账户已被禁用");
		}else if(e instanceof IncorrectCredentialsException) {
			r.setMessage("密码不正确");
		}else if(e instanceof AuthorizationException) {
			r.setMessage("没有此操作权限");
		}else {
			r.setMessage("系统维护中");
		}
		e.printStackTrace();
		return r;
	}

完整代码

package com.cy.myblog.common.web;

import javax.servlet.http.HttpServletRequest;

import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.cy.myblog.common.vo.JsonResult;

import lombok.extern.slf4j.Slf4j;

//异常(last) -> view
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
	
	@ExceptionHandler(ShiroException.class)
	public JsonResult doHandleShiroException(
			ShiroException e) {
		JsonResult r = new JsonResult();
		r.setState(0);
		if(e instanceof UnknownAccountException) {
			r.setMessage("账户不存在");
		}else if(e instanceof LockedAccountException) {
			r.setMessage("账户已被禁用");
		}else if(e instanceof IncorrectCredentialsException) {
			r.setMessage("密码不正确");
		}else if(e instanceof AuthorizationException) {
			r.setMessage("没有此操作权限");
		}else {
			r.setMessage("系统维护中");
			e.printStackTrace();
		}
		return r;
	}
	
	
	
	
	//放最后,异常按顺序触发
	@ExceptionHandler(Exception.class)
	public JsonResult doExceptionHandle(Exception e , HttpServletRequest r ) {
		log.error("request URL : {} , Exception : {}" ,r.getRequestURL(), e.getMessage()); //URL 详尽的资源信息
		return new JsonResult(e) ; 
	}
	

	
	
}

3.3 认证客户端实现

3.3.1 编写用户登陆页面

/templates/pages/ 目录下添加登陆页面( login.html )。

3.3.2 异步登陆操作实现

点击登录操作时,将输入的用户名,密码异步提交到服务端。

$(function () {
    $(".login-box-body").on("click",".btn",doLogin);
  });
  function doLogin(){
	  var params={
		 username:$("#usernameId").val(),
		 password:$("#passwordId").val()
	  }
	  var url="user/doLogin";
	  $.post(url,params,function(result){
		  if(result.state==1){
			//跳转到indexUI对应的页面
			location.href="doIndexUI?t="+Math.random();
		  }else{
			$(".login-box-msg").html(result.message); 
		  }
	  });
  }

3.4 退出操作配置实现

SpringShiroConfig 配置类中,修改过滤规则,添加黄色标记部分代码的配置,请看如下代码:

@Bean
public ShiroFilterFactoryBean shiroFilterFactory(
			 @Autowired SecurityManager securityManager) {
		 ShiroFilterFactoryBean sfBean=
		 new ShiroFilterFactoryBean();
		 sfBean.setSecurityManager(securityManager);
		 //假如没有认证请求先访问此认证的url
		 sfBean.setLoginUrl("/doLoginUI");
		 //定义map指定请求过滤规则(哪些资源允许匿名访问,哪些必须认证访问)
		 LinkedHashMap<String,String> map=new LinkedHashMap<>();
		 //静态资源允许匿名访问:"anon"
		 map.put("/bower_components/**","anon");
		 map.put("/build/**","anon");
		 map.put("/dist/**","anon");
		 map.put("/plugins/**","anon");
		map.put("/user/doLogin","anon");
		map.put("/doLogout","logout");   <------------添加
						触发此url时系统会会清除xxx 并且调到登录页面
	
		 //除了匿名访问的资源,其它都要认证("authc")后访问
		 map.put("/**","authc");
		 sfBean.setFilterChainDefinitionMap(map);
		 return sfBean;
	 }

4 Shiro框架授权过程实现

4.1 授权流程分析

授权即对用户资源访问的授权(是否允许用户访问此资源),用户访问系统资源时的授权流程如图-8所示:


图-8

其中授权流程分析如下:

  1. 系统调用 subject 相关方法将用户信息(例如 isPermitted )递交给 SecurityManager
    (方式可选)
  2. SecurityManager 将权限检测操作委托给 Authorizer 对象。
    (Securitymanager 本身就是一个 Authorizer 对象)
  3. Authorizer 将用户信息委托给 realm
  4. Realm 访问数据库获取用户权限信息并封装。
  5. Authorizer 对用户授权信息进行判定。

思考:思考不使用 shiro 如何完成授权操作?
interceptor,aop。

4.2 添加授权配置

SpringBoot 中可省略 - )

SpringShiroConfig 配置类中 (SpringShiroConfig 配置类 shiro 中对 spring bean 的扩展)

添加授权时的相关配置:

第一步:配置 bean 对象的生命周期管理( SpringBoot 可以不置)。

package com.edut.springboot.tarena.common.config;

...

import org.apache.shiro.spring.LifecycleBeanPostProcessor

...

@Configuration //bean 
public class SpringShiroConfig {
 ....

@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
	 return new LifecycleBeanPostProcessor();
}

第二步: 通过如下配置要为目标业务对象创建***对象

SpringBoot 中可省略)。

DefaultAdvisorAutoProxyCreator 是用来扫描上下文,寻找所有的 Advistor(通知器)(下面配置) ,将这些 Advisor 应用到所有符合切入点的 Bean 中。
.
所以必须在 lifecycleBeanPostProcessor 创建之后创建,所以加

@DependsOn({"lifecycleBeanPostProcessor"})

保证创建 DefaultAdvisorAutoProxyCreator 之前先创建 LifecycleBeanPostProcessor


spring容器载入bean顺序是不确定的
spring框架没有约定特定顺序逻辑规范。
但spring保证如果A依赖B(如beanA中有@Autowired B的变量),那么B将先于A被加载


@DependsOn("lifecycleBeanPostProcessor")
@Bean
public DefaultAdvisorAutoProxyCreator newDefaultAdvisorAutoProxyCreator() {
		 return new DefaultAdvisorAutoProxyCreator();
}

第三步:配置 advisor 对象, shiro 框架底层会通过此对象的 matchs 方法返回值(<mark>类似切入点</mark>)决定是否创建
***对象,进行权限控制。


在自定义的 shiro 配置类中 添加 
-------------  上面的配置  -------------
 @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
 
 
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();

		//Set whether to proxy the target class directly, instead of just proxying specific interfaces. 
		//Default is "false". 
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
---------------------------------
--------- 添加  -----------------
@Bean
public AuthorizationAttributeSourceAdvisor newAuthorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor= new AuthorizationAttributeSourceAdvisor();
		advisor.setSecurityManager(securityManager);
		return advisor;
}

说明:使用框架最重要的尊重规则,框架规则指定了什么方式就使用什么方式。

4.3 授权 服务端实现

4.3.0 开启权限拦截 - @RequiresPermissions


例如

4.3.1 核心业务分析


permission 许可
Authorizer 授权者,securityManager is a 授权者
realm “领域/权利 查询者” 操作dao

4.3.2 Dao实现

业务描述及设计实现。
基于登陆 用户ID ,认证信息获取登陆用户的用权限信息,并进行封装。
关键代码分析及实现。
第一步:在 SysUserRoleDao 中定义基于 用户id 查找 角色id 的方法,关键代码如下:

	List<Integer> findRoleIdsByUserId(Integer id);

第二步:在 SysRoleMenuDao 中定义基于 角色id 查找 菜单id 的方法,关键代码如下:
<mark>dao中不允许重载方法</mark>

	List<Integer> findMenuIdsByRoleIds(
			@Param("roleIds")Integer[] roleIds);

第三步:在 SysMenuDao 中基于 菜单id 查找 权限标识 的方法,关键代码如下:

	List<String> findPermissions(
			@Param("menuIds")
			Integer[] menuIds);

4.3.3 Mapper实现

业务描述及设计实现。
基于 Dao 中方法,定义映射元素。
关键代码分析及实现。
第一步:在 SysUserRoleMapper 中定义 findRoleIdsByUserId 元素。关键代码如下:

 <select id="findRoleIdsByUserId" resultType="int">
           select role_id
           from sys_user_roles
           where user_id=#{userId}        
</select>

第二步:在 SysRoleMenuMapper 中定义 findMenuIdsByRoleIds 元素。关键代码如下:

    <select id="findMenuIdsByRoleIds" resultType="int">
         select menu_id
         from sys_role_menus
         where role_id in 
         <foreach collection="roleIds" open="(" close=")" separator="," item="item">
               #{item}
         </foreach>
</select>

第三步:在SysMenuMapper中定义findPermissions元素,关键代码如下:

    <select id="findPermissions" resultType="string">
       select permission <!-- sys:user:update -->
       from sys_menus
       where id in 
       <foreach collection="menuIds" open="(" close=")" separator="," item="item">
            #{item}
       </foreach>
   </select>

4.3.4 Service实现

业务描述及设计实现。
ShiroUserReam 类中,重写对象realm的 doGetAuthorizationInfo 方法,并完成用户权限信息的获取以及封装,最后将信息传递给授权管理器完成授权操作。
关键代码分析及实现。
修改 ShiroUserRealm 类中的 doGetAuthorizationInfo 方法,关键代码如下:

@Service
public class ShiroUserRealm extends AuthorizingRealm {
	@Autowired
	private SysUserDao sysUserDao;
	@Autowired
	private SysUserRoleDao sysUserRoleDao;
	@Autowired
	private SysRoleMenuDao sysRoleMenuDao;
	@Autowired
	private SysMenuDao sysMenuDao;
	
----------------------------------

	基于此方法进行授权信息的获取和封装,
	会将封装好的数据传递给 SecurityManager 对象,
	由此对象进行用户权限检测和授权
	回顾,项目中业务的实现 
	1)角色和菜单是多对多的关系 (关系的维护方在 sys_role_menus)
	2)用户和角色是多对多的关系 (关系的维护方在 sys_user_roles)

	思考,业务中的资源分配过程:
	1.将菜单授权角色
	2.角色授权用户对象

--------------------------------------

	/**通过此方法完成授权信息的获取及封装*/
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(	PrincipalCollection principals) {

----------------------------

		1.获取登录用户信息
		2.基于登录用户获取用户对应的角色 id 并校验
		3.基于角色id获取角色对应的菜单id 并校验
		4.基于菜单id获取菜单模块的授权表示(sys:user:update , sys:user:delete....)
		5.封装结果并返回

--------------------------------------------

		//1.获取登录用户信息,例如用户id
		SysUser user=(SysUser)principals.getPrimaryPrincipal(); <---- 获取 授权的对象
		Integer userId=user.getId();
		
		//2.基于用户id获取用户拥有的角色(sys_user_roles)
		List<Integer> roleIds=
				sysUserRoleDao.findRoleIdsByUserId(userId);
		
		if(roleIds==null||roleIds.size()==0)throw new AuthorizationException();
		
		//3.基于角色id获取菜单id(sys_role_menus)
		Integer[] array={};
		List<Integer> menuIds=
				sysRoleMenuDao.findMenuIdsByRoleIds(
										roleIds.toArray(array));
				
	    if(menuIds==null||menuIds.size()==0) throw new AuthorizationException();
	    
		//4.基于菜单id获取权限标识(sys_menus)
	    List<String> permissions=
	   			 sysMenuDao.findPermissions(
	    							menuIds.toArray(array));
	    	
		//5.对权限标识信息进行封装并返回
	    Set<String> set=new HashSet<>();  <------用set进行去 重 
	    										(dao 也有 distinct,
	    										但是可扩展性不高, 这里最好也去重一下 。 )
	    for(String per:permissions){
	    	if(!StringUtils.isEmpty(per)){   <----- StringUtils spring的工具类 - 【去除 "空" 的permission】
	    		set.add(per);}																		否则一直报错
	    }
	    SimpleAuthorizationInfo info=
	   				 new SimpleAuthorizationInfo();
	    info.setStringPermissions(set);
		return info;//返回给授权管理器
	}

}

4.4 授权访问实描述现

在需要进行授权访问的业务层方法上添加执行此方法需要的权限标识,例如

@RequirePermissions 为shiro框架中的一个注解,此注解用于描述需要授权访问的方法,

注解内部 value 属性的值 (为符合特定规范的一个字符串),表示要访问此方法需要的权限

1) 系统执行此方法时会判定此方法上是否有 @RequiresPerssions 注解,并且获取注解中的内容
2) 基于 shrio 框架底层机制获取当前登录用户,然后获取用户的权限信息。
3) 检测登录用户的权限中是否包含 @RequiresPermission注解中的指定的字符串,
														↓
										假如包含 , 就授权可访问方法

一级菜单 : 二级菜单 : ....

@RequiresPermissions(“sys:user:update”)

说明:此要注解一定要添加到业务层方法上。

5 Shiro扩展功能应用

5.1 Shiro缓存配置

当我们进行授权操作时,每次都会从数据库查询用户权限信息,

为了提高授权性能,可以将用户权限信息查询出来以后进行缓存,
下次授权时从缓存取数据即可。

Shiro 中内置缓存应用实现,其步骤如下:
第一步:在 SpringShiroConfig 中配置缓存 Bean对象 ( Shiro 框架提供)。


配置shiro中的缓存管理器,通过此缓存管理对象中的缓存对象来对授权信息进行缓存
、
注意: 
1) 方法名不能写 cacheManager(可能会与spring中的CacheManager对象有重复)
2) 方法的返回值对象要注入给 SecurityManager 对象

当我们访问一个授权方法时, shiro框架会调用SecurityManager对象进行对用户进行权限检测
此时,需要获取权限信息
	如何获取这个权限信息呢?
	------
	假如没有配置缓存,每次都会查询数据库。
	配置缓存后,SecurityManager对象会优先从shiro缓存查询数据,	缓存中没有才会查询数据库

注意,当修改了用户权限,有了缓存后,此权限应该在下一次登录以后是生效。



@Bean
public CacheManager shiroCacheManager(){  // 这个不是cache,而是cache的管理器!
	 return new MemoryConstrainedCacheManager(); 
}

cache 在哪? 底层实现 ==》 重写map 软引用

说明:这个 CacheManager 对象的名字不能写 cacheManager
因为 spring 容器中 已经存在一个名字为 cacheManager 的对象了.

第二步:修改 securityManager 的配置,将缓存对象注入给 SecurityManager 对象。

																	添加
@Beanpublic SecurityManager securityManager(Realm realm,	CacheManager cacheManager) {
		 DefaultWebSecurityManager sManager=new DefaultWebSecurityManager();
		 sManager.setRealm(realm);
		 sManager.setCacheManager(cacheManager);   <-----------  添加
		 return sManager;
}

说明:对于 shiro 框架而言,还可以借助第三方的缓存产品(例如 redis )对用户的权限信息进行cache操作.

5.2 Shiro 功能:[记住我]

记住我功能是要在用户登录成功以后,假如关闭浏览器,下次再访问系统资源(例如首页 doIndexUI )时,无需再执行登录操作。

5.2.1 客户端业务实现

在页面上选中记住我,然后执行提交操作,将用户名,密码,记住我对应的值提交到控制层,如图-8所示:


图-8

其客户端 login.html 中关键 JS 实现:

 function doLogin(){
	  var params={
		 username:$("#usernameId").val(),
		 password:$("#passwordId").val(),
		 isRememberMe:$("#rememberId").prop("checked"),
	  }
	  var url="user/doLogin"; 
	  console.log("params",params);
	  $.post(url,params,function(result){
		  if(result.state==1){
			//跳转到indexUI对应的页面
			location.href="doIndexUI?t="+Math.random();
		  }else{
			$(".login-box-msg").html(result.message); 
		  }
		  return false;//防止刷新时重复提交
	  });
  }

5.2.2 服务端业务实现

服务端业务实现的具体步骤如下:
第一步:在 SysUserController 中的 doLogin 方法中基于是否选中记住我,设置 tokensetRememberMe 方法。

@RequestMapping("doLogin")
	 @ResponseBody
	 public JsonResult doLogin(
			 boolean isRememberMe,
			 String username,
			 String password) {
		 //1.封装用户信息
		 UsernamePasswordToken token=
		 new UsernamePasswordToken(username, password);
		 if(isRememberMe) {
			token.setRememberMe(true); 
		 }
		 //2.提交用户信息
		 Subject subject=SecurityUtils.getSubject();
		 subject.login(token);//token会提交给securityManager
		 return new JsonResult("login ok");
	 }

第二步:在 SpringShiroConfig 配置类中添加记住我配置,关键代码如下:


配置记住我管理器对象,shiro框架中的记住我,底层要基于Cookie对象实现

									配置
	@Beanpublic RememberMeManager rememberMeManager() {
		 CookieRememberMeManager cManager= new CookieRememberMeManager();
			SimpleCookie cookie=new SimpleCookie("rememberMe"); < ----- cookie key
		 cookie.setMaxAge(10*60);
		 cManager.setCookie(cookie);
		 return cManager;
	 }

第三步:在 SpringShiroConfig 中修改 securityManager 的配置,为
securityManager 注入 rememberManager 对象。

	 @Bean
	 public SecurityManager securityManager(Realm realm,CacheManager cacheManager
								RememberMeManager rememberManager) {  <------添加
		 DefaultWebSecurityManager sManager=
		 new DefaultWebSecurityManager();
		 sManager.setRealm(realm);
		 sManager.setCacheManager(cacheManager);
		 sManager.setRememberMeManager(rememberManager);  <-----添加
		 return sManager;
	 }

第四步:修改 shiro 的过滤认证级别,将 /**=author修改为/**=users

@Bean
	 public ShiroFilterFactoryBean shiroFilterFactory(
			 SecurityManager securityManager) {
		 ShiroFilterFactoryBean sfBean= new ShiroFilterFactoryBean();
		 sfBean.setSecurityManager(securityManager);
		 //假如没有认证请求先访问此认证的url
		 sfBean.setLoginUrl("/doLoginUI");
		 //定义map指定请求过滤规则(哪些资源允许匿名访问,哪些必须认证访问)
		 LinkedHashMap<String,String> map=
				 new LinkedHashMap<>();
		 //静态资源允许匿名访问:"anon"
		 map.put("/bower_components/**","anon");
		 map.put("/build/**","anon");
		 map.put("/dist/**","anon");
		 map.put("/plugins/**","anon");
		 map.put("/user/doLogin","anon");
		 map.put("/doLogout", "logout");//自动查LoginUrl
		 //除了匿名访问的资源,其它都要认证("authc")后访问
		 


		 map.put("/**","user");//authc <------------------------------ 这里!!!





		 sfBean.setFilterChainDefinitionMap(map);
		 return sfBean;
	 }

说明:查看浏览器 cookie 设置,可在浏览器中输入如下语句。

chrome://settings/content/cookies

5.3 Shiro会话时长配置

使用 shiro 框架实现认证操作,<mark>用户登录成功会将用户信息写入到会话对象中</mark>,
其默认时长为30分钟,


假如需要对此进行配置,可参考如下配置:

第一步:在 SpringShiroConfig 类中,添加会话管理器配置。关键代码如下:

@Bean   
public SessionManager sessionManager() {

		 DefaultWebSessionManager sManager= new DefaultWebSessionManager(); 
		 
		 sManager.setGlobalSessionTimeout(60*60*1000); //1h
		 return sManager;
}

第二步:在 SpringShiroConfig 配置泪中,对安全管理器 securityManager
增加 sessionManager 值的注入,关键代码如下:

@Bean
public SecurityManager securityManager(
   			Realm realm,CacheManager cacheManager,
   			RememberMeManager rememberManager,
   			SessionManager sessionManager) {     <-------------------------添加
   	 DefaultWebSecurityManager sManager=
   	 new DefaultWebSecurityManager();
   	 sManager.setRealm(realm);
   	 sManager.setCacheManager(cacheManager);
   	 sManager.setRememberMeManager(rememberMeManager);
   	 sManager.setSessionManager(sessionManager);       <-----------------------添加
   	 return sManager;
}

# 装饰设计模式



6 Shiro总结

6.1 重点和难点分析

  1. shiro 认证过程分析及实现(判定用户身份的合法性)。

  2. Shiro 授权过程分析及实现(对资源访问进行权限检测和授权)。
    基于 AOP 实现

  3. Shiro 缓存,会话时长,记住我等功能实现。

6.2 常见FAQ

  1. 说说 shiro 的核心组件?

  2. 说说 shiro 的认证流程,你如何知道的,为什么要认证?

  3. 说说 shiro 的授权流程,你如何知道流程是这样的,为什么要进行授权?

  4. Shiro 中内置缓存应用实现?为什么使用此缓存?是否可以使用第三方缓存?

  5. Shiro 中的记住我功能如何实现?为什么要使用这个功能?

  6. Shiro 中会话 session 的默认时长是多少,你怎么知道的?

3 Bug分析