Apache shiro 作为一个优秀的权限框架,其中最重要的两项工作,其一是认证,解决用户登陆的认证问题,其二是权限控制,看登陆用户有什么样的权限
首先在web.xml 里面配置过滤器 /*表示拦截所有url请求
shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
true
targetFilterLifecycle
true
shiroFilter
/*
由于spring 的DelegatingFilterProxy 只是起一个委托的作用,执行的流程委托给spring中名为shiroFilter的过滤器,所以需要配置这个过滤器
里面有详细解释
//过滤器的执行链
anon表示不进行权限控制
/index.jsp = anon
/unauthorized.jsp = anon
/login.jsp = authc
/logout = logout
/authenticated.jsp = authc
/** = user
ShiroFactoryBean实现了spring的factorybean的接口,所以shirofilter这个对象是通过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();
//Expose the constructed FilterChainManager by first wrapping it in a
// FilterChainResolver implementation. The AbstractShiroFilter implementations
// 创建给予路径匹配的过滤器链的解析器
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
//返回spring shirofilter这个对象
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
我们最终返回了一个springshirofilter对象
如果在前面设置了filterChainDefinitions属性,会调用set方法进行注入,可以手动写入,也可以通过导入配置文件来传参
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的名称,之后可以通过名称动态的添加filter进行拼接
Ini.Section实现了Map接口,其key为URL匹配符,value为Filter名称
**/
// 设置filterChainDefinitionMap
setFilterChainDefinitionMap(section);
}
通过配置文件的方式传入chailn
FilterChainManager是管理当前的shiro的所有filter,有shiro默认使用的,也有自己定义的filter,比如anon等等,首先分析源码查看FilterChainManager是如何创建的
protected FilterChainManager createFilterChainManager() {
// 创建DefaultFilterChainManager
DefaultFilterChainManager manager = new DefaultFilterChainManager();
// 创建Shiro默认Filter,根据org.apache.shiro.web.filter.mgt.DefaultFilter创建
Map defaultFilters = manager.getFilters();
//apply global settings if necessary:
for (Filter filter : defaultFilters.values()) {
// 设置相关Filter的loginUrl、successUrl、unauthorizedUrl属性
applyGlobalPropertiesIfNecessary(filter);
}
// 获取在Spring配置文件中配置的Filter
Map filters = getFilters();
if (!CollectionUtils.isEmpty(filters)) {
for (Map.Entry 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 chains = getFilterChainDefinitionMap();
if (!CollectionUtils.isEmpty(chains)) {
for (Map.Entry 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了。比如ano随影的所有url匹配ano配置的所有filter
通过上述分析可以知道,Shiro就是通过一系列的URL匹配符配置URL应该应用上的Filter,然后在Filter中完成相应的任务,所以Shiro的所有功能都是通过Filter完成的。当然认证功能也不例外,在上述配置中认证功能是由com.enation.app.base.security AuthFilter完成的。
package com.enation.app.base.security;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.filter.authc.UserFilter;
import org.springframework.beans.factory.annotation.Autowired;
import com.enation.app.base.core.model.AuthAction;
import com.enation.eop.processor.core.HttpHeaderConstants;
import com.enation.eop.resource.IMenuManager;
import com.enation.eop.resource.model.AdminUser;
import com.enation.eop.resource.model.Menu;
import com.enation.eop.sdk.context.EopSetting;
import com.enation.eop.sdk.context.UserConext;
import com.enation.framework.context.webcontext.ThreadContextHolder;
/**
* 自定义权限拦截器:拦截菜单表中定义的所有url并且没有授权给当前用户的
*
* @author tito
*
*/
public class AuthFilter extends UserFilter {
public static final String CURRENT_ADMINUSER_MENU_KEY = "CURRENT_ADMINUSER_MENU_KEY";
@Autowired
private IMenuManager menuManager;
@Override
public boolean isAccessAllowed(ServletRequest request,
ServletResponse response, Object mappedValue) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// String[] mapArr = (String[]) mappedValue;
// if (mapArr == null || mapArr.length == 0) {
// return true;
// }
if ("yes".equals(EopSetting.INSTALL_LOCK)) {
AdminUser user = UserConext.getCurrentAdminUser();
if (user != null && user.getFounder() == 1) {
return true;
}
List
下面我们就看看入口过滤器SpringShiroFilter的执行流程,是如何执行到AuthFilter的。既然是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;
}
if (t != null) {
if (t instanceof ServletException) {
throw (ServletException) t;
}
if (t instanceof IOException) {
throw (IOException) t;
}
//otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
String msg = "Filtered request failed.";
throw new ServletException(msg, t);
}
}
首先进入subject的创建方法
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}
DefaultSecurityManager类
public Subject createSubject(SubjectContext subjectContext) {
//create a copy so we don't modify the argument's backing map:
SubjectContext context = copy(subjectContext);
//ensure that the context has a SecurityManager instance, and if not, add one:
context = ensureSecurityManager(context);
//Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
//sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the
//process is often environment specific - better to shield the SF from these details:
context = resolveSession(context);
//Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
//if possible before handing off to the SubjectFactory:
context = resolvePrincipals(context);
Subject subject = doCreateSubject(context);
//save this subject for future reference if necessary:
//(this is needed here in case rememberMe principals were resolved and they need to be stored in the
//session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
//Added in 1.2:
save(subject);
return subject;
}
最后到DefaultWebSubjectFactory类
protected Subject doCreateSubject(SubjectContext context) {
return getSubjectFactory().createSubject(context);
}
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);
}
创建完subject之后,之后在上面的代码中会执行过滤器的链
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的继承体系,先执行AdviceFilter.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方法:
appliedPath中存入的是配置文件中所有的配置
key是路径,value是filter的类型,所以request和path的匹配程度,若匹配,则返回
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类
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
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();
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 第一次访问自然不是登录请求
if (isLoginRequest(request, response)) {
// 判断是否是POST请求
/*
代码如下
@SuppressWarnings({"UnusedDeclaration"})
protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) {
return (request instanceof HttpServletRequest) && WebUtils.toHttp(request).getMethod().equalsIgnoreCase(POST_METHOD);
}
*/
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;
}
}
如果是第一次访问,那么肯定返回false
FormAuthenticationFilter的onAccessDenied方法
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;
}
至此认证流程走通