思考:一个HTTP请求从客户端发送过来,需要用到哪些对象来协同做事?这些对象都来自哪个框架?在这些对象中,哪些对象是由SpringFramework来管理的?
思考:Shiro安全框架的作用?
答:认证拦截/认证/授权查询/权限控制/加密/会话管理
Shiro安全框架概述
ShiroFramework是apache旗下一个开源安全框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证/权限授权/密码加密/会话管理等功能,组成一个通用的安全认证框架,使用Shiro就可以非常快速的完成认证/授权等功能的开发,降低系统成本。
用户资源访问控制流程图
ShiroFramework概念架构
Shiro架构包含三个主要的理念:Subject/SecurityManager/Realm
Shiro概念架构图
ShiroFramework详细架构
Shiro核心架构图
说明:通过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认证拦截时序图
ShiroFramework认证过程实现
身份认证:判定用户是否是系统的合法用户。
用户访问系统资源时的认证(对用户身份信息的认证)流程图如下:
认证具体流程分析如下:
系统调用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认证业务流程时序图
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负责查询当前用户是否有此授权标识。
用户访问系统资源时授权查询流程图
系统调用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授权查询业务流程时序图
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底层完成的。