Spring Security3实践总结
注意:第5步的白名单是自定义操作。
?
三、实现
????? 使用Spring Security3的实践过程如下:
?
??1. 增加Spring Security3的MAVEN坐标依赖:
<!-- Spring Security Start --><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>3.1.2.RELEASE</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>3.1.2.RELEASE</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>3.1.2.RELEASE</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>3.1.2.RELEASE</version></dependency><!-- Spring Security End -->
?
?2. 设置项目的web.xml,Spring Security3的过滤器:
<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>
??
3. 增加applicationContext-security.xml,整体配置在附件代码中。
?
? ? 3.1) 验证管理器配置,用户登陆的时候进行的验证,这里采用JDBC的模式,如果不用开启hideUserNotFoundExceptions,则可以采用简单的标签配置。
<!-- 认证管理器.用户登陆认证的入口, 加载用户角色。 --><authentication-manager alias="authenticationManagerBean"> <authentication-provider ref="authenticationProvider"> <!-- <jdbc-user-service data-source-ref="dataSource" users-by-username-query="" authorities-by-username-query="" /> <password-encoder base64="true" hash="sha-256" /> --> </authentication-provider></authentication-manager><b:bean id="authenticationProvider" ref="userDetailsService" /> <b:property name="hideUserNotFoundExceptions" value="false" /> <b:property name="passwordEncoder" ref="passwordEncoder" /></b:bean><b:bean id="userDetailsService" ref="dataSource" /> <b:property name="usersByUsernameQuery" value="SELECT username, password, enable FROM user WHERE username=? AND enable=true;" /> <b:property name="authoritiesByUsernameQuery" value="SELECT u.username username, r.rolename rolename FROM user u, role r, user_role ur WHERE u.username=? AND u.id = ur.user_id AND ur.role_id = r.id;" /></b:bean><!-- 用户的密码加密方式 --><b:bean id="passwordEncoder" value="256" /> <b:property name="encodeHashAsBase64" value="true" /></b:bean>
?
? ? 3.2) 授权流程中安全资源加载器和决策器的配置:
<!-- 访问决策器,决定某个用户(具有的角色)是否有足够的权限去访问某个资源 --><b:bean id="accessDecisionManagerBean" > <!-- 没有显式定义的资源都保护起来。该属性默认值为false --> <b:property name="allowIfAllAbstainDecisions" value="false"/></b:bean><!-- 安全资源定义,即定义某一安全资源可以被哪些角色访问 --><b:bean id="securityMetadataSourceBean" ref="commonDao" /> <b:property name="rejectPublicInvocations" value="true" /></b:bean>
?
? ? 3.3) 指定过滤器,作为验证和授权的自定义处理入口
<!-- 一个自定义的filter,必须包含authenticationManager,accessDecisionManager,securityMetadataSource三个属性 --> <b:bean id="customizedFilter" ref="authenticationManagerBean" /> <b:property name="accessDecisionManager" ref="accessDecisionManagerBean" /> <b:property name="securityMetadataSource" ref="securityMetadataSourceBean" /></b:bean>
?
? ? 3.4) 同时增加异常处理配置,针对验证过程中的异常记性处理
<!-- 认证异常处理 --><b:bean id="exceptionMappingAuthenticationFailureHandler" value="/login.jsp?sign=No User" /> <!-- 凭证错误(密码不正确) --> <b:entry key="org.springframework.security.authentication.BadCredentialsException" value="/login.jsp?sign=Bad Credentials" /> <!-- 用户不可用 --> <b:entry key="org.springframework.security.authentication.DisabledException" value="/login.jsp?sign=User is disabled" /> <!-- 登陆凭证错误 --> <b:entry key="org.springframework.security.core.AuthenticationException" value="/login.jsp?sign=Authentication Failure" /> </b:map> </b:property></b:bean>
?
? ? 3.5) 现在将上述的整体将上述配置整体配置到SpringSecurity3的过滤器链中:
<!-- 当访问被拒绝时,会转到403.jsp --><http access-denied-page="/WEB-INF/error/403.jsp" pattern="/*.htm*"> <!-- 登陆设置 --> <form-login login-page="/login.jsp" username-parameter="loginName" password-parameter="password" login-processing-url="/login.htm" authentication-failure-url="/login.jsp?sign=BadCredentials" default-target-url="/index.htm" always-use-default-target="true" authentication-failure-handler-ref="exceptionMappingAuthenticationFailureHandler" /> <!-- 匿名用户访问控制,这里设置不允许匿名用户登陆 --> <anonymous enabled="false" /> <!-- 登出设置 --> <logout logout-success-url="/login.jsp" logout-url="/logout.htm" /> <http-basic /> <!-- 增加一个filter,位于FILTER_SECURITY_INTERCEPTOR之前 --> <custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="customizedFilter" /></http>
?
? ? 3.6) 同时增加自身项目需要白名单设置:
<!-- 安全资源白名单(URL) --><b:bean id="securityMetadataSourceTrustListHolder" ><b:property name="trustList"><b:list><b:value>/index.htm</b:value><b:value>/hello.htm</b:value></b:list></b:property></b:bean><!-- 安全用户白名单 --><b:bean id="securityUserTrustListHolder" ><b:property name="trustList"><b:list><b:value>administrator</b:value></b:list></b:property></b:bean>
?
? ? 3.7) 开启SpringSecurity3日志:
<!-- 开启Spring Security3认证和授权日志 --><b:bean name="code">/** * 权限拦截器 * * @author Watson Xu * @since 1.0.7 <p>2013-7-10 下午4:12:18</p> */public class CustomizedFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {private FilterInvocationSecurityMetadataSource securityMetadataSource;private static Log logger = LogFactory.getLog(CustomizedFilterSecurityInterceptor.class);// ~ 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 {//@1HttpServletRequest httpRequest = (HttpServletRequest)request;HttpServletResponse httpResponse = (HttpServletResponse)response;String url = httpRequest.getRequestURI().replaceFirst(httpRequest.getContextPath(), "");//1.1)判断登陆状态:如果未登陆则要求登陆if(!SessionUserDetailsUtil.isLogined()) {httpResponse.sendRedirect(httpRequest.getContextPath() + SecurityConstants.LOGIN_URL);logger.info("未登陆用户,From IP:" + SecutiryRequestUtil.getRequestIp(httpRequest) + "访问 :URI" + url);return; }//1.2)过用户白名单:如果为超级管理员,则直接执行if(SecurityUserTrustListHolder.isTrustUser(SessionUserDetailsUtil.getLoginUserName())) {chain.doFilter(request, response);return;}//1.3)过资源(URL)白名单:如果为公共页面,直接执行if(SecurityMetadataSourceTrustListHolder.isTrustSecurityMetadataSource(url)){chain.doFilter(request, response);return;}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 {//@2,进行安全验证InterceptorStatusToken token = null;try {token = super.beforeInvocation(fi);} catch (IllegalArgumentException e) {HttpServletRequest httpRequest = fi.getRequest();HttpServletResponse httpResponse = fi.getResponse();String url = httpRequest.getRequestURI().replaceFirst(httpRequest.getContextPath(), "");logger.info("用户 " + SessionUserDetailsUtil.getLoginUserName() + ",From IP:" + SecutiryRequestUtil.getRequestIp(httpRequest) + "。尝试访问未授权(或者) URI:" + url);httpResponse.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); RequestDispatcher dispatcher = httpRequest.getRequestDispatcher(SecurityConstants.NOT_ACCEPTABLE); dispatcher.forward(httpRequest, httpResponse);return;}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;}@Overridepublic void destroy() {}@Overridepublic void init(FilterConfig arg0) throws ServletException {}}
?
??5. 自定义资源的访问权限的定义加载器
/** * 安全资源(URL)和角色映射关系处理器 * * @author Watson Xu * @since 1.0.7 <p>2013-7-9 下午3:25:09</p> */public class CustomizedInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {private boolean rejectPublicInvocations = false;private CommonDao dao;private static Map<String, Integer> resources = new HashMap<String, Integer>();public CustomizedInvocationSecurityMetadataSource(CommonDao dao) {this.dao = dao;loadSecurityMetadataSource();}// According to a URL, Find out permission configuration of this URL.// 根据URL来查找所有能够访问该资源的角色。public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {// guess object is a URL.//@3String url = ((FilterInvocation)object).getRequestUrl();if(resources.isEmpty()) return null;Integer resourceId = resources.get(url);if(rejectPublicInvocations && resourceId == null) {throw new IllegalArgumentException("Secure object invocation " + object + " was denied as public invocations are not allowed via this interceptor. ");//请求不存在}return getRolesByResouceId(resourceId);}private Collection<ConfigAttribute> getRolesByResouceId(Integer resourceId) {List<String> roles = dao.getRoleByResourceId(resourceId);Collection<ConfigAttribute> atts = null;if(roles != null) {atts = new ArrayList<ConfigAttribute>();for (String role : roles) {atts.add(new SecurityConfig(role));}}return atts;}//加载所有安全资源(URL)private void loadSecurityMetadataSource() {List<Map<String, Object>> resourceDtos = dao.getAllResource();if(resourceDtos != null) {resources.clear();for (Map<String, Object> dto : resourceDtos) {resources.put(dto.get("url").toString(), Integer.parseInt(dto.get("id").toString())); }}}public boolean supports(Class<?> clazz) {return true;}public Collection<ConfigAttribute> getAllConfigAttributes() {return null;}public void setDao(CommonDao dao) {this.dao = dao;}public void setRejectPublicInvocations(boolean rejectPublicInvocations) {this.rejectPublicInvocations = rejectPublicInvocations;}}/** * 安全资源(URL)权限决策器 * * @author Watson Xu * @since 1.0.7 <p>2013-7-10 下午4:08:42</p> */public class CustomizedAccessDecisionManager implements AccessDecisionManager {private boolean allowIfAllAbstainDecisions = false; public boolean isAllowIfAllAbstainDecisions() { return allowIfAllAbstainDecisions; } public void setAllowIfAllAbstainDecisions(boolean allowIfAllAbstainDecisions) { this.allowIfAllAbstainDecisions = allowIfAllAbstainDecisions; } protected final void checkAllowIfAllAbstainDecisions() { if (!this.isAllowIfAllAbstainDecisions()) { throw new AccessDeniedException("Access is denied"); } } //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.//这里的三个参数可以片面的理解为: 用户登录后的凭证(其中包含了用户名和角色的对应关系)、请求的URL、请求的URL对应角色(这些角色可以访问这些URL)public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)throws AccessDeniedException, InsufficientAuthenticationException {//@4if(configAttributes == null){return;}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;}}}checkAllowIfAllAbstainDecisions();}@Overridepublic boolean supports(ConfigAttribute attribute) {return true;}@Overridepublic boolean supports(Class<?> clazz) {return true;}}
?
? 6. 其他工具类代码这不做赘述,具体可以参考附件中的工程代码。
?
参考:
?