思考:一个HTTP请求从客户端发送过来,需要用到哪些对象来协同做事?这些对象都来自哪个框架?在这些对象中,哪些对象是由SpringFramework来管理的?




思考:Shiro安全框架的作用?



答:认证拦截/认证/授权查询/权限控制/加密/会话管理




Shiro安全框架概述



ShiroFramework是apache旗下一个开源安全框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证/权限授权/密码加密/会话管理等功能,组成一个通用的安全认证框架,使用Shiro就可以非常快速的完成认证/授权等功能的开发,降低系统成本。




用户资源访问控制流程图




安全管理架构分为几级 安全管理机构框架图_spring



ShiroFramework概念架构


Shiro架构包含三个主要的理念:Subject/SecurityManager/Realm


Shiro概念架构图


安全管理架构分为几级 安全管理机构框架图_安全_02



ShiroFramework详细架构

Shiro核心架构图


安全管理架构分为几级 安全管理机构框架图_apache_03


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


Subject(主体对象):与软件交互的一个特定的实体(用户/第三方服务等)


SecurityManager(安全管理器):Shiro的核心对象,用来协调管理组件工作。


Authenticator(认证管理器):负责执行认证操作


Authorizer(授权管理器):负责授权检测


SessionManager(会话管理器):负责创建并管理用户Session生命周期,提供一个强有力的Session体验。


SessionDAO(会话管理器代表者):代表SessionManager执行Session持久(CRUD)操作,它允许任何存储的数据挂接到Session管理基础上。


CacheManager(缓存管理器):提供创建缓存实例和管理缓存生命周期的功能。


Cryptography(加密管理器):提供加密方式的设计及管理。


Realms(领域对象):是Shiro和你的应用程序安全数据之间的桥梁。



Spring整合Shiro实现Shiro认证拦截


说明:所谓认证拦截就是当用户访问非匿名资源时候,需要进行身份信息认证,也就是登录。


在项目中添加ShiroFramework框架依赖


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


配置Shiro核心对象


创建spring-shiro.xml核心配置文件,并添加如下配置


注意:Tomcat启动时会加载此配置文件


配置SecurityManager对象


<!-- 配置SecurityManager对象 -->
<!-- 配置安全管理者对象 -->
<!-- 注意:此对象属于FhiroFramework -->
<!-- 注意:前端控制器会将token传给此对象;SecurityManager将token传递给认证管理器;认证管理器会将token传递给realm -->
<!-- 所以:是执行到后端控制器对应的方法之后,token才传给SecurityManager对象 -->
<!-- 思考:如果只是认证拦截,要不要用到此对象? -->
<!-- 答:如果只是认证拦截,无需用到此对象 -->
<bean id="securityManager"  class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="Realm" ref="shiroUserRealm"/>
</bean>


配置ShiroFilterFactoryBean对象


<!-- 配置ShiroFilterFactoryBean对象 -->
<!-- 配置过滤工厂Bean对象,通过此对象创建过滤Filter对象 -->
<!-- 注意:此对象属于shiro -->
<!-- 注意:Spring中的授权过滤代理对象需要注入此工厂对象 -->
<bean id="shiroFilterFactory"  class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- 依赖注入安全管理者对象 -->
<!-- 思考:为什么要注入此对象? -->
<!-- 答:因为当执行认证操作的时候,登陆请求到达前端控制器,前端控制器调用后端控制器的登陆方法,将token传给shiro的SecurityManager -->
<!-- 注意:前端控制器只与Filter过滤器对象有关系,一般都是Filter过滤器对象依赖前端控制器 -->
<!-- 结论:所有请求一定会经过Filter过滤器对象,此对象由ShiroFilterFactoryBean.getObject创建 -->
<property name="SecurityManager"  ref="securityManager" />

<!-- 当浏览器访问必须要实名访问的资源时候,需要执行认证拦截  -->
<!-- 注意:此URL还是会到达前端控制器 -->
<property name="LoginUrl" value="/doLoginUI.do"/><!--  注意:此请求将发给前端控制器 -->

<!-- 设置请求过滤规则 -->
<property name="FilterChainDefinitionMap">
<map>
<entry key="/bower_components/**"  value="anon" /> <!-- 匿名访问 -->
<entry key="/build/**" value="anon" />  <!-- 匿名访问 -->
<entry key="/dist/**" value="anon" /> <!--  匿名访问 -->
<entry key="/plugins/**" value="anon" />  <!-- 匿名访问 -->
<entry key="/user/doLogin.do"  value="anon"/> <!-- 登陆前允许匿名访问 -->
<entry key="/**" value="authc" /><!-- 必须认证 -->
</map>
</property>
</bean>


说明:spring-shiro.xml配置文件需要在spring-configs.xml进行导入


最后需要在项目的web.xml文件中添加如下配置:


<!-- 配置授权过滤代理对象 -->
<!-- 注意:此对象是属于SpringFramework中的一个对象 -->
<!-- 注意:此对象最终要找前端控制器 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilterFactory</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern> <!-- 认证拦截所有请求 -->
</filter-mapping>
<!-- 说明 -->
<!-- targetBeanName名字由DelegatingFilterProxy对象底层设置并读取 -->
<!-- shiroFilterFactory名字要与ShiroFilterFactoryBean配置的id相同 -->


服务端实现


在PageController中添加一个呈现登录页面的方法


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


在spring-shiro.xml中的shiroFilterFactoryBean的配置中添加如下配置


<property name="LoginUrl" value="/doLoginUI.do"/>

客户端实现


在/WEB-INF/pages/添加一个login.html页面



Shiro认证拦截时序图


安全管理架构分为几级 安全管理机构框架图_java_04



ShiroFramework认证过程实现


身份认证:判定用户是否是系统的合法用户。


用户访问系统资源时的认证(对用户身份信息的认证)流程图如下:


安全管理架构分为几级 安全管理机构框架图_apache_05


认证具体流程分析如下:


系统调用Subject的login方法将用户信息(token)提交给SecurityManager


SecurityManager将认证操作委托给认证器对象Authenticator


Authenticator将身份信息传递给Realm,此Realm具体实现类由我们自己写,实际就是一个特殊的Service层对象


Realm访问数据库获取用户信息然后对信息进行封装并返回


Authenticator对Realm返回的信息进行身份认证


思考:如果不使用ShiroFramework如何完成认证操作?


答:Filter/Intercetor



Shiro认证服务端具体案例实现


Dao层接口实现


业务描述:在SysUserDao中根据用户名获取用户对象


业务实现:根据用户名查询用户对象的方法定义


代码实现:


SysUser findUserByUserName(String username);



Mapper映射文件定义


根据SysUserDao中定义的抽象方法,添加SQL元素定义


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



Service接口实现


业务描述:此认证模块的Service层可以借助Realm实现,我们编写Realm时可以继承AuthorizingRealm并重写相关的方法完成相关业务实现。


注意:此Realm对象实则是一个特殊的Service对象,没有实现任何一个Service层接口,直接继承AuthorizingRealm


Realm类代码实现:


package com.db.sys.service.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import  org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.db.sys.dao.SysUserDao;
import com.db.sys.entity.SysUser;
/**
* 领域对象
* 一个特殊的业务层对象
* @author HuangZhengHua
*
*/
@Service
public class ShiroUserRealm extends AuthorizingRealm{
@Autowired
private SysUserDao sysUserDao;

/**
* 设置凭证匹配器
* 思考:为什么要设置凭证匹配器?
* 答:因为需要将前端传来的用户密码执行加密操作
*/
public void setCredentialsMatcher(CredentialsMatcher  credentialsMatcher) {
//构建凭证匹配对象
HashedCredentialsMatcher cMatcher = new  HashedCredentialsMatcher();
//设置加密算法
cMatcher.setHashAlgorithmName("MD5");
//设置加密次数
cMatcher.setHashIterations(1);
super.setCredentialsMatcher(cMatcher);
}

/**
* 通过此方法完成认证数据的获取及封装;
* 系统底层会将认证数据传递给认证管理器;
* 由认证管理器完成认真操作。
*/
@Override
protected AuthenticationInfo  doGetAuthenticationInfo(AuthenticationToken token) throws  AuthenticationException {
//1.获取用户名(用户在浏览器页面输入)
//注意:token包含前端传来的用户信息
UsernamePasswordToken upToken =  (UsernamePasswordToken)token;
String username = upToken.getUsername();
//2.基于用户名查询用户信息
SysUser user =  sysUserDao.findUserByUserName(username);
//3.判定用户是否存在
if(user == null)
throw new UnknownAccountException();
//4.判定用户是否已经被禁用
if(user.getValid() == 0)
throw new LockedAccountException();
//5.封装用户信息
ByteSource credentialsSalt =  ByteSource.Util.bytes(user.getSalt());
// 记住:构建什么对象要看方法的返回值
SimpleAuthenticationInfo info = new  SimpleAuthenticationInfo(
user, // principal (身份)
user.getPassword(), // hashedCredentials
credentialsSalt, // credentialsSalt
getName());// realName
//6.返回封装结果
return info;
//注意:返回值会传递给认证管理器_Authenticator(后续认证管理器会通过此信息完成认证操作)
}

@Override
protected AuthorizationInfo  doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
}


对此Realm在spring-shiro.xml配置文件中以属性的形式注入给SecurityManager对象


注意:如果之前已经配置上,则无需配置


<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="Realm" ref="shiroUserRealm"/>
</bean>



Controller类实现


在SysUserController类中添加doLogin登录方法,具体代码实现如下:


/**
* 前段用户登录认证
* 注意:此后端控制器方法对应前端页面执行登陆操作
* @param username
* @param password
* @return
*/
@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");
}


注意:此方法接受用户及密码参数,并对其进行有效验证


说明:此控制层方法必须允许匿名访问,需要在spring-shiro.xml配置文件中对/user/doLogin.do这个URL进行允许匿名访问的配置,例码如下:


<entry key="/user/doLogin.do" value="anon"/>



Shiro认证业务流程时序图


安全管理架构分为几级 安全管理机构框架图_安全_06



Shiro认证客户端实现


编写用户登录页面


在WEB-INF/pages/目录下添加登陆页面(login.html)


异步登录操作前端JS代码实现


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



ShiroFramework授权查询&权限控制过程实现


授权查询流程分析


授权:对用户资源访问的授权;而Shiro负责查询当前用户是否有此授权标识。


用户访问系统资源时授权查询流程图


安全管理架构分为几级 安全管理机构框架图_安全_07


系统调用Subject相关方法将用户信息(例如isPermitted)递交给SecurityManager


SecurityManager将权限检测操作委托给Authorizer对象


Authorizer将用户信息委托给Realm


Realm访问数据库获取用户权限信息并封装


Authorizer对用户授权信息进行判定


思考:如果不使用Shiro,如何完成授权操作?intercetor,aop



添加授权配置


在spring-shiro.xml中追加如下配置:


<!-- 授权查询 -->
<!-- 配置Service层Bean代理对象的生命周期管理 -->
<bean id="lifecycleBeanPostProcessor"  class="org.apache.shiro.spring.LifecycleBeanPostProcessor"></bean>

<!-- 配置Service层Bean代理对象的代理 -->
<bean  class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"  depends-on="lifecycleBeanPostProcessor"></bean>

<!-- 配置授权属性 -->
<!-- 授权拦截器 -->
<!-- 注意:这里拦截的是Service层方法请求 -->
<bean  class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="SecurityManager"  ref="securityManager"></property>
</bean>



授权查询服务端实现


注意:这里主要体现实现思路,因为不同的业务有不同的实现思路,具体的代码略。


基于用户ID查询角色ID信息


基于角色ID查询菜单ID信息


基于菜单ID查询权限标识信息



权限控制实现


在需要进行授权访问的方法上添加执行此方法需要的权限标识,例码:


@RequiredPermissions(“sys:user:valid”)

Shiro授权查询业务流程时序图

安全管理架构分为几级 安全管理机构框架图_java_08



ShiroFramework应用增强


Shiro缓存配置:当执行认证查询的时候不用每次都从数据库中调取数据


在pom.xml中添加ehcache依赖,例码:


<!-- 配置授权缓存依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>


添加ehcache配置文件


在项目中的src/main/resources目录下添加ehcache.xml


注意:可以从其他项目中拷贝


在spring-shiro.xml中配置Bean标签,例码:


<!-- 配置授权查询缓存 -->
<bean id="cacheManager"  class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile"  value="classpath:ehcache.xml" />
</bean>


将cacheManager添加到securityManager中,例码:


<!-- 配置SecurityManager对象 -->
<!-- 配置安全管理者对象 -->
<!-- 注意:此对象属于FhiroFramework -->
<!-- 注意:前端控制器会将token传给此对象;SecurityManager将token传递给认证管理器;认证管理器会将token传递给realm -->
<!-- 所以:是执行到后端控制器对应的方法之后,token才传给SecurityManager对象 -->
<!-- 思考:如果只是认证拦截,要不要用到此对象? -->
<!-- 答:如果只是认证拦截,无需用到此对象 -->
<bean id="securityManager"  class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="Realm" ref="shiroUserRealm"/>
<!-- 配置授权查询缓存 -->
<property name="cacheManager" ref="cacheManager"/>
</bean>



ShiroFramework记住我功能实现


暂略



ShiroFramework总结


Shiro认证拦截过程总结:


DelegatingFilterProxy.doFilter(ServletRequest , ServletResponse , FilterChain):请求到达Spring授权过滤代理器对象...→


ShiroFilterFactoryBean$SpringShiroFilter.doFilter(ServletRequest , ServletResponse , FilterChain):通过Shiro中的过滤工厂对象创建SpringShiroFilter过滤器对象执行过滤...→


ProxiedFilterChain.doFilter(ServletRequest , ServletResponse):ProxiedFilterChain过滤器对象执行过滤...→


DispatcherServlet.service(HttpServletRequest , HttpServletResponse):前端控制器调用service方法判断请求方式...→


RequestMappingHandlerAdapter.invokeHandlerMethod(HttpServletRequest , HttpServletResponse , HandlerMethod):通过RequestMapping映射器对象获取URL映射的Controller后端控制器对象+Method方法对象...→


PageController.doLoginUI():Controller后端控制器对象执行doLoginUI()方法返回登录界面...→


注意:认证拦截无需DefaultWebSecurityManager,但是需要配置DefaultWebSecurityManager,请求最后才达Controller后端控制器,返回登录界面



Shiro认证过程总结:


DelegatingFilterProxy开始的;


SysUserController.doLogin(String , String):登录请求到达Controller后端控制器...→


WebDelegatingSubject.login:主体对象Subject将token当前登录用户信息传给DefaultWebSecurityManager安全管理器对象...→


DefaultWebSecurityManager.login(Subject , AuthenticationToken):安全管理器对象将token当前登录用户信息传给Authenticator认证管理器对象...→


ModularRealmAuthenticator.doSingleRealmAuthentication(Realm , AuthenticationToken):认证管理器将token当前登录用户信息传给领域对象ShiroUserRealm...→


ShiroUserRealm.doGetAuthenticationInfo:根据用户名到数据库查询,然后将数据封装到SimpleAuthenticationInfo对象中,最后将此对象返回给Authenticator认证管理器对象做比对...→再次发起请求



Shiro授权查询过程总结:


SysUserController.doValidById(Integer , integer):请求到达Controller后端控制器...→


$Proxy39.validById(Integer , Integer , String):Service层AOP动态代理对象,通过反射获取设置了权限控制方法上的注解,也就是权限标识符...→


JdkDynamicAopProxy.invoke(Object , Method , Object[]):InvocationHandler接口的其中一个实现类对象,调用invoke方法...→


......中间省略的过程可以Debug调试查看...→


WebDelegatingSubject.checkPermission(String):主体对象调用checkPermission方法将从方法对象上获取的权限标识符提交给DefaultWebSecurityManager安全管理器对象...→


DefaultWebSecurityManager.checkPermission(PrincipalCollection , String):安全管理器对象将对象信息传给Authorizer权限管理器对象...→


ModularRealmAuthorizer.isPermitted(PrincipalCollection , String):权限管理器对象将当前用户信息传递给领域对象ShiroUserRealm...→


ShiroUserRealm.doGetAuthorizationInfo:根据用户ID查询当前用户的权限标识符,然后将数据封装到SimpleAuthorizationInfo对象中,最后将此对象返回给Authorizer权限管理器对象做比对...→执行目标方法


注意:AOP(面向切面编程)是一种编程思想,Shiro授权查询正是运用了这种编程思想,实现授权查询;底层会生成一个业务层的动态代理对象($Proxy39),当执行了扩展业务后,最后执行目标类的业务方法。


注意:当然执行完扩展业务后,不一定会执行目标方法,如果权限管理器在比对时发现此用户没有此权限,将会提醒“没有此权限!”;


权限管理器可以看成是一个切面,且这个切面不是我们自己写的,是ShiroFramework底层完成的。