Spring Security賬號密碼認證 + 自定義鑑權(示例)

聲明 本文先演示效果,然後再給出幾個相對關鍵的類;完整測試項目,可詳見文末鏈接


效果演示

說明

  1. 張三屬於普通用戶,能訪問一些普通的頁面以及/user頁。
  2. 李四屬於數據庫管理員,能訪問一些普通的頁面以及/user頁、/dba頁。
  3. 王五屬於超級管理員,能訪問一些普通的頁面以及/user頁、/dba頁、/admin頁。

演示

  • 普通用戶張三:在這裏插入圖片描述

  • 數據庫管理員李四:在這裏插入圖片描述

  • 超級管理員王五:在這裏插入圖片描述


相關代碼(庫表)

項目整體說明在這裏插入圖片描述

注:上圖中,只對相對關鍵的內容進行了簡單說明;完整測試項目,可詳見文末鏈接

相關庫表說明

在這裏插入圖片描述

幾個相對關鍵的類

提示 完整測試項目,可詳見文末鏈接

  • MyAccessDecisionManager:

    import com.pingan.springsecurity.model.MyUserDetails;
    import com.pingan.springsecurity.service.impl.MyUserDetailsService;
    import org.springframework.security.access.AccessDecisionManager;
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.access.ConfigAttribute;
    import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
    import org.springframework.security.authentication.InsufficientAuthenticationException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.web.FilterInvocation;
    import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.Collection;
    import java.util.List;
    import java.util.stream.Collectors;
    
    /**
     * 決策器 (判斷鑑權是否通過)
     *
     * @author JustryDeng
     * @date 2019/12/14 11:34
     */
    @Component
    public class MyAccessDecisionManager implements AccessDecisionManager {
    
        /**
         * 決策
         *
         * @param authentication
         *            當前用戶的信息模型
         *            注: 由於重寫了{@link MyUserDetailsService#loadUserByUsername},
         *                重寫後該方法實際返回的類型是{@link MyUserDetails}, 所以這裏直接
         *                將Authentication強轉爲MyUserDetails。
         * @param object
         *            當前request的封裝
         * @param configAttributes
         *            與訪問的目標uri相關聯的配置屬性
         *            注:在本示例中,不需要用到此屬性; 如果在這裏需要用到此屬性的話,可以在通過
         *               實現{@link FilterInvocationSecurityMetadataSource},重寫相關返
         *               回Collection<ConfigAttribute>的方法,該返回值會作爲形參傳遞到本方法
         *               然後在這裏就能拿到對應的值了。
         *               可參考網友的示例<linked>https://www.jianshu.com/p/e715cc993bf0</linked>
         *
         * @throws AccessDeniedException
         *             當前用戶無權訪問
         * @throws InsufficientAuthenticationException
         *             當前用戶信任級別不夠,無法訪問
         * @date 2019/12/14 12:06
         */
        @Override
        public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
            HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
            String targetPath = request.getRequestURI();
            Object principal = authentication.getPrincipal();
            /*
             * 若認證過,那麼principal instanceof MyUserDetails爲true, 否則用戶沒有認證過
             *
             * 注: 鑑權一般都在認證之後, 沒有認證談何鑑權。
             *
             * 注: 一旦拋出異常後,
             *     在{@link ExceptionTranslationFilter#doFilter}中會對拋出的AuthenticationException異常(包括其子異常)
             *     進行相關處理。如: 拋出AuthenticationCredentialsNotFoundException異常,就會被處理然後頁面跳轉至登錄頁
             *
             */
            if (!(principal instanceof MyUserDetails)) {
                throw new AuthenticationCredentialsNotFoundException(" there is no any Authentication object MyUserDetails in the SecurityContext");
            }
            MyUserDetails myUserDetails = (MyUserDetails)principal;
            // 這個用戶可訪問的所有資源信息
            Collection<? extends GrantedAuthority> grantedAuthority = myUserDetails.getAuthorities();
            List<String> list = grantedAuthority.parallelStream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.toList());
            if(list.contains(targetPath)) {
                // 鑑權通過, 有訪問權限
                return;
            }
            // 鑑權不通過, 沒有訪問權限
            throw new AccessDeniedException(
                    String.format("You(%s) don't have any authorizion access %s", myUserDetails.getName(), targetPath)
            );
        }
    
        /**
         * 當前AccessDecisionManager實例能否處理 傳遞的ConfigAttribute呈現的授權請求
         */
        @Override
        public boolean supports(ConfigAttribute attribute) {
            return true;
        }
    
        /**
         * 當前AccessDecisionManager實例是否支持提供訪問控制決策
         */
        @Override
        public boolean supports(Class<?> clazz) {
            return true;
        }
    }
    
  • MyFilterInvocationSecurityMetadataSource:

    import com.pingan.springsecurity.mapper.DaoMapper;
    import com.pingan.springsecurity.model.ApiResource;
    import lombok.RequiredArgsConstructor;
    import org.springframework.security.access.ConfigAttribute;
    import org.springframework.security.web.FilterInvocation;
    import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.PostConstruct;
    import javax.servlet.http.HttpServletRequest;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    
    /**
     * 存儲ConfigAttribute信息, 並根據ConfigAttribute信息的有無, 決定是否走 決策器
     * 即: 若{@link this#getAttributes}返回的集合滿足CollectionUtils.isEmpty(list)爲true的話,
     *     那麼不會走決策器,
     *     否者會走決策器
     *
     * @author JustryDeng
     * @date 2019/12/14 11:20
     */
    @Component
    @RequiredArgsConstructor
    public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    
        private List<String> needAuthPaths = new ArrayList<>(16);
    
        private final DaoMapper mapper;
    
        @PostConstruct
        private void init() {
            needAuthPaths.addAll(mapper.selectNeedAuthPaths());
        }
    
        @Override
        public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
            HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
            String targetPath = request.getRequestURI();
            if (needAuthPaths.contains(targetPath)) {
                // 需要鑑權
                List<ConfigAttribute> list = new ArrayList<>(1);
                list.add(ApiResource.builder().build());
                return list;
            }
            // 不需要鑑權
            return null;
        }
    
        @Override
        public Collection<ConfigAttribute> getAllConfigAttributes() {
            return null;
        }
    
        @Override
        public boolean supports(Class<?> clazz) {
            return true;
        }
    }
    
  • MySecurityInterceptor:

    import org.springframework.security.access.SecurityMetadataSource;
    import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
    import org.springframework.security.access.intercept.InterceptorStatusToken;
    import org.springframework.security.web.FilterInvocation;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.*;
    import java.io.IOException;
    
    /**
     * 通過MySecurityInterceptor 註冊 MyAccessDecisionManager
     *
     * @author JustryDeng
     * @date 2019/12/14 12:50
     */
    @Component
    public class MySecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    
        private final MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;
    
        public MySecurityInterceptor(MyAccessDecisionManager myAccessDecisionManager,
                                     MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource) {
            this.myFilterInvocationSecurityMetadataSource = myFilterInvocationSecurityMetadataSource;
            // 設置 以 自定義的決策權 進行 鑑權管理
            super.setAccessDecisionManager(myAccessDecisionManager);
        }
    
        @Override
        public Class<?> getSecureObjectClass() {
            return FilterInvocation.class;
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
                throws IOException, ServletException {
            FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
            invoke(fi);
        }
    
        public void invoke(FilterInvocation fi) throws IOException, ServletException {
            InterceptorStatusToken token = super.beforeInvocation(fi);
            try {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            } finally {
                super.afterInvocation(token, null);
            }
        }
    
        /**
         * 返回自定義的SecurityMetadataSource
         */
        @Override
        public SecurityMetadataSource obtainSecurityMetadataSource() {
            // 如果有實現SecurityMetadataSource的話,可以設置採用自定義的 資源器, 如:
            // 如果實現有FilterInvocationSecurityMetadataSource的話,可以將其進行註冊(即:返回其實例)
            return myFilterInvocationSecurityMetadataSource;
        }
    }
    
  • MyWebSecurityConfigurerAdapter:

    import com.pingan.springsecurity.service.impl.MyUserDetailsService;
    import lombok.RequiredArgsConstructor;
    import org.springframework.context.annotation.Bean;
    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.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    /**
     * SpringSecurity配置
     *
     * @author JustryDeng
     * @date 2019/12/7 14:08
     */
    @Configuration
    @RequiredArgsConstructor
    public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
    
        private final MyUserDetailsService myUserDetailsService;
    
        @Override
        public void configure(WebSecurity web) {
            /*
             * 對於那些沒必要進行保護的資源, 可以使用ignoring,使其跳過SpringSecurity
             *
             * 注:configure(HttpSecurity http)方法裏的permitAll();也有類似的效果,
             *    不過permitAll會走SpringSecurity,只是說無條件放行而已。
             */
            web.ignoring().antMatchers("/picture/**");
            web.ignoring().antMatchers("/md/**");
            // 開發時,可以將SpringSecurity的debug打開
            web.debug(false);
        }
    
        /**
         * SpringSecurity提供有一些基本的頁面(如:login、logout等);如果覺得它提供的
         * 基礎頁面難看,想使用自己的頁面的話,可以在此方法裏面進行相關配置。
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 設置登錄方式爲 表單登錄
            http.formLogin();
            /// 設置登錄方式爲 彈框登錄
            /// http.httpBasic();
            /// 自定義登錄頁
            /// http.formLogin().loginPage("myLoginPae");
            /// 自定義登出頁
            /// http.logout().logoutUrl("myLogoutPae");
            // 登出成功時,跳轉至此url
            http.logout().logoutSuccessUrl("/logout/success");
            // 登錄成功時,跳轉至此url
            // 注意:如果未登錄,直接訪問 登錄失敗頁的話,會被DefaultLoginPageGeneratingFilter識別,並跳轉至登錄頁進行登錄
            http.formLogin().successForwardUrl("/index");
            // 登錄失敗時,跳轉至此url
            // 注意:如果未登錄,直接訪問 登錄失敗頁的話,會被DefaultLoginPageGeneratingFilter識別,並跳轉至登錄頁進行登錄
            http.formLogin().failureUrl("/login/failed");
            /// 當鑑權不通過,是 跳轉至此url
            http.exceptionHandling().accessDeniedPage("/403");
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 配置 UserDetailsService, 用戶自定義查詢用戶的信息
            auth.userDetailsService(myUserDetailsService);
        }
    
        /**
         * 自定義 加密器
         *
         * 注:只需要將其註冊進入容器中即可,InitializeUserDetailsBeanManagerConfigurer類會從容器
         *    拿去PasswordEncoder.class實現,作爲其加密器
         *
         * @date 2019/12/21 17:59
         */
        @Bean
        public PasswordEncoder myPasswordEncoder() {
            return new PasswordEncoder() {
                @Override
                public String encode(CharSequence rawPassword) {
                    return rawPassword == null ? "" : rawPassword.toString();
                }
    
                @Override
                public boolean matches(CharSequence rawPassword, String encodedPassword) {
                    if (rawPassword == null || rawPassword.length() == 0) {
                        return false;
                    }
                    return rawPassword.equals(encodedPassword);
                }
            };
        }
    }
    
  • MyUserDetailsService:

    import com.pingan.springsecurity.mapper.DaoMapper;
    import com.pingan.springsecurity.model.ApiResource;
    import com.pingan.springsecurity.model.MyUserDetails;
    import com.pingan.springsecurity.model.Role;
    import lombok.RequiredArgsConstructor;
    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.Service;
    
    import java.util.List;
    import java.util.stream.Collectors;
    
    /**
     * 對{@link UserDetailsService#loadUserByUsername(String)}進行重寫
     *
     * @author JustryDeng
     * @date 2019/12/14 10:20
     */
    @Service
    @RequiredArgsConstructor
    public class MyUserDetailsService implements UserDetailsService {
    
        private final DaoMapper mapper;
    
        /**
         * 根據賬號名, 查詢用戶信息
         *
         * todo 用戶名不存在時,不處理的話,最終拋出InternalAuthenticationServiceException
         */
        @Override
        public UserDetails loadUserByUsername(String accountNo) throws UsernameNotFoundException {
            // 查詢用戶基本信息
            MyUserDetails myUserDetails = mapper.selectUserBasicInfoByAccountNo(accountNo);
            // 查詢用戶角色信息
            List<Role> roleList = mapper.selectRolesByUserId(myUserDetails.getId());
            // 查詢用戶權限信息(即:查詢用戶可訪問的資源)
            List<Integer> roleIdList = roleList.parallelStream().map(Role::getId).collect(Collectors.toList());
            List<ApiResource> apiResources = mapper.selectApiResourcesByRoleIds(roleIdList);
    
            // 組裝信息並返回
            myUserDetails.setRoles(roleList);
            myUserDetails.setAccessibleApis(apiResources);
            return myUserDetails;
        }
    
    }
    

Spring Security賬號密碼認證 + 自定義鑑權,簡單示例完畢 !


^_^ 如有不當之處,歡迎指正

^_^ 參考資料
        《SpringSecurity5.2.1RELEASE源碼》

^_^ 測試代碼託管鏈接
         https://github.com/JustryDeng…SpringSecurity…

^_^ 本文已經被收錄進《程序員成長筆記(六)》,筆者JustryDeng

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章