本文共 11782 字,大约阅读时间需要 39 分钟。
本Blog所有内容不得随意转载,版权属于作者所有。如需转载请与作者联系( QQ:9184314)。 未经许可的转载,本人保留一切法律权益。 Spring Security 3.x 出来一段时间了,跟Acegi是大不同了,与2.x的版本也有一些小小的区别,网上有一些文档,也有人翻译Spring Security 3.x的guide,但通过阅读guide,无法马上就能很容易的实现一个完整的实例。 我花了点儿时间,根据以前的实战经验,整理了一份完整的入门教程,供需要的朋友们参考。 1,建一个web project,并导入所有需要的lib,这步就不多讲了。 2,配置web.xml,使用Spring的机制装载: <? xml version="1.0" encoding="UTF-8" ?> < web-app version ="2.4" xmlns ="http://java.sun.com/xml/ns/j2ee" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" > < context-param > < param-name > contextConfigLocation </ param-name > < param-value > classpath:applicationContext*.xml </ param-value > </ context-param > < listener > < listener-class > org.springframework.web.context.ContextLoaderListener </ listener-class > </ listener > < filter > < filter-name > springSecurityFilterChain </ filter-name > < filter-class > org.springframework.web.filter.DelegatingFilterProxy </ filter-class > </ filter > < filter-mapping > < filter-name > springSecurityFilterChain </ filter-name > < url-pattern > /* </ url-pattern > </ filter-mapping > < welcome-file-list > < welcome-file > login.jsp </ welcome-file > </ welcome-file-list > </ web-app > 这个文件中的内容我相信大家都很熟悉了,不再多说了。 2,来看看applicationContext-security.xml这个配置文件,关于Spring Security的配置均在其中: <? xml version="1.0" encoding="UTF-8" ?> < beans:beans xmlns ="http://www.springframework.org/schema/security" xmlns:beans ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd" > < http access-denied-page ="/403.jsp" > <!-- 当访问被拒绝时,会转到403.jsp --> < intercept-url pattern ="/login.jsp" filters ="none" /> < form-login login-page ="/login.jsp" authentication-failure-url ="/login.jsp?error=true" default-target-url ="/index.jsp" /> < logout logout-success-url ="/login.jsp" /> < http-basic /> <!-- 增加一个filter,这点与Acegi是不一样的,不能修改默认的filter了,这个filter位于FILTER_SECURITY_INTERCEPTOR之前 --> < custom-filter before ="FILTER_SECURITY_INTERCEPTOR" ref ="myFilter" /> </ http > <!-- 一个自定义的filter,必须包含authenticationManager,accessDecisionManager,securityMetadataSource三个属性, 我们的所有控制将在这三个类中实现,解释详见具体配置 --> < beans:bean id ="myFilter" class ="com.robin.erp.fwk.security.MyFilterSecurityInterceptor" > < beans:property name ="authenticationManager" ref ="authenticationManager" /> < beans:property name ="accessDecisionManager" ref ="myAccessDecisionManagerBean" /> < beans:property name ="securityMetadataSource" ref ="securityMetadataSource" /> </ beans:bean > <!-- 认证管理器,实现用户认证的入口,主要实现UserDetailsService接口即可 --> < authentication-manager alias ="authenticationManager" > < authentication-provider user-service-ref ="myUserDetailService" > <!-- 如果用户的密码采用加密的话,可以加点“盐” <password-encoder hash="md5" /> --> </ authentication-provider > </ authentication-manager > < beans:bean id ="myUserDetailService" class ="com.robin.erp.fwk.security.MyUserDetailService" /> <!-- 访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源 --> < beans:bean id ="myAccessDecisionManagerBean" class ="com.robin.erp.fwk.security.MyAccessDecisionManager" > </ beans:bean > <!-- 资源源数据定义,即定义某一资源可以被哪些角色访问 --> < beans:bean id ="securityMetadataSource" class ="com.robin.erp.fwk.security.MyInvocationSecurityMetadataSource" /> </ beans:beans > 3,来看看自定义filter的实现: package com.robin.erp.fwk.security; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import org.springframework.security.access.SecurityMetadataSource; import org.springframework.security.access.intercept.AbstractSecurityInterceptor; import org.springframework.security.access.intercept.InterceptorStatusToken; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { private FilterInvocationSecurityMetadataSource securityMetadataSource; // ~ Methods // ======================================================================================================== /** * Method that is actually called by the filter chain. Simply delegates to * the { @link #invoke(FilterInvocation)} method. * * @param request * the servlet request * @param response * the servlet response * @param chain * the filter chain * * @throws IOException * if the filter chain fails * @throws ServletException * if the filter chain fails */ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() { return this.securityMetadataSource; } public Class<? extends Object> getSecureObjectClass() { return FilterInvocation.class; } public void invoke(FilterInvocation fi) throws IOException, ServletException { InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.afterInvocation(token, null); } } public SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource; } public void setSecurityMetadataSource( FilterInvocationSecurityMetadataSource newSource) { this.securityMetadataSource = newSource; } @Override public void destroy() { } @Override public void init(FilterConfig arg0) throws ServletException { } } 最核心的代码就是invoke方法中的InterceptorStatusToken token = super.beforeInvocation(fi);这一句,即在执行doFilter之前,进行权限的检查,而具体的实现已经交给accessDecisionManager了,下文中会讲述。 4,来看看authentication-provider的实现: package com.robin.erp.fwk.security; import java.util.ArrayList; import java.util.Collection; import org.springframework.dao.DataAccessException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.GrantedAuthorityImpl; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; public class MyUserDetailService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { Collection<GrantedAuthority> auths=new ArrayList<GrantedAuthority>(); GrantedAuthorityImpl auth2=new GrantedAuthorityImpl("ROLE_ADMIN"); auths.add(auth2); if(username.equals("robin1")){ auths=new ArrayList<GrantedAuthority>(); GrantedAuthorityImpl auth1=new GrantedAuthorityImpl("ROLE_ROBIN"); auths.add(auth1); } // User(String username, String password, boolean enabled, boolean accountNonExpired, // boolean credentialsNonExpired, boolean accountNonLocked, Collection<GrantedAuthority> authorities) { User user = new User(username, "robin", true, true, true, true, auths); return user; } } 在这个类中,你就可以从数据库中读入用户的密码,角色信息,是否锁定,账号是否过期等,我想这么简单的代码就不再多解释了。 5,对于资源的访问权限的定义,我们通过实现FilterInvocationSecurityMetadataSource这个接口来初始化数据。 package com.robin.erp.fwk.security; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.security.web.util.AntUrlPathMatcher; import org.springframework.security.web.util.UrlMatcher; /** * * 此类在初始化时,应该取到所有资源及其对应角色的定义 * * @author Robin * */ public class MyInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private UrlMatcher urlMatcher = new AntUrlPathMatcher();; private static Map<String, Collection<ConfigAttribute>> resourceMap = null; public MyInvocationSecurityMetadataSource() { loadResourceDefine(); } private void loadResourceDefine() { resourceMap = new HashMap<String, Collection<ConfigAttribute>>(); Collection<ConfigAttribute> atts = new ArrayList<ConfigAttribute>(); ConfigAttribute ca = new SecurityConfig("ROLE_ADMIN"); atts.add(ca); resourceMap.put("/index.jsp", atts); resourceMap.put("/i.jsp", atts); } // According to a URL, Find out permission configuration of this URL. public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { // guess object is a URL. String url = ((FilterInvocation)object).getRequestUrl(); Iterator<String> ite = resourceMap.keySet().iterator(); while (ite.hasNext()) { String resURL = ite.next(); if (urlMatcher.pathMatchesUrl(resURL, url)) { return resourceMap.get(resURL); } } return null; } public boolean supports(Class<?> clazz) { return true; } public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } } 看看loadResourceDefine方法,我在这里,假定index.jsp和i.jsp这两个资源,需要ROLE_ADMIN角色的用户才能访问。 这个类中,还有一个最核心的地方,就是提供某个资源对应的权限定义,即getAttributes方法返回的结果。注意,我例子中使用的是AntUrlPathMatcher这个path matcher来检查URL是否与资源定义匹配,事实上你还要用正则的方式来匹配,或者自己实现一个matcher。 6,剩下的就是最终的决策了,make a decision,其实也很容易,呵呵。 package com.robin.erp.fwk.security; import java.util.Collection; import java.util.Iterator; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; public class MyAccessDecisionManager implements AccessDecisionManager { //In this method, need to compare authentication with configAttributes. // 1, A object is a URL, a filter was find permission configuration by this URL, and pass to here. // 2, Check authentication has attribute in permission configuration (configAttributes) // 3, If not match corresponding authentication, throw a AccessDeniedException. public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { if(configAttributes == null){ return ; } System.out.println(object.toString()); //object is a URL. Iterator<ConfigAttribute> ite=configAttributes.iterator(); while(ite.hasNext()){ ConfigAttribute ca=ite.next(); String needRole=((SecurityConfig)ca).getAttribute(); for(GrantedAuthority ga:authentication.getAuthorities()){ if(needRole.equals(ga.getAuthority())){ //ga is user's role. return; } } } throw new AccessDeniedException("no right"); } @Override public boolean supports(ConfigAttribute attribute) { // TODO Auto-generated method stub return true; } @Override public boolean supports(Class<?> clazz) { return true; } } 在这个类中,最重要的是decide方法,如果不存在对该资源的定义,直接放行;否则,如果找到正确的角色,即认为拥有权限,并放行,否则throw new AccessDeniedException("no right");这样,就会进入上面提到的403.jsp页面。 参考资料: 1,Spring官方网站: 2,文章所用的代码,MyEclipse工程,去掉了lib,请自行下载Spring Security 3.x的包,并copy至对应目录。 3,根据网络上的资料,制作的CHM版的 4,2009年3月,我在“IBM WebSphere技术专家沙龙(华南区广州站)”演讲时的PPT: ,当时是Spring Security 2.x,很多原理是一样,可作参考。 教程中为了尽可能不跟其它框架关联上,所以去掉了访问数据库的部分,比如用户信息和资源配置信息的读取,直接写死在代码中了,大家可以根据自己的实际情况补充完整。 如有任何疑问,欢迎大家以评论的方式提问,也欢迎大家讨论!