转载地址:https://blog.csdn.net/xtayfjpk/article/details/53729135
Shiro源码分析----认证流程
2016年12月18日 19:25:12
阅读数:4485
由于本文是基于源码分析Shiro认证流程,所以假设阅读者对Shiro已经有一定的了解,如果对Shiro还不大了解的话,推荐一下博文:跟我学Shiro目录贴
Apache Shiro作为一个优秀的权限框架,其最重要的两项工作:其一是认证,即解决登录的用户的身份是否合法;其二是用户登录后有什么样的权限。本文将基于Shiro源码来剖析Shiro的认证流程,只有深层次的理解Shiro认证流程,认证过程中各个组件的作用,才能在实际应用中灵活使用。由于Shiro一般用于Web环境且会与Spring集成使用,所以此次认证流程的分析的前提也是Web环境且Shiro已与Spring集成。
特别说明:本文使用的Shiro版本:1.2.2。
Shiro与Spring集成时,需要在web.xml中配置Shiro入口过滤器:
-
<filter>
-
<filter-name>shiroFilter</filter-name>
-
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
-
<async-supported>true</async-supported>
-
<init-param>
-
<param-name>targetFilterLifecycle</param-name>
-
<param-value>true</param-value>
-
</init-param>
-
</filter>
-
<filter-mapping>
-
<filter-name>shiroFilter</filter-name>
-
<url-pattern>/*</url-pattern>
-
</filter-mapping>
熟悉Spring的人应该都知道DelegatingFilterProxy的作用,该Spring提供的过滤器只起委托作用,执行流程委托给Spring容器中名为shiroFilter的过滤器。所以还需要在Spring配置文件中配置shiroFilter,如下:
-
<!-- Shiro的Web过滤器 -->
-
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
-
<property name="securityManager" ref="securityManager"/>
-
<property name="loginUrl" value="/login.jsp"/>
-
<property name="unauthorizedUrl" value="/unauthorized.jsp"/>
-
<property name="filters">
-
<util:map>
-
<entry key="authc" value-ref="formAuthenticationFilter"/>
-
</util:map>
-
</property>
-
<property name="filterChainDefinitions">
-
<value>
-
/index.jsp = anon
-
/unauthorized.jsp = anon
-
/login.jsp = authc
-
/logout = logout
-
/authenticated.jsp = authc
-
/** = user
-
</value>
-
</property>
-
</bean>
ShiroFilterFactoryBean实现了org.springframework.beans.factory.FactoryBean接口,所以shiroFilter对象是由ShiroFilterFactoryBean的getObject()方法返回的:
-
public Object getObject() throws Exception {
-
if (instance == null) {
-
instance = createInstance();
-
}
-
return instance;
-
}
-
protected AbstractShiroFilter createInstance() throws Exception {
-
log.debug("Creating Shiro Filter instance.");
-
// 获取配置文件中设置的安全管理器
-
SecurityManager securityManager = getSecurityManager();
-
if (securityManager == null) {
-
String msg = "SecurityManager property must be set.";
-
throw new BeanInitializationException(msg);
-
}
-
// 必须是Web环境的安全管理器
-
if (!(securityManager instanceof WebSecurityManager)) {
-
String msg = "The security manager does not implement the WebSecurityManager interface.";
-
throw new BeanInitializationException(msg);
-
}
-
// 创建过滤器链管理器
-
FilterChainManager manager = createFilterChainManager();
-
// 创建基于路径匹配的过滤器链解析器
-
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
-
chainResolver.setFilterChainManager(manager);
-
// 返回SpringShiroFilter对象
-
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
-
}
从上述源码中可以看到,最终返回了一个SpringShiroFilter对象,即Spring配置文件中的shiroFilter对象,该过滤器拥有三个重要对象:SecurityManager、PathMatchingFilterChainResolver、FilterChainManager。
由于在Spring配置中设置了filterChainDefinitions属性,所以会调用setFilterChainDefinitions方法:
-
public void setFilterChainDefinitions(String definitions) {
-
Ini ini = new Ini();
-
ini.load(definitions);
-
//did they explicitly state a 'urls' section? Not necessary, but just in case:
-
Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
-
if (CollectionUtils.isEmpty(section)) {
-
//no urls section. Since this _is_ a urls chain definition property, just assume the
-
//default section contains only the definitions:
-
section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
-
}
-
/** 获取默认section,也就是加载
-
/index.jsp = anon
-
/unauthorized.jsp = anon
-
/login.jsp = authc
-
/logout = logout
-
/authenticated.jsp = authc
-
/** = user
-
这段配置,从这段配置中可以知道哪种URL需要应用上哪些Filter,像anon、authc、logout就是Filter的名称,
-
Ini.Section实现了Map接口,其key为URL匹配符,value为Filter名称
-
**/
-
// 设置filterChainDefinitionMap
-
setFilterChainDefinitionMap(section);
-
}
FilterChainManager用于管理当前Shiro应用的所有Filter,有Shiro默认使用的Filter,也可以是自定义的Filter。下面我们看看FilterChainManager是如何创建出来的:
-
protected FilterChainManager createFilterChainManager() {
-
// 创建DefaultFilterChainManager
-
DefaultFilterChainManager manager = new DefaultFilterChainManager();
-
// 创建Shiro默认Filter,根据org.apache.shiro.web.filter.mgt.DefaultFilter创建
-
Map<String, Filter> defaultFilters = manager.getFilters();
-
//apply global settings if necessary:
-
for (Filter filter : defaultFilters.values()) {
-
// 设置相关Filter的loginUrl、successUrl、unauthorizedUrl属性
-
applyGlobalPropertiesIfNecessary(filter);
-
}
-
// 获取在Spring配置文件中配置的Filter
-
Map<String, Filter> filters = getFilters();
-
if (!CollectionUtils.isEmpty(filters)) {
-
for (Map.Entry<String, Filter> entry : filters.entrySet()) {
-
String name = entry.getKey();
-
Filter filter = entry.getValue();
-
applyGlobalPropertiesIfNecessary(filter);
-
if (filter instanceof Nameable) {
-
((Nameable) filter).setName(name);
-
}
-
// 将配置的Filter添加至链中,如果同名Filter已存在则覆盖默认Filter
-
manager.addFilter(name, filter, false);
-
}
-
}
-
//build up the chains:
-
Map<String, String> chains = getFilterChainDefinitionMap();
-
if (!CollectionUtils.isEmpty(chains)) {
-
for (Map.Entry<String, String> entry : chains.entrySet()) {
-
String url = entry.getKey();
-
String chainDefinition = entry.getValue();
-
// 为配置的每一个URL匹配创建FilterChain定义,
-
// 这样当访问一个URL的时候,一旦该URL配置上则就知道该URL需要应用上哪些Filter
-
// 由于URL配置符会配置多个,所以以第一个匹配上的为准,所以越具体的匹配符应该配置在前面,越宽泛的匹配符配置在后面
-
manager.createChain(url, chainDefinition);
-
}
-
}
-
return manager;
-
}
PathMatchingFilterChainResolver对象职责很简单,就是使用ant路径匹配方法匹配访问的URL,由于pathMatchingFilterChainResolver拥有FilterChainManager对象,所以URL匹配上后可以获取该URL需要应用的FilterChain了。
通过上述分析可以知道,Shiro就是通过一系列的URL匹配符配置URL应该应用上的Filter,然后在Filter中完成相应的任务,所以Shiro的所有功能都是通过Filter完成的。当然认证功能也不例外,在上述配置中认证功能是由org.apache.shiro.web.filter.authc.FormAuthenticationFilter完成的。
下面我们就看看入口过滤器SpringShiroFilter的执行流程,是如何执行到FormAuthenticationFilter的。既然是Filter,那么最重要的就是doFilter方法了,由于SpringShiroFilter继承自OncePerRequestFilter,doFilter方法也是在OncePerRequestFilter中定义的:
-
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
-
throws ServletException, IOException {
-
// 用于保证链中同一类型的Filter只会被执行一次
-
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
-
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
-
log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName());
-
filterChain.doFilter(request, response);
-
} else //noinspection deprecation
-
if (/* added in 1.2: */ !isEnabled(request, response) ||
-
/* retain backwards compatibility: */ shouldNotFilter(request) ) {
-
log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.",
-
getName());
-
filterChain.doFilter(request, response);
-
} else {
-
// Do invoke this filter...
-
log.trace("Filter '{}' not yet executed. Executing now.", getName());
-
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
-
try {
-
// 执行真正的功能代码
-
doFilterInternal(request, response, filterChain);
-
} finally {
-
// Once the request has finished, we're done and we don't
-
// need to mark as 'already filtered' any more.
-
request.removeAttribute(alreadyFilteredAttributeName);
-
}
-
}
-
}
doFilterInternal方法定义AbstractShiroFilter中:
-
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
-
throws ServletException, IOException {
-
Throwable t = null;
-
try {
-
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
-
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
-
// 创建Subject对象,由此可见,每一个请求到来,都会调用createSubject方法
-
final Subject subject = createSubject(request, response);
-
// 通过Subject对象执行过滤器链,
-
subject.execute(new Callable() {
-
public Object call() throws Exception {
-
// 更新会话最后访问时间,用于计算会话超时
-
updateSessionLastAccessTime(request, response);
-
// 执行过滤器链
-
executeChain(request, response, chain);
-
return null;
-
}
-
});
-
} catch (ExecutionException ex) {
-
t = ex.getCause();
-
} catch (Throwable throwable) {
-
t = throwable;
-
}
-
// 省略一些代码...
-
}
先看一下,Subject如果是如何创建的:
-
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
-
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
-
}
跟踪代码最终调用DefaultWebSubjectFactory.createSubject方法:
-
public Subject createSubject(SubjectContext context) {
-
if (!(context instanceof WebSubjectContext)) {
-
return super.createSubject(context);
-
}
-
WebSubjectContext wsc = (WebSubjectContext) context;
-
SecurityManager securityManager = wsc.resolveSecurityManager();
-
Session session = wsc.resolveSession();
-
boolean sessionEnabled = wsc.isSessionCreationEnabled();
-
PrincipalCollection principals = wsc.resolvePrincipals();
-
// 判断是已经认证,如果是在没有登录之前,明显返回是false
-
boolean authenticated = wsc.resolveAuthenticated();
-
String host = wsc.resolveHost();
-
ServletRequest request = wsc.resolveServletRequest();
-
ServletResponse response = wsc.resolveServletResponse();
-
return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,
-
request, response, securityManager);
-
}
接下来看看过滤器链是如何创建与执行的:
-
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
-
throws IOException, ServletException {
-
// 获取当前URL匹配的过滤器链
-
FilterChain chain = getExecutionChain(request, response, origChain);
-
// 执行过滤器链中的过滤器
-
chain.doFilter(request, response);
-
}
-
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
-
FilterChain chain = origChain;
-
// 获取过滤器链解析器,即上面创建的PathMatchingFilterChainResolver对象
-
FilterChainResolver resolver = getFilterChainResolver();
-
if (resolver == null) {
-
log.debug("No FilterChainResolver configured. Returning original FilterChain.");
-
return origChain;
-
}
-
// 调用其getChain方法,根据URL匹配相应的过滤器链
-
FilterChain resolved = resolver.getChain(request, response, origChain);
-
if (resolved != null) {
-
log.trace("Resolved a configured FilterChain for the current request.");
-
chain = resolved;
-
} else {
-
log.trace("No FilterChain configured for the current request. Using the default.");
-
}
-
return chain;
-
}
根据上述Spring配置,假设现在第一次访问URL: "/authenticated.jsp",则会应用上名为authc的Filter,即FormAuthenticationFilter,根据FormAuthenticationFilter的继承体系,先执行dviceFilter.doFilterInternal方法:
-
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
-
throws ServletException, IOException {
-
Exception exception = null;
-
try {
-
// 执行preHandle
-
boolean continueChain = preHandle(request, response);
-
if (log.isTraceEnabled()) {
-
log.trace("Invoked preHandle method. Continuing chain?: [" + continueChain + "]");
-
}
-
// 如果preHandle返回false则过滤器链不再执行
-
if (continueChain) {
-
executeChain(request, response, chain);
-
}
-
postHandle(request, response);
-
if (log.isTraceEnabled()) {
-
log.trace("Successfully invoked postHandle method");
-
}
-
} catch (Exception e) {
-
exception = e;
-
} finally {
-
cleanup(request, response, exception);
-
}
-
}
接下来执行:PathMatchingFilter.preHandle方法:
-
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
-
if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
-
if (log.isTraceEnabled()) {
-
log.trace("appliedPaths property is null or empty. This Filter will passthrough immediately.");
-
}
-
return true;
-
}
-
for (String path : this.appliedPaths.keySet()) {
-
// 根据配置,访问URL:"/authenticated.jsp"时,会匹配上FormAuthenticationFilter,
-
// 而FormAuthenticationFilter继承自PathMatchingFilter,所以返回true
-
if (pathsMatch(path, request)) {
-
log.trace("Current requestURI matches pattern '{}'. Determining filter chain execution...", path);
-
Object config = this.appliedPaths.get(path);
-
// 执行isFilterChainContinued方法,该方法调用onPreHandle方法
-
return isFilterChainContinued(request, response, path, config);
-
}
-
}
-
//no path matched, allow the request to go through:
-
return true;
-
}
接着执行AccessControlFilter.onPreHandle方法:
-
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
-
// 如果isAccessAllowed方法返回false,则会执行onAccessDenied方法
-
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
-
}
接着执行AuthenticatingFilter.isAccessAllowed方法:
-
@Override
-
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
-
return super.isAccessAllowed(request, response, mappedValue) ||
-
(!isLoginRequest(request, response) && isPermissive(mappedValue));
-
}
-
super.isAccessAllowed方法,即AuthenticationFilter.isAccessAllowed方法:
-
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
-
Subject subject = getSubject(request, response);
-
return subject.isAuthenticated();
-
}
由以上代码可知,由于是第一次访问URL:"/authenticated.jsp",所以isAccessAllowed方法返回false,所以接着执行FormAuthenticationFilter.onAccessDenied方法:
-
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
-
// 第一次访问自然不是登录请求
-
if (isLoginRequest(request, response)) {
-
// 判断是否是POST请求
-
if (isLoginSubmission(request, response)) {
-
if (log.isTraceEnabled()) {
-
log.trace("Login submission detected. Attempting to execute login.");
-
}
-
return executeLogin(request, response);
-
} else {
-
if (log.isTraceEnabled()) {
-
log.trace("Login page view.");
-
}
-
//allow them to see the login page ;)
-
return true;
-
}
-
} else {
-
if (log.isTraceEnabled()) {
-
log.trace("Attempting to access a path which requires authentication. Forwarding to the " +
-
"Authentication url [" + getLoginUrl() + "]");
-
}
-
// 所以执行该方法
-
saveRequestAndRedirectToLogin(request, response);
-
return false;
-
}
-
}
-
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
-
// 将request对象保存在session中,以便登录成功后重新转至上次访问的URL
-
saveRequest(request);
-
// 重定向至登录页面,即:"/login.jsp"
-
redirectToLogin(request, response);
-
}
根据配置,访问URL:"/login.jsp"时也会应用上FormAuthenticationFilter,由于是重定向所以发起的是GET请求,所以isLoginSubmission()返回false,所以没有执行executeLogin方法,所以能够访问/login.jsp页面。在登录表单中应该设置action="",这样登录请求会提交至/login.jsp,这时为POST请求,所以会执行executeLogin方法:
-
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
-
// 根据表单填写的用户名密码创建AuthenticationToken
-
AuthenticationToken token = createToken(request, response);
-
if (token == null) {
-
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
-
"must be created in order to execute a login attempt.";
-
throw new IllegalStateException(msg);
-
}
-
try {
-
// 获取Subject对象
-
Subject subject = getSubject(request, response);
-
// 执行Subject.login方法进行登录
-
subject.login(token);
-
// 如果登录成功,重定向至上次访问的URL
-
return onLoginSuccess(token, subject, request, response);
-
} catch (AuthenticationException e) {
-
// 如果登录失败,则设置错误信息至request,并重新返回登录页面
-
return onLoginFailure(token, e, request, response);
-
}
-
}
-
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
-
ServletRequest request, ServletResponse response) throws Exception {
-
// 重定向至上次访问的URL
-
issueSuccessRedirect(request, response);
-
// 由于返回false,所以过滤器链不再执行
-
return false;
-
}
-
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
-
ServletRequest request, ServletResponse response) {
-
// 设置错误信息至request
-
setFailureAttribute(request, e);
-
// 由于返回true,所以过滤器链继续执行,所以又返回了登录页面
-
return true;
-
}
至此,认证流程大致流程就是这样了,限于篇幅,登录的流程具体,请期待下篇博文。