Acegi久负盛名,这个家伙是一个spring中广泛使用的认证和安全工具,最初由spring社区爱好者发起,目的是为spring应用提供一个安全服务,比如用户认证及授权等。后来spring官方觉得这个东西很不错,就收编了,并且在2006年发布了spring官方的1.0版本。虽然是基于Acegi,但springsecurity已经在原有基础上增加了很多新的特性进来。为了能够方便一窥Acegi的真容,我们通过一个basic模式来看下Acegi是如何来处理用户认证及授权工作。
1、配置安全所需过滤器org.acegisecurity.util.FilterChainProxy,填充 filterInvocationDefinitionSource 属性如下所示:
PATTERN_TYPE_APACHE_ANT /**=basicProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
注:默认情况下,在filterInvocationDefinitionSource属性中指明使用PATTERN_TYPE_APACHE_ANT,则说明,这里的配置信息是启用Apache Ant路径风格的URL匹配模式,FilterInvocationDefinitionSourceEditor会实例化PathBasedFilterInvocationDefinitionMap实例。如果这里没有指定则采用默认的正是表达式,此时RegExpBasedFilterInvocationDefinitionMap会被实例化。
FilterInvocationDefinitionSourceEditor在进行初始化过程中,acegi源码处理过程的片段代码如下:
if ((s == null) || "".equals(s)) { // Leave target object empty source.setDecorated(new RegExpBasedFilterInvocationDefinitionMap()); } else { // Check if we need to override the default definition map if (s.lastIndexOf(DIRECTIVE_PATTERN_TYPE_APACHE_ANT) != -1) { source.setDecorated(new PathBasedFilterInvocationDefinitionMap()); if (logger.isDebugEnabled()) { logger.debug(("Detected " + DIRECTIVE_PATTERN_TYPE_APACHE_ANT + " directive; using Apache Ant style path expressions")); } } else { source.setDecorated(new RegExpBasedFilterInvocationDefinitionMap()); } if (s.lastIndexOf(DIRECTIVE_CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON) != -1) { if (logger.isDebugEnabled()) { logger.debug("Detected " + DIRECTIVE_CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON + " directive; Instructing mapper to convert URLs to lowercase before comparison"); } source.setConvertUrlToLowercaseBeforeComparison(true); }
另外需要说明的是,PathBasedFilterInvocationDefinitionMap和RegExpBasedFilterInvocationDefinitionMap都是接口FilterInvocationDefinitionSource的实现类。
FilterInvocationDefinitionSourceEditor在初始化athBasedFilterInvocationDefinitionMap和RegExpBasedFilterInvocationDefinitionMap类时,提供了2个常量用:
- DIRECTIVE_CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON:FilterInvocationDefinitionSourceEditor类通过该常量的设值情况判断是否对当前路径进行小写转换
- PATTERN_TYPE_APACHE_ANT:FilterInvocationDefinitionSourceEditor通过这个常量决定具体是采用正则模式还是ant路径风格模式。
我们这里根据执行顺序,指明了3个filter类过滤执行安全策略:basicProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
basicProcessingFilter:在Basic模式下,用户名和密码通过对称加密算法,将用户的登录信息存放在http请求的header信息中。服务器在收到浏览器发送来的验证请求后,将加密过的用户名密码通过Apache提供的commons-codec工具包中的org.apache.commons.codec.binary.Base64进行解码。
例如:发起一次请求验证通过后的http头摘要如下:
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Encoding:gzip, deflate, sdch, br Accept-Language:zh-CN,zh;q=0.8 Authorization:Basic dGVzdDox Connection:keep-alive Cookie:JSESSIONID=A749345B4E56805343189AA5A1223655 Host:localhost:8080 Referer:http://localhost:8080/rest-common-acegi/secure.jsp Upgrade-Insecure-Requests:1 User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
basicProcessingFilter片段代码如下:
String header = httpRequest.getHeader("Authorization"); if (logger.isDebugEnabled()) { logger.debug("Authorization header: " + header); } if ((header != null) && header.startsWith("Basic ")) { String base64Token = header.substring(6); String token = new String(Base64.decodeBase64(base64Token.getBytes())); String username = ""; String password = ""; int delim = token.indexOf(":"); if (delim != -1) { username = token.substring(0, delim); password = token.substring(delim + 1); } if (authenticationIsRequired(username)) { UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); authRequest.setDetails(authenticationDetailsSource.buildDetails((HttpServletRequest) request)); Authentication authResult; try { authResult = authenticationManager.authenticate(authRequest); } catch (AuthenticationException failed) { // Authentication failed if (logger.isDebugEnabled()) { logger.debug("Authentication request for user: " + username + " failed: " + failed.toString()); } SecurityContextHolder.getContext().setAuthentication(null); if (rememberMeServices != null) { rememberMeServices.loginFail(httpRequest, httpResponse); } if (ignoreFailure) { chain.doFilter(request, response); } else { authenticationEntryPoint.commence(request, response, failed); } return; } // Authentication success if (logger.isDebugEnabled()) { logger.debug("Authentication success: " + authResult.toString()); } SecurityContextHolder.getContext().setAuthentication(authResult); if (rememberMeServices != null) { rememberMeServices.loginSuccess(httpRequest, httpResponse, authResult); } } } chain.doFilter(request, response); }
可以看出,basicProcessingFilter从Header中获取Authorization信息,并通过Apache的codec包中的解码工具对token进行解码,从而获取用户输入的用户名、密码信息,用于后面的校验动作。
basicProcessingFilter在获取到验证请求需要用到的用户名及密码信息后,实际的用户有效性验证,交给了org.acegisecurity.providers.ProviderManager来管理的org.acegisecurity.providers.dao.DaoAuthenticationProvider类的执行实际验证处理过程。
对basicProcessingFilter的详细配置如下:
test=111111,ROLE_SUPERVISOR zhangsan=111111,ROLE_SUPERVISOR,disabled
接下来开始配置exceptionTranslationFilter,配置信息如下:
注:ExceptionTranslationFilter类用来处理权限验证失败时页面的路由情况,我们这里给ExceptionTranslationFilter配置了一个默认的basicProcessingFilterEntryPoint
对异常处理的片段代码如下:
public void commence(ServletRequest request, ServletResponse response, AuthenticationException authException) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.addHeader("WWW-Authenticate", "Basic realm=\"" + realmName + "\""); httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); }
从上面的代码可以看到,当权限验证失败后,response请求被指向了HttpServletResponse.SC_UNAUTHORIZED页面(“401”访问受限页面)
在HttpServletResponse中定义的返回取值范围及常量定义如下所示:
public static final int SC_CONTINUE = 100; public static final int SC_SWITCHING_PROTOCOLS = 101; public static final int SC_OK = 200; public static final int SC_CREATED = 201; public static final int SC_ACCEPTED = 202; public static final int SC_NON_AUTHORITATIVE_INFORMATION = 203; public static final int SC_NO_CONTENT = 204; public static final int SC_RESET_CONTENT = 205; public static final int SC_PARTIAL_CONTENT = 206; public static final int SC_MULTIPLE_CHOICES = 300; public static final int SC_MOVED_PERMANENTLY = 301; public static final int SC_MOVED_TEMPORARILY = 302; public static final int SC_FOUND = 302; public static final int SC_SEE_OTHER = 303; public static final int SC_NOT_MODIFIED = 304; public static final int SC_USE_PROXY = 305; public static final int SC_TEMPORARY_REDIRECT = 307; public static final int SC_BAD_REQUEST = 400; public static final int SC_UNAUTHORIZED = 401; public static final int SC_PAYMENT_REQUIRED = 402; public static final int SC_FORBIDDEN = 403; public static final int SC_NOT_FOUND = 404; public static final int SC_METHOD_NOT_ALLOWED = 405; public static final int SC_NOT_ACCEPTABLE = 406; public static final int SC_PROXY_AUTHENTICATION_REQUIRED = 407; public static final int SC_REQUEST_TIMEOUT = 408; public static final int SC_CONFLICT = 409; public static final int SC_GONE = 410; public static final int SC_LENGTH_REQUIRED = 411; public static final int SC_PRECONDITION_FAILED = 412; public static final int SC_REQUEST_ENTITY_TOO_LARGE = 413; public static final int SC_REQUEST_URI_TOO_LONG = 414; public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415; public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416; public static final int SC_EXPECTATION_FAILED = 417; public static final int SC_INTERNAL_SERVER_ERROR = 500; public static final int SC_NOT_IMPLEMENTED = 501; public static final int SC_BAD_GATEWAY = 502; public static final int SC_SERVICE_UNAVAILABLE = 503; public static final int SC_GATEWAY_TIMEOUT = 504; public static final int SC_HTTP_VERSION_NOT_SUPPORTED = 505;
最后配置filterInvocationInterceptor:
注:FilterSecurityInterceptor是filterchain中比较复杂,也是比较核心的过滤器,主要负责授权的工作
spring通过HttpConfigurationBuilder类来为filter构造过滤器实例,代码片段如下:
private void createFilterSecurityInterceptor(BeanReference authManager) { boolean useExpressions = FilterInvocationSecurityMetadataSourceParser .isUseExpressions(httpElt); RootBeanDefinition securityMds = FilterInvocationSecurityMetadataSourceParser .createSecurityMetadataSource(interceptUrls, addAllAuth, httpElt, pc); RootBeanDefinition accessDecisionMgr; ManagedListvoters = new ManagedList (2); if (useExpressions) { //表达式模式,这里省略,不是本例重点 } else { voters.add(GrantedAuthorityDefaultsParserUtils.registerWithDefaultRolePrefix(pc, RoleVoterBeanFactory.class)); voters.add(new RootBeanDefinition(AuthenticatedVoter.class)); } accessDecisionMgr = new RootBeanDefinition(AffirmativeBased.class); accessDecisionMgr.getConstructorArgumentValues().addGenericArgumentValue(voters); accessDecisionMgr.setSource(pc.extractSource(httpElt)); // Set up the access manager reference for http String accessManagerId = httpElt.getAttribute(ATT_ACCESS_MGR); if (!StringUtils.hasText(accessManagerId)) { accessManagerId = pc.getReaderContext().generateBeanName(accessDecisionMgr); pc.registerBeanComponent(new BeanComponentDefinition(accessDecisionMgr, accessManagerId)); } BeanDefinitionBuilder builder = BeanDefinitionBuilder .rootBeanDefinition(FilterSecurityInterceptor.class); builder.addPropertyReference("accessDecisionManager", accessManagerId); builder.addPropertyValue("authenticationManager", authManager); if ("false".equals(httpElt.getAttribute(ATT_ONCE_PER_REQUEST))) { builder.addPropertyValue("observeOncePerRequest", Boolean.FALSE); } builder.addPropertyValue("securityMetadataSource", securityMds); BeanDefinition fsiBean = builder.getBeanDefinition(); String fsiId = pc.getReaderContext().generateBeanName(fsiBean); pc.registerBeanComponent(new BeanComponentDefinition(fsiBean, fsiId)); // Create and register a DefaultWebInvocationPrivilegeEvaluator for use with // taglibs etc. BeanDefinition wipe = new RootBeanDefinition( DefaultWebInvocationPrivilegeEvaluator.class); wipe.getConstructorArgumentValues().addGenericArgumentValue( new RuntimeBeanReference(fsiId)); pc.registerBeanComponent(new BeanComponentDefinition(wipe, pc.getReaderContext() .generateBeanName(wipe))); this.fsi = new RuntimeBeanReference(fsiId); }
从上面的代码片段可以看出,在FilterInvocationSecurityMetadataSourceParser类中定义了一个静态方法用于处理鉴权元数据,代码片段如下:
static RootBeanDefinition createSecurityMetadataSource(ListinterceptUrls, boolean addAllAuth, Element httpElt, ParserContext pc) { MatcherType matcherType = MatcherType.fromElement(httpElt); boolean useExpressions = isUseExpressions(httpElt); ManagedMap requestToAttributesMap = parseInterceptUrlsForFilterInvocationRequestMap( matcherType, interceptUrls, useExpressions, addAllAuth, pc); BeanDefinitionBuilder fidsBuilder; if (useExpressions) { Element expressionHandlerElt = DomUtils.getChildElementByTagName(httpElt, Elements.EXPRESSION_HANDLER); String expressionHandlerRef = expressionHandlerElt == null ? null : expressionHandlerElt.getAttribute("ref"); if (StringUtils.hasText(expressionHandlerRef)) { logger.info("Using bean '" + expressionHandlerRef + "' as web SecurityExpressionHandler implementation"); } else { expressionHandlerRef = registerDefaultExpressionHandler(pc); } fidsBuilder = BeanDefinitionBuilder .rootBeanDefinition(ExpressionBasedFilterInvocationSecurityMetadataSource.class); fidsBuilder.addConstructorArgValue(requestToAttributesMap); fidsBuilder.addConstructorArgReference(expressionHandlerRef); } else { fidsBuilder = BeanDefinitionBuilder .rootBeanDefinition(DefaultFilterInvocationSecurityMetadataSource.class); fidsBuilder.addConstructorArgValue(requestToAttributesMap); } fidsBuilder.getRawBeanDefinition().setSource(pc.extractSource(httpElt)); return (RootBeanDefinition) fidsBuilder.getBeanDefinition(); }
完整的spring-acegi.xml配置如下所示(完整路径:src/main/resources/META-INF/spring/spring-acegi.xml):
PATTERN_TYPE_APACHE_ANT /**=basicProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor test=1,ROLE_SUPERVISOR zhangsan=1,ROLE_SUPERVISOR,disabled
对应的web.xml文件配置信息如下所示:
Archetype Created Web Application contextConfigLocation classpath:META-INF/spring/spring-acegi.xml AcegiFilterChainProxy org.acegisecurity.util.FilterToBeanProxy targetBean filterChainProxy AcegiFilterChainProxy /j_acegi_security_check AcegiFilterChainProxy /j_acegi_logout AcegiFilterChainProxy *.do AcegiFilterChainProxy *.jsp index.jsp org.springframework.web.context.ContextLoaderListener
不失完整性,用于构建工程用到的指令如下:
mvn archetype:generate -DgroupId=com.myteay -DartifactId=rest-common-acegi -Dversion=1.0.0 -Dpackage=com.myteay -DarchetypeArtifactId=maven-archetype-webapp -DarchetypeGroupId=org.apache.maven.archetypes -DinteractiveMode=false
方便使用起见,贴出工程用到的完整pom
4.0.0 com.myteay rest-common-acegi war 1.0.0 rest-common-acegi Maven Webapp http://maven.apache.org org.springframework.security spring-security-core 4.2.2.RELEASE org.springframework.security spring-security-web 4.2.2.RELEASE org.springframework.security spring-security-config 4.2.2.RELEASE runtime org.apache.geronimo.specs geronimo-servlet_2.4_spec 1.1.1 provided org.springframework spring-aop 4.2.2.RELEASE org.springframework spring-beans 4.2.2.RELEASE org.springframework spring-context 4.2.2.RELEASE org.springframework spring-context-support 4.2.2.RELEASE org.springframework spring-core 4.2.2.RELEASE org.springframework spring-expression 4.2.2.RELEASE org.springframework spring-jdbc 4.2.2.RELEASE org.springframework spring-oxm 4.2.2.RELEASE org.springframework spring-tx 4.2.2.RELEASE org.springframework spring-web 4.2.2.RELEASE org.springframework spring-webmvc 4.2.2.RELEASE org.springframework spring-orm 4.2.2.RELEASE org.springframework spring-test 4.2.2.RELEASE jmock jmock 1.2.0 jmock jmock-cglib 1.2.0 junit junit 3.8.1 test org.acegisecurity acegi-security 1.0.7 rest-common-acegi org.mortbay.jetty maven-jetty-plugin 6.1.26 3 80 src/main/webapp/WEB-INF **/*.jsp **/*.properties **/*.xml