Spring Security 進階-細節總結

關於 Spring Security 的學習已經告一段落了,剛開始接觸該安全框架感受很迷茫,總以爲沒有 Shiro 靈活,到後來的深刻學習和探究才發現它很是強大。簡單快速集成,基本不用寫任何代碼,拓展起來也很是靈活和強大。

系統集成

集成完該框架默認狀況下,系統幫咱們生成一個登錄頁,默認除了登錄其餘請求都須要進行身份認證,沒有身份認證前的任何操做都會跳轉到默認登陸頁。
默認生成的密碼也會在控制檯輸出。html

簡單頁面自定義

接下來咱們可能須要本身控制一下權限,自定義一下登陸界面前端

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
        .formLogin()
            .loginPage("/login.html") //自定義登陸界面
            .loginProcessingUrl("/login.action") //指定提交地址
            .defaultSuccessUrl("/main.html") //指定認證成功跳轉界面
            //.failureForwardUrl("/error.html") //指定認證失敗跳轉界面(注: 轉發須要與提交登陸請求方式一致)
            .failureUrl("/error.html") //指定認證失敗跳轉界面(注: 重定向須要對應的方法爲 GET 方式)
            .usernameParameter("username") //username
            .passwordParameter("password") //password
            .permitAll()
            .and()
        .logout()
            .logoutUrl("/logout.action") //指定登出的url, controller裏面不用寫對應的方法
            .logoutSuccessUrl("/login.html") //登出成功跳轉的界面
            .permitAll()
            .and()
        .authorizeRequests()
            .antMatchers("/register*").permitAll() //設置不須要認證的
            .mvcMatchers("/main.html").hasAnyRole("admin")
            .anyRequest().authenticated() //其餘的所有須要認證
            .and()
        .exceptionHandling()
            .accessDeniedPage("/error.html"); //配置權限失敗跳轉界面 (注: url配置不會被springmvc異常處理攔截, 可是註解配置springmvc異常機制能夠攔截到)
}

從上面配置能夠看出自定義配置能夠簡單地分爲四個模塊(登陸頁面自定義、登出自定義、權限指定、異常設定),每一個模塊都對應着一個過濾器,詳情請看 Spring Security 進階-原理篇spring

須要注意的是:數據庫

  • 配置登陸提交的URL loginProcessingUrl(..)、登出URL logoutUrl(..) 都是對應攔截器的匹配地址,會在對應的過濾器裏面執行相應的邏輯,不會執行到 Controller 裏面的方法。
  • 配置的登陸認證成功跳轉的URL defaultSuccessUrl(..)、登陸認證失敗跳轉的URL failureUrl(..)、登陸認證失敗轉發的URL failureForwardUrl(..)......以及下面登出和權限配置的URL 能夠是靜態界面地址,也能夠是 Controller 裏面對應的方法
  • 這裏配置 URL 對應的訪問權限,訪問失敗不會被 SpringMVC 的異常方法攔截到,註解配置的能夠被攔截到。可是咱們最好不要在 SpringMVC 裏面對他進行處理,而是放到配置的權限異常來處理。
  • 登陸身份認證失敗跳轉對應的地址前會把異常保存到 request(轉發) 或 session(重定向) 裏面,能夠經過 key WebAttributes.AUTHENTICATION_EXCEPTION 來取出,可是前提是使用系統提供的身份認證異常處理handler SimpleUrlAuthenticationFailureHandler
  • 上面這種配置身份認證失敗都會跳轉到登陸頁,權限失敗會跳轉指定的 URL,沒有配置 URL 則會響應 403 的異常給前端,前提是在使用系統爲咱們提供的默認權限異常處理handler AccessDeniedHandlerImpl

異步響應配置

大多數開發狀況下都是先後端分離,響應也都是異步的,不是上面那種表單界面的響應方式,雖然經過上面跳轉到URL對應的 Controller 裏面的方法也能解決,可是大多數狀況下咱們須要的是極度簡化,這時候一些自定義的處理 handler 就油然而生。segmentfault

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
        .formLogin()
            .loginProcessingUrl("/login")
            .successHandler(new AuthenticationSuccessHandler() {
                @Override
                public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                    System.out.println("****** 身份認證成功 ******");
                    response.setStatus(HttpStatus.OK.value());
                }
            })
            .failureHandler(new AuthenticationFailureHandler() {
                @Override
                public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                    System.out.println("****** 身份認證失敗 ******");
                    response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
                }
            })
            .permitAll()
            .and()
        .logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(new LogoutSuccessHandler() {
                @Override
                public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                    System.out.println("****** 登出成功 ******");
                    response.setStatus(HttpStatus.OK.value());
                }
            })
            .permitAll()
            .and()
        .authorizeRequests()
            .antMatchers("/main").hasAnyRole("admin")
            .and()
        .exceptionHandling()
            .authenticationEntryPoint(new AuthenticationEntryPoint() {
                @Override
                public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                    System.out.println("****** 沒有進行身份認證 ******");
                    response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
                }
            })
            .accessDeniedHandler(new AccessDeniedHandler() {
                @Override
                public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                    System.out.println("****** 沒有權限 ******");
                    response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
                }
            });
}

注意:後端

  • 沒有指定登陸界面,那麼久須要至少配置兩個 handler,登出失敗 hander logoutSuccessHandler(..),登陸身份認證失敗的 handler failureHandler(..),以避免默認這樣兩個步驟向不存在的登陸頁跳轉。
  • 配置的登陸身份認證失敗 handler failureHandler(..) 和 沒有進行身份認證的異常 handler authenticationEntryPoint(..),這兩個有區別,前者是在認證過程當中出現異常處理,後者是在訪問須要進行身份認證的URL時沒有進行身份認證異常處理。

自定義身份認證過程

開發的時候咱們須要本身來實現登陸登出的流程,下面來個最簡單的自定義。安全

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
        .logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(new LogoutSuccessHandler() {
                @Override
                public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                    System.out.println("****** 登出成功 ******");
                    response.setStatus(HttpStatus.OK.value());
                }
            })
            .permitAll()
            .and()
        .authorizeRequests()
            .antMatchers("/main").hasAnyRole("admin")
            .and()
        .exceptionHandling()
            .authenticationEntryPoint(new AuthenticationEntryPoint() {
                @Override
                public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                    System.out.println("****** 沒有進行身份認證 ******");
                    response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
                }
            })
            .accessDeniedHandler(new AccessDeniedHandler() {
                @Override
                public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                    System.out.println("****** 沒有權限 ******");
                    response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
                }
            })
            .and()
        .addFilterBefore(new LoginFilter(), UsernamePasswordAuthenticationFilter.class);
}

注意:session

  • 這裏配置了登出,也能夠不配置,在自定義登出的 Controller 方法裏面進行手動清空 SecurityContextHolder.clearContext();,可是建議配置,通常登陸和登出最好都在過濾器裏面進行處理。
  • 添加自定義登陸過濾器,至關於配置登陸。
  • 記得配置登陸認證前和過程當中的一些請求不須要身份認證。

自定義登陸過濾器詳情mvc

public class LoginFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        if ("/login".equals(httpServletRequest.getServletPath())) { //開始登陸過程

            String username = httpServletRequest.getParameter("username");
            String password = httpServletRequest.getParameter("password");
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, password);

            //模擬數據庫查出來的
            User.UserBuilder userBuilder = User.withUsername(username);
            userBuilder.password("123");
            userBuilder.roles("user", "admin");
            UserDetails user = userBuilder.build();

            if (user == null) {
                System.out.println("****** 自定義登陸過濾器 該用戶不存在 ******");
                httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
            }
            if (!user.getUsername().equals(authentication.getPrincipal())) {
                System.out.println("****** 自定義登陸過濾器 帳號有問題 ******");
                httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
            }
            if (!user.getPassword().equals(authentication.getCredentials())) {
                System.out.println("****** 自定義登陸過濾器 密碼有問題 ******");
                httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
            }

            UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(user.getUsername(), authentication.getCredentials(), user.getAuthorities());
            result.setDetails(authentication.getDetails());

            //注: 最重要的一步
            SecurityContextHolder.getContext().setAuthentication(result);

            httpServletResponse.setStatus(HttpStatus.OK.value());
        } else  {
            chain.doFilter(request, response);
        }
    }
}

注意:框架

  • 不是登陸認證就接着執行下一個過濾器或其餘。
  • 登陸認證失敗不能直接拋出錯誤,須要向前端響應異常。
  • 完成登陸邏輯直接響應,不須要接着往下執行什麼。
相關文章
相關標籤/搜索