spring-security權限控制詳解


在本例中,主要講解spring-boot與spring-security的集成,實現方式為:

  • 將用戶、權限、資源(url)采用數據庫存儲
  • 自定義過濾器,代替原有的 FilterSecurityInterceptor
  • 自定義實現 UserDetailsService、AccessDecisionManager和InvocationSecurityMetadataSourceService,並在配置文件進行相應的配置
    GitHub 地址:https://github.com/fp2952/spring-boot-security-demo

用戶角色表(基於RBAC權限控制)

  • 用戶表(base_user)
code type length
ID varchar 32
USER_NAME varchar 50
USER_PASSWORD varchar 100
NIKE_NAME varchar 50
STATUS int 11
  • 用戶角色表(base_user_role)
code type length
ID varchar 32
USER_ID varchar 32
ROLE_ID varchar 32
  • 角色表(base_role)
code type length
ID varchar 32
ROLE_CODE varchar 32
ROLE_NAME varchar 64
  • 角色菜單表(base_role_menu)
code type length
ID varchar 32
ROLE_ID varchar 32
MENU_ID varchar 32
  • 菜單表(base_menu)
code type length
ID varchar 32
MENU_URL varchar 120
MENU_SEQ varchar 120
MENU_PARENT_ID varchar 32
MENU_NAME varchar 50
MENU_ICON varchar 20
MENU_ORDER int 11
IS_LEAF varchar 20

實現主要配置類

實現AbstractAuthenticationProcessingFilter

用於用戶表單驗證,內部調用了authenticationManager完成認證,根據認證結果執行successfulAuthentication或者unsuccessfulAuthentication,無論成功失敗,一般的實現都是轉發或者重定向等處理。

   @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        //獲取表單中的用戶名和密碼
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        if (username == null) {
            username = "";
        }
        if (password == null) {
            password = "";
        }
        username = username.trim();
        //組裝成username+password形式的token
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        //交給內部的AuthenticationManager去認證,並返回認證信息
        return this.getAuthenticationManager().authenticate(authRequest);
    }

AuthenticationManager

AuthenticationManager是一個用來處理認證(Authentication)請求的接口。在其中只定義了一個方法authenticate(),該方法只接收一個代表認證請求的Authentication對象作為參數,如果認證成功,則會返回一個封裝了當前用戶權限等信息的Authentication對象進行返回。
Authentication authenticate(Authentication authentication) throws AuthenticationException;
在Spring Security中,AuthenticationManager的默認實現是ProviderManager,而且它不直接自己處理認證請求,而是委托給其所配置的AuthenticationProvider列表,然后會依次使用每一個AuthenticationProvider進行認證,如果有一個AuthenticationProvider認證后的結果不為null,則表示該AuthenticationProvider已經認證成功,之后的AuthenticationProvider將不再繼續認證。然后直接以該AuthenticationProvider的認證結果作為ProviderManager的認證結果。如果所有的AuthenticationProvider的認證結果都為null,則表示認證失敗,將拋出一個ProviderNotFoundException。
校驗認證請求最常用的方法是根據請求的用戶名加載對應的UserDetails,然后比對UserDetails的密碼與認證請求的密碼是否一致,一致則表示認證通過。
Spring Security內部的DaoAuthenticationProvider就是使用的這種方式。其內部使用UserDetailsService來負責加載UserDetails。在認證成功以后會使用加載的UserDetails來封裝要返回的Authentication對象,加載的UserDetails對象是包含用戶權限等信息的。認證成功返回的Authentication對象將會保存在當前的SecurityContext中。

實現UserDetailsService

UserDetailsService只定義了一個方法 loadUserByUsername,根據用戶名可以查到用戶並返回的方法。

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.debug("權限框架-加載用戶");
        List<GrantedAuthority> auths = new ArrayList<>();

        BaseUser baseUser = new BaseUser();
        baseUser.setUserName(username);
        baseUser = baseUserService.selectOne(baseUser);

        if (baseUser == null) {
            logger.debug("找不到該用戶 用戶名:{}", username);
            throw new UsernameNotFoundException("找不到該用戶!");
        }
        if(baseUser.getStatus()==2)
        {
            logger.debug("用戶被禁用,無法登陸 用戶名:{}", username);
            throw new UsernameNotFoundException("用戶被禁用!");
        }
        List<BaseRole> roles = baseRoleService.selectRolesByUserId(baseUser.getId());
        if (roles != null) {
            //設置角色名稱
            for (BaseRole role : roles) {
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getRoleCode());
                auths.add(authority);
            }
        }

        return new org.springframework.security.core.userdetails.User(baseUser.getUserName(), baseUser.getUserPassword(), true, true, true, true, auths);
    }

實現AbstractSecurityInterceptor

訪問url時,會被AbstractSecurityInterceptor攔截器攔截,然后調用FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的全部權限,再調用授權管理器AccessDecisionManager鑒權。

public class CustomSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    private FilterInvocationSecurityMetadataSource securityMetadataSource;
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }
    @Override
    public void destroy() {
    }
    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }
    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
    public void invoke(FilterInvocation fi) throws IOException {
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } catch (ServletException e) {
            super.afterInvocation(token, null);
        }
    }
    public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
        return securityMetadataSource;
    }

    public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource securityMetadataSource) {
        this.securityMetadataSource = securityMetadataSource;
    }
}

FilterInvocationSecurityMetadataSource 獲取所需權限

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        //獲取當前訪問url
        String url = ((FilterInvocation) object).getRequestUrl();
        int firstQuestionMarkIndex = url.indexOf("?");
        if (firstQuestionMarkIndex != -1) {
            url = url.substring(0, firstQuestionMarkIndex);
        }
        List<ConfigAttribute> result = new ArrayList<>();

        try {
            //設置不攔截
            if (propertySourceBean.getProperty("security.ignoring") != null) {
                String[] paths = propertySourceBean.getProperty("security.ignoring").toString().split(",");
                //判斷是否符合規則
                for (String path: paths) {
                    String temp = StringUtil.clearSpace(path);
                    if (matcher.match(temp, url)) {
                        return SecurityConfig.createList("ROLE_ANONYMOUS");
                    }
                }
            }

            //如果不是攔截列表里的, 默認需要ROLE_ANONYMOUS權限
            if (!isIntercept(url)) {
                return SecurityConfig.createList("ROLE_ANONYMOUS");
            }

            //查詢數據庫url匹配的菜單
            List<BaseMenu> menuList = baseMenuService.selectMenusByUrl(url);
            if (menuList != null && menuList.size() > 0) {
                for (BaseMenu menu : menuList) {
                    //查詢擁有該菜單權限的角色列表
                    List<BaseRole> roles = baseRoleService.selectRolesByMenuId(menu.getId());
                    if (roles != null && roles.size() > 0) {
                        for (BaseRole role : roles) {
                            ConfigAttribute conf = new SecurityConfig(role.getRoleCode());
                            result.add(conf);
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

     /**
     * 判斷是否需要過濾
     * @param url
     * @return
     */
    public boolean isIntercept(String url) {
        String[] filterPaths = propertySourceBean.getProperty("security.intercept").toString().split(",");
        for (String filter: filterPaths) {
            if (matcher.match(StringUtil.clearSpace(filter), url) & !matcher.match(indexUrl, url)) {
                return true;
            }
        }

        return false;
    }

AccessDecisionManager 鑒權

    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        if (collection == null) {
            return;
        }
        for (ConfigAttribute configAttribute : collection) {
            String needRole = configAttribute.getAttribute();
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                if (needRole.trim().equals(ga.getAuthority().trim()) || needRole.trim().equals("ROLE_ANONYMOUS")) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("無權限!");
    }

配置 WebSecurityConfigurerAdapter

/**
 * spring-security配置
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PropertySource propertySourceBean;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        logger.debug("權限框架配置");

        String[] paths = null;
        //設置不攔截
        if (propertySourceBean.getProperty("security.ignoring") != null) {
            paths = propertySourceBean.getProperty("security.ignoring").toString().split(",");
            paths = StringUtil.clearSpace(paths);
        }

        //設置過濾器
        http    // 根據配置文件放行無需驗證的url
                .authorizeRequests().antMatchers(paths).permitAll()
                .and()
                .httpBasic()
                // 配置驗證異常處理
                .authenticationEntryPoint(getCustomLoginAuthEntryPoint())
                // 配置登陸過濾器
                .and().addFilterAt(getCustomLoginFilter(), UsernamePasswordAuthenticationFilter.class)
                // 配置 AbstractSecurityInterceptor
                .addFilterAt(getCustomSecurityInterceptor(), FilterSecurityInterceptor.class)
                // 登出成功處理
                .logout().logoutSuccessHandler(getCustomLogoutSuccessHandler())
                // 關閉csrf
                .and().csrf().disable()
                // 其他所有請求都需要驗證
                .authorizeRequests().anyRequest().authenticated()
                // 配置登陸url, 登陸頁面並無需驗證
                .and().formLogin().loginProcessingUrl("/login").loginPage("/login.ftl").permitAll()
                // 登出
                .and().logout().logoutUrl("/logout").permitAll();
        
        logger.debug("配置忽略驗證url");

    }

    @Autowired
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.authenticationProvider(getDaoAuthenticationProvider());
    }


    /**
     * spring security 配置
     * @return
     */
    @Bean
    public CustomLoginAuthEntryPoint getCustomLoginAuthEntryPoint() {
        return new CustomLoginAuthEntryPoint();
    }

    /**
     * 用戶驗證
     * @return
     */
    @Bean
    public DaoAuthenticationProvider getDaoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setHideUserNotFoundExceptions(false);
        provider.setPasswordEncoder(new BCryptPasswordEncoder());
        return provider;
    }

    /**
     * 登陸
     * @return
     */
    @Bean
    public CustomLoginFilter getCustomLoginFilter() {
        CustomLoginFilter filter = new CustomLoginFilter();
        try {
            filter.setAuthenticationManager(this.authenticationManagerBean());
        } catch (Exception e) {
            e.printStackTrace();
        }
        filter.setAuthenticationSuccessHandler(getCustomLoginAuthSuccessHandler());
        filter.setAuthenticationFailureHandler(new CustomLoginAuthFailureHandler());

        return filter;
    }

    @Bean
    public CustomLoginAuthSuccessHandler getCustomLoginAuthSuccessHandler() {
        CustomLoginAuthSuccessHandler handler =  new CustomLoginAuthSuccessHandler();
        if (propertySourceBean.getProperty("security.successUrl")!=null){
            handler.setAuthSuccessUrl(propertySourceBean.getProperty("security.successUrl").toString());
        }
        return handler;
    }

    /**
     * 登出
     * @return
     */
    @Bean
    public CustomLogoutSuccessHandler getCustomLogoutSuccessHandler() {
        CustomLogoutSuccessHandler handler = new CustomLogoutSuccessHandler();
        if (propertySourceBean.getProperty("security.logoutSuccessUrl")!=null){
            handler.setLoginUrl(propertySourceBean.getProperty("security.logoutSuccessUrl").toString());
        }
        return handler;
    }

    /**
     * 過濾器
     * @return
     */
    @Bean
    public CustomSecurityInterceptor getCustomSecurityInterceptor() {
        CustomSecurityInterceptor interceptor = new CustomSecurityInterceptor();
        interceptor.setAccessDecisionManager(new CustomAccessDecisionManager());
        interceptor.setSecurityMetadataSource(getCustomMetadataSourceService());
        try {
            interceptor.setAuthenticationManager(this.authenticationManagerBean());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return interceptor;
    }
    
    @Bean
    public CustomMetadataSourceService getCustomMetadataSourceService() {
        CustomMetadataSourceService sourceService = new CustomMetadataSourceService();
        if (propertySourceBean.getProperty("security.successUrl")!=null){
            sourceService.setIndexUrl(propertySourceBean.getProperty("security.successUrl").toString());
        }
        return sourceService;
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM