SpringSecurity——基于Spring、SpringMVC和MyBatis自定义SpringSecurity权限认证规则

 

本文在SpringMVC和MyBatis项目框架的基础上整合Spring Security作为权限管理。并且完全实现一套自定义的权限管理规则。


1.权限管理
在本例中所使用的权限管理的思路如下图所示,在系统中存在着许多帐号,同时存在着许多资源,在一个Web系统中一个典型的资源就是访问页面的URL,控制了这个就能够直接控制用户的访问权。

由于资源非常多,直接针对资源与用户进行设置关系会比较繁琐,因此针对同一类或者同一组的资源打个包,称为一组权限,这样将权限分配给用户的时候,一组权限中的资源也就都分配给用户了。

这个只是一个非常简单的权限管理方案,并且只能适用于较小的项目,因为此处给出这个只是为了便于理解自定义的Spring Security认证规则。

2.Spring Security的认证规则
要编写自定义的认证规则,首先需要对Spring Security中的认证规则有一定的了解,下面简单介绍下Spring Security的认证规则。

1)在Spring Security中每个URL都是一个资源,当系统启动的时候,Spring Security会根据配置将所有的URL与访问这个URL所需要的权限的映射数据加载到Spring Security中。

2)当一个请求访问一个资源时,Spring Security会判断这个URL是否需要权限验证,如果不需要,那么直接访问即可。

3)如果这个URL需要进行权限验证,那么Spring Security会检查当前请求来源所属用户是否登录,如果没有登录,则跳转到登录页面,进行登录操作,并加载这个用户的相关信息

4)如果登录,那么判断这个用户所拥有的权限是否包含访问这个URL所需要的权限,如果有则允许访问

5)如果没有权限,那么就给出相应的提示信息

3.自定义认证规则思路
根据上面一小节介绍的Spring Security认证的过程,我们相应的就能够分析出对于这个过程我们如果要修改的话,需要进行哪些方面的改动。

3.1.自定义SecurityMetadataSource
在Spring Security中的 SecurityMetadataSource 处于上面的步骤一中,也就是用于加载URL与权限对应关系的,对于这个我们需要自己进行定义

package com.oolong.customsecurity;
 
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
 
import org.apache.log4j.LogManager;
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.stereotype.Component;
 
/**
 * 加载URL与权限资源,并提供根据URL匹配权限的方法
 * @author weilu2
 * @date 2016年12月17日 上午11:18:52
 *
 */
@Component
public class CustomSecurityMetadataSource 
        implements FilterInvocationSecurityMetadataSource {
 
    private Map<String, List<ConfigAttribute>> resources;
    
    public CustomSecurityMetadataSource() {
        loadAuthorityResources();
    }
    
    private void loadAuthorityResources() {
        // 此处在创建时从数据库中初始化权限数据
        // 将权限与资源数据整理成 Map<resource, List<Authority>> 的形式
        // 注意:加载URL资源时,需要对资源进行排序,要由精确到粗略进行排序,让精确的URL优先匹配
        resources = new HashMap<>();
        
        // 此处先伪造一些数据
        List<ConfigAttribute> authorityList = new ArrayList<>();
        ConfigAttribute auth = new SecurityConfig("AUTH_WELCOME");
        authorityList.add(auth);
        resources.put("/welcome", authorityList);
    }
    
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        
        String url = ((FilterInvocation) object).getRequestUrl();
        
        Set<String> keys = resources.keySet();
        
        for (String k : keys) {
            if (url.indexOf(k) >= 0) {
                return resources.get(k);
            }
        }
        return null;
    }
 
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        // TODO Auto-generated method stub
        return null;
    }
 
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

}

在这个类中,实现了FilterInvocationSecurityMetadataSource接口,这个接口中的 getAttributes(Object object)方法能够根据请求的URL,获取这个URL所需要的权限,那么我们就可以在这个类初始化的时候将所有需要的权限加载进来,然后根据我们的规则进行获取,因此这里还需要编写一个加载数据的方法 loadAuthorityResources(),并且在构造函数中调用。

此处加载资源为了简化,只是随意填充了一些数据,实际可以从数据库中获取。

3.2.自定义AccessDecisionManager
编写自定义的决策管理器,决策管理器是Spring Security用来决定对于一个用户的请求是否基于通过的中心控制。

package com.oolong.customsecurity;
 
import java.util.Collection;
import java.util.Iterator;
 
import org.apache.log4j.LogManager;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
 
/**
 * 进行决策,根据URL获得访问这个资源所需要的权限,然后在与当前用户所拥有的权限进行对比
 * 如果当前用户拥有相关权限,就直接返回,否则抛出 AccessDeniedException异常
 * @author weilu2
 * @date 2016年12月17日 上午11:30:40
 *
 */
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
 
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException {
        
        LogManager.getLogger("CustomAccessDecisionManager").info("decide invoke");
        
        if (configAttributes == null) {
            return;
        }
        
        if (configAttributes.size() <= 0) {
            return;
        }
        
        Iterator<ConfigAttribute> authorities = configAttributes.iterator();
        String needAuthority = null;
        
        while(authorities.hasNext()) {
            ConfigAttribute authority = authorities.next();
            
            if (authority == null || (needAuthority = authority.getAttribute()) == null) {
                continue;
            }
 
            LogManager.getLogger("CustomAccessDecisionManager").info("decide == " + needAuthority);
            
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                if (needAuthority.equals(ga.getAuthority().trim())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("No Authority");
    }
 
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }
 
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

}

决策管理器最重要的就是这个 decide()方法,Spring Security会将当前登录用户信息包装到一个 Authentication对象中,并传入这个方法;并且调用 SecurityMetadataSource.getAttributes() 方法获取这个URL相关的权限以参数 Collection<ConfigAttribute> 的形式传入这个方法。

然后这个decide方法获取到这两个信息之后就可以进行对比决策了。如果当前用户允许登录,那么直接return即可。如果当前用户不许运行登录,则抛出一个 AccessDeniedException异常。


3.3.自定义 UserDetailsService 和 AuthenticationProvider

前面说过,要进行验证,除了有URL与权限的映射关系,还需要有用户的权限信息。要编写自定义的用户数据加载,就需要实现这两个接口。

3.3.1.UserDetailsService

package com.oolong.customsecurity;
 
import java.util.ArrayList;
import java.util.List;
 
import org.apache.log4j.LogManager;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
 
import com.oolong.model.AccountInfoModel;
import com.oolong.model.AuthorityModel;
 
@Component
public class CustomUserDetailsService implements UserDetailsService {
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 
        LogManager.getLogger("CustomUserDetailsService").info("loadUserByUsername invoke");
 
        // 提供到数据库查询该用户的权限信息
        // 关于角色和权限的转换关系在此处处理,根据用户与角色的关系、角色与权限的关系,
        // 将用户与权限的管理整理出来
        
        // 此处伪造一些数据
        // 伪造权限
        AuthorityModel authority = new AuthorityModel("AUTH_WELCOME");
        List<AuthorityModel> authorities = new ArrayList<>();
        authorities.add(authority);
        
        AccountInfoModel account = new AccountInfoModel("oolong", "12345");
        account.setAuthorities(authorities);
        
        return account;
    }
}
 

 

3.3.2.AuthenticationProvider
AuthenticationProvider用于包装UserDetailsService,并将其提供给 Spring Security使用。这个接口中最重要的是实现 retrieveUser() 方法,这个请参考接口的说明进行实现,此处不再赘述。

package com.oolong.customsecurity;
 
import org.apache.log4j.LogManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
 
/**
 * 这两个方法用于添加额外的检查功能,此处不需要添加,因此空着,直接实现这个抽象类即可。
 * @author weilu2
 * @date 2016年12月17日 下午12:20:27
 *
 */
@Component
public class CustomUserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
 
    @Autowired
    private UserDetailsService userDetailsService;
    
    public UserDetailsService getUserDetailService() {
        return this.userDetailsService;
    }
 
    public void setUserDetailService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
 
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        
    }
 
    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
 
        LogManager.getLogger("CustomUserDetailsAuthenticationProvider").info("retrieveUser invoke");
        
        if (userDetailsService == null) {
            throw new AuthenticationServiceException("");
        }
        
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        
        if (userDetails == null) {
            throw new UsernameNotFoundException(username);
        }
        
        if (userDetails.getUsername().equals(authentication.getPrincipal().toString()) 
                && userDetails.getPassword().equals(authentication.getCredentials().toString())) {
            return userDetails;
        }
        
        throw new BadCredentialsException(username + authentication.getCredentials());
    }
}

 

 

3.4.UserDetails和GrantedAuthority
这两个接口非常简单,请参考源码,此处不再赘述

4.配置
上面编写的这些自定义的实现都有了,但是仅仅这样是没有用的,如何配置能够让它们起作用呢?

package com.oolong.config;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
 
import com.oolong.customsecurity.CustomAccessDecisionManager;
import com.oolong.customsecurity.CustomSecurityMetadataSource;
import com.oolong.customsecurity.CustomUserDetailsAuthenticationProvider;
import com.oolong.customsecurity.TempHook;
 
@Configuration
@ComponentScan(basePackageClasses={TempHook.class})
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private CustomUserDetailsAuthenticationProvider customAuthenticationProvider;
    
    @Autowired
    private CustomAccessDecisionManager customAccessDecisionManager;
    
    @Autowired
    private CustomSecurityMetadataSource customSecurityMetadataSource;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(customAuthenticationProvider);
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(customFilterSecurityInterceptor(), ExceptionTranslationFilter.class);
        http.formLogin();
    }
    
    @Bean
    public FilterSecurityInterceptor customFilterSecurityInterceptor() {
        FilterSecurityInterceptor fsi = new FilterSecurityInterceptor();
        fsi.setAccessDecisionManager(customAccessDecisionManager);
        fsi.setSecurityMetadataSource(customSecurityMetadataSource);
        
        return fsi;
    }
}

在Spring MVC中,Spring Security是通过过滤器发挥作用的,因此我们就爱那个决策管理器与数据加载放到一个过滤器中,然后将这个过滤器插入到系统的过滤器链中。

此外,我们向系统中提供了一个用于检索用户的 AuthenticationProvicer。

还有,别忘记了,告诉系统,如果用户没有权限应该怎么办,http.formLogin(),告诉Spring Security要跳转到表单登录页面。

参考

[1] 源码

 

posted @ 2016-12-20 11:23  大肥肥就是我  阅读(7208)  评论(1编辑  收藏  举报