在线项目最近要对管理系统进行细粒度的权限控制,细化到URL级别。Spring Security3在这个时候引入到了系统总来。Spring Security3的学习曲线并不是非常的平坦。现在将使用场景和使用方法总结如下。
一、需求
做项目肯定要从项目背景和需求谈起。这个在线项目的背景和需求如下:
- 该项目为一个对外网开发的管理系统,系统功能丰富,需要将系统的功能进行切分,不同用户拥有不同的权限。该系统为JAVA开发,使用了Spring3。
- 权限的粒度细化到最小功能级别。
- 根据用户权限展现菜单。
- 根据用户权限显示每个页面的功能按钮等。
- 用户必须登录才能操作。
- 系统有超级管理员,其拥有最大的权限,可以操作任何功能。
- 系统存在公共页面,登录用户均可查看和使用。
二、方案
有了需求肯定就要有解决方案,针对上面的背景和需求。我们的解决方案和办法如下:
- 既然已经用了Spring3,那就直接上Spring Security3,这里为了保证和系统版本统一,使用3.1.2.RELEASE。
- URL请求就是最小的用需要的最小权限粒度对应物,所以将URL作为Spring Security中的资源。
- 根据用户权限情况加载用户菜单树。
- 获取登陆用户的资源权限,来展现页面功能按钮。SpringSecurity有标签可以控制资源的显示,由于本文涉及的项目中需要向下兼容,而且存在白名单,所以做了自定义标签处理。
- 增加用户操作拦截,判定用户登陆状态。
- 增加用户白名单来实现超级管理员操作权限最大化,实质是登陆的超级管理员用户不需要过Spring Security的资源授权功能。
- 增加公共资源白名单,使任何登陆用户均可访问公共页面,实质是在用户已经登陆的情况下公共资源不需要过Spring Security的资源授权功能。
Spring Security3对(用户登录)验证和(安全资源)授权的基本流程如下图:
注意:第5步的白名单是自定义操作。
三、实现
使用Spring Security3的实践过程如下:
1. 增加Spring Security3的MAVEN坐标依赖:
- <dependency>
- <groupId>org.springframework.securitygroupId>
- <artifactId>spring-security-webartifactId>
- <version>3.1.2.RELEASEversion>
- dependency>
- <dependency>
- <groupId>org.springframework.securitygroupId>
- <artifactId>spring-security-taglibsartifactId>
- <version>3.1.2.RELEASEversion>
- dependency>
- <dependency>
- <groupId>org.springframework.securitygroupId>
- <artifactId>spring-security-coreartifactId>
- <version>3.1.2.RELEASEversion>
- dependency>
- <dependency>
- <groupId>org.springframework.securitygroupId>
- <artifactId>spring-security-configartifactId>
- <version>3.1.2.RELEASEversion>
- dependency>
2. 设置项目的web.xml,Spring Security3的过滤器:
- <filter>
- <filter-name>springSecurityFilterChainfilter-name>
- <filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
- filter>
- <filter-mapping>
- <filter-name>springSecurityFilterChainfilter-name>
- <url-pattern>/*url-pattern>
- filter-mapping>
3. 增加applicationContext-security.xml,整体配置在附件代码中。
3.1) 验证管理器配置,用户登陆的时候进行的验证,这里采用JDBC的模式,如果不用开启hideUserNotFoundExceptions,则可以采用简单的标签配置。
- <authentication-manager alias="authenticationManagerBean">
- <authentication-provider ref="authenticationProvider">
- <b:bean id="passwordEncoder"
- class="org.springframework.security.authentication.encoding.ShaPasswordEncoder">
- <b:constructor-arg index="0" value="256" />
- <b:property name="encodeHashAsBase64" value="true" />
- b:bean>
3.2) 授权流程中安全资源加载器和决策器的配置:
- <b:bean id="accessDecisionManagerBean"
- class="demo.security.CustomizedAccessDecisionManager" >
- <b:property name="allowIfAllAbstainDecisions" value="false"/>
- b:bean>
- <b:bean id="securityMetadataSourceBean"
- class="demo.security.CustomizedInvocationSecurityMetadataSource">
- <b:constructor-arg index="0" ref="commonDao" />
- <b:property name="rejectPublicInvocations" value="true" />
- b:bean>
3.3) 指定过滤器,作为验证和授权的自定义处理入口
- <b:bean id="customizedFilter" class="demo.security.CustomizedFilterSecurityInterceptor">
- <b:property name="authenticationManager" ref="authenticationManagerBean" />
- <b:property name="accessDecisionManager" ref="accessDecisionManagerBean" />
- <b:property name="securityMetadataSource" ref="securityMetadataSourceBean" />
- b:bean>
3.4) 同时增加异常处理配置,针对验证过程中的异常记性处理
- <b:bean id="exceptionMappingAuthenticationFailureHandler"
- class="org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler">
- <b:property name="exceptionMappings">
- <b:map>
- <b:entry key="org.springframework.security.core.userdetails.UsernameNotFoundException"
- 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的过滤器链中:
- <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 />
- <custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="customizedFilter" />
- http>
3.6) 同时增加自身项目需要白名单设置:
- <b:bean id="securityMetadataSourceTrustListHolder"
- class="demo.security.util.SecurityMetadataSourceTrustListHolder" >
- <b:property name="trustList">
- <b:list>
- <b:value>/index.htmb:value>
- <b:value>/hello.htmb:value>
- b:list>
- b:property>
- b:bean>
- <b:bean id="securityUserTrustListHolder"
- class="demo.security.util.SecurityUserTrustListHolder" >
- <b:property name="trustList">
- <b:list>
- <b:value>administratorb:value>
- b:list>
- b:property>
- b:bean>
3.7) 开启SpringSecurity3日志:
- <b:bean class="org.springframework.security.authentication.event.LoggerListener"/>
- <b:bean class="org.springframework.security.access.event.LoggerListener"/>
4. 自定义拦截器代码。最核心的代码就是invoke方法中的InterceptorStatusToken token = super.beforeInvocation(fi);这一句,即在执行doFilter之前,进行权限的检查,而具体的实现已经交给accessDecisionManager了。
- /**
- * 权限拦截器
- *
- * @author Watson Xu
- * @since 1.0.7
2013-7-10 下午4:12:18
- */
- 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 {
- //@1
- HttpServletRequest 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;
- }
- @Override
- public void destroy() {
- }
- @Override
- public void init(FilterConfig arg0) throws ServletException {
- }
- }
5. 自定义资源的访问权限的定义加载器
- /**
- * 安全资源(URL)和角色映射关系处理器
- *
- * @author Watson Xu
- * @since 1.0.7
2013-7-9 下午3:25:09
- */
- public class CustomizedInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
- private boolean rejectPublicInvocations = false;
- private CommonDao dao;
- private static Map
resources = new HashMap (); - public CustomizedInvocationSecurityMetadataSource(CommonDao dao) {
- this.dao = dao;
- loadSecurityMetadataSource();
- }
- // According to a URL, Find out permission configuration of this URL.
- // 根据URL来查找所有能够访问该资源的角色。
- public Collection
getAttributes(Object object) throws IllegalArgumentException { - // guess object is a URL.
- //@3
- String 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
getRolesByResouceId(Integer resourceId) { - List
roles = dao.getRoleByResourceId(resourceId); - Collection
atts = null; - if(roles != null) {
- atts = new ArrayList
(); - for (String role : roles) {
- atts.add(new SecurityConfig(role));
- }
- }
- return atts;
- }
- //加载所有安全资源(URL)
- private void loadSecurityMetadataSource() {
- List
- if(resourceDtos != null) {
- resources.clear();
- for (Map
dto : resourceDtos) { - resources.put(dto.get("url").toString(), Integer.parseInt(dto.get("id").toString()));
- }
- }
- }
- public boolean supports(Class> clazz) {
- return true;
- }
- public Collection
getAllConfigAttributes() { - return null;
- }
- public void setDao(CommonDao dao) {
- this.dao = dao;
- }
- public void setRejectPublicInvocations(boolean rejectPublicInvocations) {
- this.rejectPublicInvocations = rejectPublicInvocations;
- }
- }