SpringSecurity3.2.5自定义角色及权限的教程

最近阴差阳错的搞上了SpringSecurity3,因为是自己做的小系统,中间遇到了很多坑,基本每个坑都踩过了,网上也查了不少资料,发现有不少错误的,更是让我绕了一圈又一圈,现在把一些基本的东西总结一下。

先从整体上总结一下为什么使用SS,一般的,在不使用ss的情况下,我们基本会在每个业务方法执行前,插入一段用于验证权限的代码,从而判断当前用户是否有相应权限进行操作,这样做就会让业务方法和验证权限有了一个紧密的耦合;如果使用ss,我们就可以通过注解或者XML配置方式代替权限验证,使得业务和权限代码彻底分离,通过下图可以更形象的理解:


目前,权限管理采用最多的技术都是基于角色访问控制技术RBAC(Role Based Access Control)。一般来说,提供如下功能:1,角色管理界面,由用户定义角色,给角色赋权限;2,用户角色管理界面,由用户给系统用户赋予角色。什么是RBAC,说到底其实就是五张表,权限表-权限角色对应表-角色表-角色用户对应表-用户表,比较常见。但是ss3默认支持的并不是这种模式,而是通过XML配置角色及用户的方式实现的权限验证等操作,所以需要我们去实现SS中一些接口,让其支持RBAC,下面开始搭建一套支持RBAC技术的SS框架:

(1)数据库相关表格:

1.用户表Users

    CREATE TABLE `users` (

       `password` varchar(255) default NULL,
       `username` varchar(255) default NULL,
       `uid` int(11) NOT NULL auto_increment,
       PRIMARY KEY  (`uid`)
    )

 

   2.角色表Roles

   CREATE TABLE `roles` (
     `rolename` varchar(255) default NULL,
     `rid` int(11) NOT NULL auto_increment,
     PRIMARY KEY  (`rid`)
   )

 

   3 用户_角色表users_roles

   CREATE TABLE `users_roles` (

     --用户表的外键
     `uid` int(11) default NULL,

     --角色表的外键
     `rid` int(11) default NULL,
     `urid` int(11) ,
     PRIMARY KEY  (`urid`),
   )

 

   4.资源表resources

   CREATE TABLE `resources` (

     -- 权限所对应的url地址
     `url` varchar(255) default NULL,

     --权限所对应的编码,例201代表发表文章
     `resourcename` varchar(255) default NULL,
     `rsid` int(11) ,
     PRIMARY KEY  (`rsid`)
   )

 

   5.角色_资源表roles_resources

    CREATE TABLE `roles_resources` (
      `rsid` int(11) default NULL,
      `rid` int(11) default NULL,
      `rrid` int(11) NOT NULL ,
      PRIMARY KEY  (`rrid`),
      )


(2)在继续配置前,需要知道ss是如何通过权限验证的,实际上ss通过拦截器,拦截发来的请求,对其进行验证的。而具体验证的方式则是通过我们实现相关接口的方法来进行的。既然是拦截器,web.xml势必是优先配置的。





  Archetype Created Web Application
      
	    
	    springSecurityFilterChain  
	    org.springframework.web.filter.DelegatingFilterProxy  
	    
	    
	    
	    springSecurityFilterChain  
	    /*  
	    
	 
	
	    spring
	    org.springframework.web.servlet.DispatcherServlet
	     
	        contextConfigLocation
	        classpath:spring-mvc.xml
	    
	    1
	
	 
	
	    spring
	    *.do
	
	 
	 
	
	
	    org.springframework.web.context.ContextLoaderListener
	
	
		org.springframework.web.util.Log4jConfigListener
	
	 
	
	
	    contextConfigLocation
	    classpath:applicationContext*.xml,classpath:spring-mybatis.xml
	
	
		encodingFilter
		org.springframework.web.filter.CharacterEncodingFilter
		
			encoding
			UTF-8
		
	
	
	   
      
          
            org.springframework.web.util.IntrospectorCleanupListener  
          
      
      
      
      
          
            org.springframework.security.web.session.HttpSessionEventPublisher   
          
      
  
      
      
        20  
      

接下来是spring security3的一些配置,具体的每一个是什么意思,网上很多资料,这里不赘述了。总之,需要根据自己的需求,进行相应的修改。


      
      
      
   
   
   
   
   
     
        
    
     
   
   
    
        
                   
        
    
       
      
          
          
          
          
          
         
      
    
    
	

到上面的这个配置文件,则是重中之重了,和ss3打交道,主要都是这个文件。简单说一下,我们需要实现一个自己的filter,在配置中就是myFilterSecurityInterceptor,而这个filter中,还需要我们额外注入三个bean,分别是accessDecisionManager、fisMetadataSource以及authenticationManager,这三个属性中除了fisMetadataSource可以自定义名称外,其他两个都在ss3的父类中定义好了,所以此处需要特别注意,在这里掉过坑了。另外这里说一下这三个分别的作用,accessDecisionManager中有decide(Authentication authentication, Object object,Collection configAttributes)方法,该方法用于判断当前用户是否有权限进行操作,参数中authentication包含了当前用户所拥有的权限,configAttributes中包含了进行该步骤需要的权限,对其进行对比就可以判断该用户是否有权限进行操作。

/** 
 * @description  访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源 ;做最终的访问控制决定 

 */ 
public class MyAccessDescisionManager implements AccessDecisionManager{

	@Override
	public void decide(Authentication authentication, Object object,
			Collection configAttributes)
			throws AccessDeniedException, InsufficientAuthenticationException {
		// TODO Auto-generated method stub
		 System.out.println("MyAccessDescisionManager.decide()------------------验证用户是否具有一定的权限--------");  
	        if(configAttributes==null) return;  
	        Iterator it = configAttributes.iterator();  
	        while(it.hasNext()){  
	            String needResource = it.next().getAttribute();  
	            //authentication.getAuthorities()  用户所有的权限  
	            for(GrantedAuthority ga:authentication.getAuthorities()){  
	                if(needResource.equals(ga.getAuthority())){  
	                    return;  
	                }  
	            }  
	        }  
	        throw new AccessDeniedException("--------MyAccessDescisionManager:decide-------权限认证失败!");  
		
	}

	@Override
	public boolean supports(ConfigAttribute attribute) {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public boolean supports(Class clazz) {
		// TODO Auto-generated method stub
		return true;
	}

}

到这里,可以很自然的想到是权限和用户数据从哪里得到的,filterInvocationSecurityMetadataSource在被加载时候,会首先将权限的信息建立起来,这里我用一个map,key为url,value为该权限的名称,这一步是在构造方法中进行的,也就是服务器启动时候完成的。而当用户访问某一个地址时,ss会到该类中调用getAttributes(Object obj)方法,obj中包含了访问的url地址,我们需要做的就是将该url对应的权限名称返回给ss,而ss会将返回的这个对象,其实就是accessDecisionManager的decide方法中的configAttributes对象。

/** 
 * @description  资源源数据定义,将所有的资源和权限对应关系建立起来,即定义某一资源可以被哪些角色访问 
 * @author aokunsang 
 * @date 2012-8-15 
 */  
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {  

    private UserMapper userMapper;  
    public UserMapper getUserMapper() {
		return userMapper;
	}

	public void setUserMapper(UserMapper userMapper) {
		this.userMapper = userMapper;
	}

	/* 保存资源和权限的对应关系  key-资源url  value-权限 */  
    private static Map> resourceMap = null;   
    private AntPathMatcher urlMatcher = new AntPathMatcher();  
      
    public MySecurityMetadataSource(UserMapper userMapper) {  
        this.userMapper = userMapper;  
        loadResourcesDefine();  
    }  
      
    @Override  
    public Collection getAllConfigAttributes() {  
        return null;  
    }  
  
    private void loadResourcesDefine(){  
        resourceMap = new HashMap>();  
          
        System.out.println("MySecurityMetadataSource.loadResourcesDefine()--------------开始加载资源列表数据--------");  
        List roles = userMapper.findAllRoles();  
        for(RolePO role : roles){  
            List resources = role.getResources(); 
            for(ResourcePO resource : resources){  
                Collection configAttributes = null;  
                ConfigAttribute configAttribute = new SecurityConfig(resource.getResourceName());  
                if(resourceMap.containsKey(resource.getUrl())){  
                    configAttributes = resourceMap.get(resource.getUrl());  
                    configAttributes.add(configAttribute);  
                }else{  
                    configAttributes = new ArrayList() ;  
                    configAttributes.add(configAttribute);  
                    resourceMap.put(resource.getUrl(), configAttributes);  
                }  
            }  
        }
        System.out.println("11");
        Set set = resourceMap.keySet();
        Iterator it = set.iterator();
        while(it.hasNext()){
        	String s = it.next();
        	System.out.println("key:"+s+"|value:"+resourceMap.get(s));
        }
    }  
    /*  
     * 根据请求的资源地址,获取它所拥有的权限 
     */  
    @Override  
    public Collection getAttributes(Object obj)  
            throws IllegalArgumentException {  
        //获取请求的url地址  
        String url = ((FilterInvocation)obj).getRequestUrl();  
        System.out.println("MySecurityMetadataSource:getAttributes()---------------请求地址为:"+url);  
        Iterator it = resourceMap.keySet().iterator();  
        while(it.hasNext()){  
            String _url = it.next();  
            if(_url.indexOf("?")!=-1){  
                _url = _url.substring(0, _url.indexOf("?"));  
            }  
            if(urlMatcher.match(_url,url)){
            	System.out.println("MySecurityMetadataSource:getAttributes()---------------需要的权限是:"+resourceMap.get(_url));  
                return resourceMap.get(_url);  
            }
            	
        }
        Collection nouse = new ArrayList();
        nouse.add(new SecurityConfig("无相应权限"));
        return nouse;
    }  
  
    @Override  
    public boolean supports(Class arg0) {  
        System.out.println("MySecurityMetadataSource.supports()---------------------");  
        return true;  
    }  
      
}  

到这里,我们还有一个疑问,就是decide方法中的authentication对象(authentication.getAuthorities()包含当前用户拥有的权限),用户的对应角色和权限信息是从哪里获得的?其实这里是通过调用MyUserDetailsServiceImpl来获取的,该类需要实现UserDetailService接口,更具体一些实际上是通过loadUserByUsername进行获取用户权限信息的,这里注意返回的User不是我们自己定义的PO,而是ss3框架中的User。(这里说下为什么我自己的UserPO没有继承ss的User,就是因为User没有默认无参构造方法,导致mybatis无法创建对象,具体可能还是有办法的,比如重写mybatis的相关接口,比较麻烦,所以这里是先通过返回我们自己的UserPO后,再组装成ss需要的User对象进行的)这里在回到刚才AccessDescisionManager中的decide方法想一下,authentication.getAuthorities()其实获得的就是下面的Collection类型的对象。

最后下面的这段代码,我没有直接从username中直接获得resource,而是通过先获得role,再通过role获取resource,我感觉这样方便一些,sql也简单,当然有更好的可以替换掉。

@Component("myUserDetailsServiceImpl")
public class MyUserDetailsServiceImpl implements UserDetailsService{

	@Resource
	private UserMapper userMapper;
	
	@Override
	public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		System.out.println("username is " + username);  
        UserPO user = userMapper.getUserByUserName(username);
        if(user == null) {  
            throw new UsernameNotFoundException(username);  
        }  
        Collection grantedAuths = obtionGrantedAuthorities(user);
          System.out.println(user.getUsername());
        return new User(
                user.getUsername(),  
                user.getPassword(),   
                true, 
                true,
                true,
                true,
                grantedAuths);  
	}

	//取得用户的权限  
    private Set obtionGrantedAuthorities(UserPO user) {  
        Set authSet = new HashSet();  
        List roles = user.getRoles();  
          
        for(RolePO role : roles) {  
        	RolePO innerRole = userMapper.getRoleByRoleName(role.getRoleName());
            List tempRes = innerRole.getResources();
            for(ResourcePO res : tempRes) {  
                authSet.add(new GrantedAuthorityImpl(res.getResourceName()));  
           }  
        }  
        return authSet;  
    }  
    
   
}

到这里,所有的权限-角色-用户信息已经可以串起来了。再来梳理一下流程,启动服务器时,通过FilterInvocationSecurityMetadataSource获得用户的所有角色及权限信息,当用户登陆时,通过MyUserDetailsServiceImpl中的loadUserByUsername获得该登陆用户所有的权限,发出请求时,通过FilterInvocationSecurityMetadataSource的getAttributes(Object url)获得需要的权限名,最后在AccessDecisionManager中decide方法进行对比,如果用户拥有的权限名称和该url需要的权限名相同,那么放行,否则认证失败!清楚这些后,我们还需要一个filter,把上述流程串起来,就像提葡萄一样~

/** 
 * @description 一个自定义的filter, 
 *  必须包含authenticationManager,accessDecisionManager,securityMetadataSource三个属性, 
        我们的所有控制将在这三个类中实现 
 */ 
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter{

	 private FilterInvocationSecurityMetadataSource fisMetadataSource;  
     
	    

		/* (non-Javadoc) 
	     * @see org.springframework.security.access.intercept.AbstractSecurityInterceptor#getSecureObjectClass() 
	     */  
	    @Override  
	    public Class getSecureObjectClass() {  
	        return FilterInvocation.class;  
	    }  
	  
	    @Override  
	    public SecurityMetadataSource obtainSecurityMetadataSource() {  
	        return fisMetadataSource;  
	    }  
	  
	    @Override  
	    public void destroy() {}  
	      
	    @Override  
	    public void doFilter(ServletRequest request, ServletResponse response,  
	            FilterChain chain) throws IOException, ServletException {  
	        System.out.println("------------MyFilterSecurityInterceptor.doFilter()-----------开始拦截器了....");  
	        FilterInvocation fi = new FilterInvocation(request, response, chain);  
	        InterceptorStatusToken token = super.beforeInvocation(fi);  
	        
	        try {  
	            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());  
	        } catch (Exception e) {  
	            e.printStackTrace();  
	        }finally{  
	            super.afterInvocation(token,null);  
	        }  
	        
	        System.out.println("------------MyFilterSecurityInterceptor.doFilter()-----------拦截器该方法结束了....");  
	    }  
	   
	    @Override  
	    public void init(FilterConfig config) throws ServletException {  
	          
	    }  
	      
	      
	    public void setFisMetadataSource(  
	            FilterInvocationSecurityMetadataSource fisMetadataSource) {  
	        this.fisMetadataSource = fisMetadataSource;  
	    }  
	    public FilterInvocationSecurityMetadataSource getFisMetadataSource() {
			return fisMetadataSource;
		}

}

如果全部照搬上边的代码,到这里就已经结束了。

但是昨天晚上遇到一个大坑,就是发现如果我在数据库中配置了该用户的相关权限url后,用户可以访问,如果用户没有该url的权限,该用户依然可以访问url,这是让我无比吃惊,因为大部分都是参考网络的资料写的,后来看了一下ss的源码,才发现可能是其他人写错了。这里简单说一下,因为单位电脑没有ss的源码,主要问题出在MyFilterSecurityInterceptor中的doFilter方法:InterceptorStatusToken token = super.beforeInvocation(fi);  当ss在未匹配到url的权限时,即MySecurityMetadataSource中的getAttributes返回的对象为空时,该方法beforeInvocation直接return null,而实际decide方法在下方并未运行。

 protected InterceptorStatusToken beforeInvocation(Object object) {

        if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
            .....
        }

        Collection attributes = this.obtainSecurityMetadataSource().getAttributes(object);

        if (attributes == null || attributes.isEmpty()) {//此处判断MySecurityMetadataSource中的getAttributes返回的对象
            if (rejectPublicInvocations) {
                throw new IllegalArgumentException("Secure object invocation " + object +
                        " was denied as public invocations are not allowed via this interceptor. "
                                + "This indicates a configuration error because the "
                                + "rejectPublicInvocations property is set to 'true'");
            }

            if (debug) {
                logger.debug("Public object - authentication not attempted");
            }

            publishEvent(new PublicInvocationEvent(object));

            return null; // no further work post-invocation
        }

        if (debug) {
            logger.debug("Secure object: " + object + "; Attributes: " + attributes);
        }

        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            credentialsNotFound(messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
                    "An Authentication object was not found in the SecurityContext"), object, attributes);
        }

        Authentication authenticated = authenticateIfRequired();//实际运行decide方法的地方

        // Attempt authorization
        try {
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException accessDeniedException) {
            publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException));

            throw accessDeniedException;
        }

        if (debug) {
            logger.debug("Authorization successful");
        }

        if (publishAuthorizationSuccess) {
            publishEvent(new AuthorizedEvent(object, attributes, authenticated));
        }

        // Attempt to run as a different user
        Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);

        if (runAs == null) {
            if (debug) {
                logger.debug("RunAsManager did not change Authentication object");
            }

            // no further work post-invocation
            return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
        } else {
            if (debug) {
                logger.debug("Switching to RunAs Authentication: " + runAs);
            }

            SecurityContext origCtx = SecurityContextHolder.getContext();
            SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
            SecurityContextHolder.getContext().setAuthentication(runAs);

            // need to revert to token.Authenticated post-invocation
            return new InterceptorStatusToken(origCtx, true, attributes, object);
        }
    }


在我看的所有BLOG中,当匹配不到时,全部返回了Null,而当我追到 super.beforeInvocation(fi)源码中时,发现当getAttributes返回null后,ss就会跳过AccessDecisionManager的decide方法,导致未进行判断!从而ss会让用户请求顺利的通过。之后,查了一下ss官方英文文档,如下描述:

Collection getAttributes(Object object)
                                          throws IllegalArgumentException
Accesses the  ConfigAttributes that apply to a given secure object.
Parameters:
object - the object being secured
Returns:
the attributes that apply to the passed in secured object. Should return an empty collection if there are no applicable attributes.
Throws:
IllegalArgumentException - if the passed object is not of a type supported by the  SecurityMetadataSource implementation

红色标出了,应当返回一个空的对象集合如果没有相应权限的时候。而其他blog文返回的是null,导致后续跳过了decide方法!所以我在MySecurityMetadataSource中的getAttributes中写的是:

        Collection nouse = new ArrayList();
        nouse.add(new SecurityConfig("无相应权限"));
        return nouse;
这样当没有权限时,才可以正常拦截!现在博文抄来抄去,正确的还好,但凡有错误。。真是坑死人。

这里发下几个帮助比较大的供参考:

http://aokunsang.iteye.com/blog/1638558

http://blog.csdn.net/k10509806/article/details/6369131

和只允许登陆一次的具体方法,需要重写UserPO中的hashCode和equal方法。

http://flashing.iteye.com/blog/823666


你可能感兴趣的:(spring相关,spring,security,spring,security3.2,RBAC,自定义角色,自定义权限)