《Spring實戰》-第九章:Spring Web應用安全(Spring Security)

慢慢來比較快,虛心學技術

安全性是絕大多數應用系統中的一個重要切面( aspect ),之所以說是切面,是因爲安全性是超越應用程序功能的一個關注點。應用系統的絕大部分內容都不應該參與到與自己相關的安全性處理中。儘管我們可以直接在應用程序中編寫安全性功能相關的代碼(這種情況並不少見),但更好的方式還是將安全性相關的關注點與應用程序本身的關注點進行分離

一、什麼是Spring Security?

一種基於 Spring AOP 和 Servlet 規範中的 Filter 實現的安全框架。 Spring Security 提供了完整的安全性解決方案,它能夠在 Web 請求級別和方法調用級別處理身份認證和授權

Spring Security 從兩個角度來解決安全性問題。

  • 它使用 Servlet 規範中的 Filter 保護 Web 請求並限制 URL 級別的訪問。
  • Spring Security 還能夠使用 Spring AOP 保護方法調用 —— 藉助於對象代理和使用通知,能夠確保只有具備適當權限的用戶才能訪問安全保護的方法

Spring Security的核心是 用戶認證(Authentication)和用戶授權(Authorization)

二、Spring Security基本組成

Spring Security 被分成了 11 個模塊
ACL:支持通過訪問控制列表( access control list , ACL )爲域對象提供安全性
切面( Aspects ):一個很小的模塊,當使用 Spring Security 註解時,會使用基於 AspectJ 的切面,而不是使用標準的 Spring AOP
CAS 客戶端( CAS Client ):提供與 Jasig 的中心認證服務( Central Authentication Service , CAS )進行集成的功能
配置( Configuration ):包含通過 XML 和 Java 配置 Spring Security 的功能支持
核心( Core ):提供 Spring Security 基本庫
加密( Cryptography ):提供了加密和密碼編碼的功能
LDAP:支持基於 LDAP 進行認證
OpenID:支持使用 OpenID 進行集中式認證
Remoting:提供了對 Spring Remoting 的支持
標籤庫( Tag Library ):Spring Security 的 JSP 標籤庫
Web:提供了 Spring Security 基於 Filter 的 Web 安全性支持

一個基本的Spring Security應用至少包括Core和Configuration模塊,當涉及到Web應用時,還需要包含Web模塊。

三、Spring Security使用

pom文件中引入基本的Spring Security及Spring MVC所需要的包

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
    <spring.security.version>5.1.3.RELEASE</spring.security.version>
</properties>

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>

    <!--引入Servlet支持-->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope>
    </dependency>

    <!-- jstl -->
    <dependency>
        <groupId>jstl</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
    </dependency>

    <!--個人封裝的一個模塊,可有可無,不影響功能實現-->
    <dependency>
        <groupId>com.my.spring</groupId>
        <artifactId>com.m.spring.common</artifactId>
    </dependency>

    <!--引入Spring支持-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${org.springframework.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${org.springframework.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${org.springframework.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>${org.springframework.version}</version>
    </dependency>

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.13</version>
    </dependency>

    <!--引入Spring MVC支持-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${org.springframework.version}</version>
    </dependency>

    <!--引入Spring Security支持-->
    <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-core</artifactId>
        <version>${spring.security.version}</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-web -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>${spring.security.version}</version>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-config -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>${spring.security.version}</version>
    </dependency>
</dependencies>

①配置SpringMVC默認DispatcherServlet

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

    /*AbstractAnnotationConfigDispatcherServletInitializer 會同時創
    建 DispatcherServlet 和 ContextLoaderListener 。 GetServlet-ConfigClasses() 方法返回的帶有 @Configuration 註解的
    類將會用來定義 DispatcherServlet 應用上下文中的 bean 。 getRootConfigClasses() 方法返回的帶有 @Configuration 註解的類將
    會用來配置 ContextLoaderListener 創建的應用上下文中的 bean 。*/

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{RootConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        logger.debug("DispatcherServlet獲取匹配的前端控制器。。。。。。");
        return new String[]{"/"};
    }
}

②創建Spring Security的DelegatingFilterProxy,自動加載springSecurityFilterChain

/**
 *  攔截髮往應用中的請求,並將請求委託給 ID 爲 springSecurityFilterChain的bean
 *  springSecurityFilterChain 本身是另一個特殊的 Filter,它也被稱爲 FilterChainProxy.它可以鏈接任意一個或多個其他的 Filter。
 *  Spring Security 依賴一系列 Servlet Filter 來提供不同的安全特性。
 **/
public class SpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer {}

③配置SpringMVC的根配置和Web配置

@Configuration
@ComponentScan(basePackages ={"com.my.spring"},excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = {EnableWebMvc.class})})
public class RootConfig {
}

@EnableWebMvc
@Configuration
@ComponentScan(basePackages = {"com.my.spring.controller"})
@Import(SecurityConfig.class)//引入Spring Security的配置
public class WebConfig extends WebMvcConfigurationSupport {

    /**
     * 定義一個視圖解析器
     *
     * @return org.springframework.web.servlet.ViewResolver
     *
     * @author lai.guanfu 2019/3/5
     * @version 1.0
     **/
    @Bean
    public ViewResolver viewResolver(){
        InternalResourceViewResolver resourceViewResolver = new InternalResourceViewResolver();
        resourceViewResolver.setPrefix("/WEB-INF/view/");
        resourceViewResolver.setSuffix(".jsp");
        resourceViewResolver.setExposeContextBeansAsAttributes(true);
        resourceViewResolver.setViewClass(JstlView.class);
        return resourceViewResolver;
    }

    @Override
    protected void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

④SecurityConfig.java:開啓Spring Security並配置基本攔截和用戶信息,增加兩個用戶

@Configuration
@EnableWebSecurity//啓用Spring Security
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 設置攔截路徑
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //配置攔截路徑以及認證通過的身份,此處攔截任意/admin/**路徑,必須以ADMIN身份登錄
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
                //配置攔截路徑以及認證通過的身份,此處指定只有訪問/dba/**的GET請求會被攔截認證,必須以DBA或者ADMIN的身份登錄
                .antMatchers(HttpMethod.GET,"/dba/**").access("hasAnyRole('ROLE_DBA','ROLE_ADMIN')")
                //表明除了上述路徑需要攔截認證外,其餘路徑全部不進行認證
                .anyRequest().permitAll()
                //add()方法用於連接各種配置指令
                .and() 
                //當重寫configure(HttpSecurity http)方法後,將失去Spring Security的默認登錄頁,可以使用formLogin()重新啓用
                .formLogin();
    }

    /**
     * 使用內存設置基本人物信息
     * @param auth
     * @throws Exception
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        //使用內存添加用戶名及登陸密碼和身份,使用指定編碼器對密碼進行編碼
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN");
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("dba").password(new BCryptPasswordEncoder().encode("123456")).roles("DBA");

        //Spring Security 5.0之前的寫法,5.0之後,如果沒有指定密碼編碼器,將會報There is no PasswordEncoder mapped for the id "null"的錯
        /*auth.inMemoryAuthentication().withUser("admin").password("123456").roles("ADMIN");
        auth.inMemoryAuthentication().withUser("dba").password("123456").roles("DBA");*/
    }
}

⑤編寫controller方法

@Controller
public class MyController{

    //不需要權限的路徑
    @RequestMapping(value = {"/","/welcome**"},method = RequestMethod.GET)
    public ModelAndView toWelcomePage(){
        ModelAndView model = new ModelAndView();
        model.addObject("title","Spring Security Welcome Page!!");
        model.addObject("message","Hello World");
        model.setViewName("welcome");
        return model;
    }

    //需要ADMIN角色權限
    @RequestMapping(value = {"/admin**"},method = RequestMethod.GET)
    public ModelAndView toAdminPage(){
        ModelAndView model = new ModelAndView();
        model.addObject("title","Spring Security Admin Page!!");
        model.addObject("message","Hello World");
        model.setViewName("admin");
        return model;
    }

    //需要DBA或者ADMIN權限
    @RequestMapping(value = {"/dba**"},method = RequestMethod.GET)
    public ModelAndView toDBAPage(){
        ModelAndView model = new ModelAndView();
        model.addObject("title","Spring Security DBA Admin Page!!");
        model.addObject("message","Hello World");
        model.setViewName("dba");
        return model;
    }
}

⑥編寫視圖:

welcome.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page isELIgnored="false" %>
<html>
<head>
    <title>歡迎頁</title>
</head>
<body>
    <h1><c:out value="${title}"></c:out></h1>
    <hr>
    <h3><c:out value="${message}"></c:out></h3>
</body>
</html>

admin.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page isELIgnored="false" %>
<html>
<head>
    <title>管理員頁面</title>
</head>
<body>
<h1>Title : ${title}</h1>
<h1>Message : ${message}</h1>

<!--獲取用戶信息-->
<c:if test="${pageContext.request.userPrincipal.name != null}">
    <h2>Welcome : ${pageContext.request.userPrincipal.name}
        | <a href="<c:url value="/logout" />" > Logout</a></h2>
</c:if>
</body>
</html>

dba.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page isELIgnored="false" %>
<html>
<head>
    <h1>Title : ${title}</h1>
    <h1>Message : ${message}</h1>

    <c:if test="${pageContext.request.userPrincipal.name != null}">
        <h2>Welcome : ${pageContext.request.userPrincipal.name}
            | <a href="<c:url value="/logout" />" > Logout</a></h2>
    </c:if>
</head>
<body>

</body>
</html>

啓動服務器,瀏覽器校驗如下:

訪問welcome,無登陸身份狀態可訪問


訪問/admin,自動跳轉到Spring Security提供的登錄頁


輸入錯誤登錄名或密碼時會顯示錯誤

使用對/admin無權限的用戶登錄,會顯示403頁面


使用admin進行登陸,正常進入並獲取到登陸用戶的信息

訪問/dba,從上面代碼中,我們知道,對於/dba路徑的請求,使用ADMIN和DBA角色訪問都是允許的,

測試完成,那麼,Spring Security究竟是怎麼實現攔截的呢?

四、Spring Security 原理分析

實際上,Spring Security通過一層層基於Servlet的過濾器Filter對請求和方法調用的攔截過濾,從而實現用戶身份驗證和用戶授權。

我們先來分析一下Speing Security的運轉流程:

可以看到,Speing Security的核心組件是一個名爲DelegatingFilterProxy的過濾器,它將一系列的過濾器集合成FilterChain鏈條,進行層層過濾實現用戶身份認證及授權。

從Demo代碼中可以看到Spring Security的幾個核心類和接口以及註解,以下一一分析:

DelegatingFilterProxy

public class DelegatingFilterProxy extends GenericFilterBean {
    。。。
    //根據名稱創建一個過濾器實例
    public DelegatingFilterProxy(String targetBeanName) {
       this(targetBeanName, null);
    }

    public DelegatingFilterProxy(String targetBeanName, @Nullable WebApplicationContext wac) {
       Assert.hasText(targetBeanName, "Target Filter bean name must not be null or empty");
       this.setTargetBeanName(targetBeanName);
       this.webApplicationContext = wac;
       if (wac != null) {
          this.setEnvironment(wac.getEnvironment());
       }
    }

    //初始化(獲取過濾器實例)
    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
       String targetBeanName = getTargetBeanName();
       Assert.state(targetBeanName != null, "No target bean name set");
       Filter delegate = wac.getBean(targetBeanName, Filter.class);
       if (isTargetFilterLifecycle()) {
          delegate.init(getFilterConfig());
       }
       return delegate;
    }

    //進入過濾操作
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
          throws ServletException, IOException {

       // Lazily initialize the delegate if necessary.------delegate判空填充操作
       Filter delegateToUse = this.delegate;
       if (delegateToUse == null) {
          synchronized (this.delegateMonitor) {
             delegateToUse = this.delegate;
             if (delegateToUse == null) {
                WebApplicationContext wac = findWebApplicationContext();
                if (wac == null) {
                   throw new IllegalStateException("No WebApplicationContext found: " +
                         "no ContextLoaderListener or DispatcherServlet registered?");
                }
                delegateToUse = initDelegate(wac);
             }
             this.delegate = delegateToUse;
          }
       }

       // Let the delegate perform the actual doFilter operation.執行真正的過濾
       invokeDelegate(delegateToUse, request, response, filterChain);
    }

    //真正執行過濾
    protected void invokeDelegate(
      Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
        //執行目標過濾方法,而不是本類內的過濾方法,相當於將過濾委派給實例
       delegate.doFilter(request, response, filterChain);
    }
}

可以看到,DelegatingFilterProxy實際上就是一個Filter,只不過他並不會直接執行過濾操作,而是將過濾操作委託給過濾鏈條

AbstractSecurityWebApplicationInitializer

關鍵源代碼如下:

public abstract class AbstractSecurityWebApplicationInitializer
      implements WebApplicationInitializer {

   private static final String SERVLET_CONTEXT_PREFIX = "org.springframework.web.servlet.FrameworkServlet.CONTEXT.";

   public static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain";

   private final Class<?>[] configurationClasses;

   protected AbstractSecurityWebApplicationInitializer() {
      this.configurationClasses = null;
   }

   protected AbstractSecurityWebApplicationInitializer(
         Class<?>... configurationClasses) {
      this.configurationClasses = configurationClasses;
   }

    //初始執行,生成名爲springSecurityFilterChain的Bean,是一個DelegatingFilterProxy的實例
   public final void onStartup(ServletContext servletContext) throws ServletException {
      beforeSpringSecurityFilterChain(servletContext);
      if (this.configurationClasses != null) {
         AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
         rootAppContext.register(this.configurationClasses);
         servletContext.addListener(new ContextLoaderListener(rootAppContext));
      }
      if (enableHttpSessionEventPublisher()) {
         servletContext.addListener(
               "org.springframework.security.web.session.HttpSessionEventPublisher");
      }
      servletContext.setSessionTrackingModes(getSessionTrackingModes());
      //生成springSecurityFilterChain
      insertSpringSecurityFilterChain(servletContext);
      afterSpringSecurityFilterChain(servletContext);
   }

   /**
    * 註冊springSecurityFilterChain,Spring Security的過濾器鏈條
    * Registers the springSecurityFilterChain 
    */
   private void insertSpringSecurityFilterChain(ServletContext servletContext) {
      String filterName = DEFAULT_FILTER_NAME;
      //關鍵代碼
      DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy(filterName);
      //獲取應用上下文配置中的屬性並置入springSecurityFilterChain中
      String contextAttribute = getWebApplicationContextAttribute();
      if (contextAttribute != null) {
         springSecurityFilterChain.setContextAttribute(contextAttribute);
      }

      //註冊過濾器鏈條
      registerFilter(servletContext, true, filterName, springSecurityFilterChain);
   }

   //註冊過濾器鏈條
   private void registerFilters(ServletContext servletContext,boolean insertBeforeOtherFilters, Filter... filters) {
       Assert.notEmpty(filters, "filters cannot be null or empty");
       for (Filter filter : filters) {
           if (filter == null) {
             throw new IllegalArgumentException("filters cannot contain null values. Got "+ Arrays.asList(filters));
           }
           String filterName = Conventions.getVariableName(filter);
           registerFilter(servletContext, insertBeforeOtherFilters, filterName, filter);
       }
    }

    //註冊過濾器
    private final void registerFilter(ServletContext servletContext,boolean insertBeforeOtherFilters, String filterName, Filter filter) {
       Dynamic registration = servletContext.addFilter(filterName, filter);
       if (registration == null) {
          throw new IllegalStateException("Duplicate Filter registration for '" + filterName+ "'. Check to ensure the Filter is only configured once.");
       }
       registration.setAsyncSupported(isAsyncSecuritySupported());
       EnumSet<DispatcherType> dispatcherTypes = getSecurityDispatcherTypes();
       registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters,
         "/*");
    }

   //供開發者自定義Filter 
   protected final void insertFilters(ServletContext servletContext, Filter... filters) {
      registerFilters(servletContext, true, filters);
   }

   //供開發者自定義Filter 
   protected final void appendFilters(ServletContext servletContext, Filter... filters) {
      registerFilters(servletContext, false, filters);
   }
}

AbstractSecurityWebApplicationInitializer實現了WebWebApplicationInitializer,所以在應用啓動的時候是可以被Spring裝配並進行初始化的,在進行初始化的過程中,AbstractSecurityWebApplicationInitializer讀取配置並生成一個名爲springSecurityFilterChain的Bean,是DelegatingFilterProxy的實例,並將配置中所定義的過濾鏈條進行注入

AbstractSecurityWebApplicationInitializer還提供了兩個方法供開發者自定義過濾器:insertFilters(ServletContext servletContext, Filter... filters)和appendFilters(ServletContext servletContext, Filter... filters),但是一般來說我們不會用到,除非我們希望可以自定義過濾邏輯。

WebSecurityConfigurerAdapter類&&@EnableWebSecurity註解

當創建了一個繼承了AbstractSecurityWebApplicationInitializer的初始化類後,我們只需再創建一個配置類繼承WebSecurityConfigurerAdapter類且標註@EnableWebSecurity註解即可開啓Web 的Security服務了。但是此時的應用是封閉的,因爲沒有配置任何的用戶身份,也沒有配置任何的過濾路徑規則,應用默認封鎖所有路徑。

那麼,應該如何配置用戶身份和過濾器規則呢?

WebSecurityConfigurerAdapter類中有幾個關鍵方法可以重載

configure(HttpSecurity):配置攔截模式
configure(AuthenticationManagerBuilder):配置用戶信息
configure(WebSecurity):配置Spring Security的Filter鏈

在進行配置分析之前,我們需要先了解幾個類:(摘自:https://www.cnblogs.com/xz816111/p/8528896.html

Authentication

是一個接口,用來表示用戶認證信息,在用戶登錄認證之前相關信息會封裝爲一個Authentication具體實現類的對象,在登錄認證成功之後又會生成一個信息更全面,包含用戶權限等信息的Authentication對象,然後把它保存 SecurityContextHolder所持有的SecurityContext中,供後續的程序進行調用,如訪問權限的鑑定等

AuthenticationManager

用來做驗證的最主要的接口爲AuthenticationManager,這個接口只有一個方法:

public interface AuthenticationManager {
  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;
}

其中authenticate()方法運行後可能會有三種情況:

  1. 驗證成功,返回一個帶有用戶信息的Authentication。
  2. 驗證失敗,拋出一個AuthenticationException異常。
  3. 無法判斷,返回null。

ProviderManager

ProviderManager是上面的AuthenticationManager最常見的實現,它不自己處理驗證,而是將驗證委託給其所配置的AuthenticationProvider列表然後會依次調用每一個 AuthenticationProvider進行認證,這個過程中只要有一個AuthenticationProvider驗證成功,就不會再繼續做更多驗證,會直接以該認證結果作爲ProviderManager的認證結果。

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
      InitializingBean {

   private static final Log logger = LogFactory.getLog(ProviderManager.class);

   private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
   private List<AuthenticationProvider> providers = Collections.emptyList();
   protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
   private AuthenticationManager parent;
   private boolean eraseCredentialsAfterAuthentication = true;

   public ProviderManager(List<AuthenticationProvider> providers) {
      this(providers, null);
   }

   public ProviderManager(List<AuthenticationProvider> providers,
         AuthenticationManager parent) {
      Assert.notNull(providers, "providers list cannot be null");
      this.providers = providers;
      this.parent = parent;
      checkState();
   }

   public void afterPropertiesSet() throws Exception {
      checkState();
   }

   private void checkState() {
      if (parent == null && providers.isEmpty()) {
         throw new IllegalArgumentException(
               "A parent AuthenticationManager or a list "
                     + "of AuthenticationProviders is required");
      }
   }

   //執行認證
   public Authentication authenticate(Authentication authentication)
         throws AuthenticationException {
      Class<? extends Authentication> toTest = authentication.getClass();
      AuthenticationException lastException = null;
      AuthenticationException parentException = null;
      Authentication result = null;
      Authentication parentResult = null;
      boolean debug = logger.isDebugEnabled();
      //並不直接驗證,而是調用其provider列表進行驗證,只要有一個驗證通過,則通過
      for (AuthenticationProvider provider : getProviders()) {
         if (!provider.supports(toTest)) {
            continue;
         }

         if (debug) {
            logger.debug("Authentication attempt using "
                  + provider.getClass().getName());
         }

         try {
            result = provider.authenticate(authentication);

            if (result != null) {
               copyDetails(authentication, result);
               break;
            }
         }
         catch (AccountStatusException e) {
            prepareException(e, authentication);
            // SEC-546: Avoid polling additional providers if auth failure is due to
            // invalid account status
            throw e;
         }
         catch (InternalAuthenticationServiceException e) {
            prepareException(e, authentication);
            throw e;
         }
         catch (AuthenticationException e) {
            lastException = e;
         }
      }

      if (result == null && parent != null) {
         // Allow the parent to try.
         try {
            result = parentResult = parent.authenticate(authentication);
         }
         catch (ProviderNotFoundException e) {
            // ignore as we will throw below if no other exception occurred prior to
            // calling parent and the parent
            // may throw ProviderNotFound even though a provider in the child already
            // handled the request
         }
         catch (AuthenticationException e) {
            lastException = parentException = e;
         }
      }

      if (result != null) {
         if (eraseCredentialsAfterAuthentication
               && (result instanceof CredentialsContainer)) {
            // Authentication is complete. Remove credentials and other secret data
            // from authentication
            ((CredentialsContainer) result).eraseCredentials();
         }

         // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
         // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
         if (parentResult == null) {
            eventPublisher.publishAuthenticationSuccess(result);
         }
         return result;
      }

      // Parent was null, or didn't authenticate (or throw an exception).

      if (lastException == null) {
         lastException = new ProviderNotFoundException(messages.getMessage(
               "ProviderManager.providerNotFound",
               new Object[] { toTest.getName() },
               "No AuthenticationProvider found for {0}"));
      }

      // If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
      // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
      if (parentException == null) {
         prepareException(lastException, authentication);
      }

      throw lastException;
   }

}

瞭解完基本的關鍵類,我們開始看一下配置的技巧:

  • 配置用戶信息

兩種方式:

Ⅰ、自定義方法將用戶信息存入內存

/**
 * 使用內存設置基本人物信息
 * @param auth
 * @throws Exception
 */
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
      //使用內存添加用戶名及登陸密碼和身份,使用指定編碼器對密碼進行編碼
      auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN");
      auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("dba").password(new BCryptPasswordEncoder().encode("123456")).roles("DBA");

      //Spring Security 5.0之前的寫法,5.0之後,如果沒有指定密碼編碼器,將會報There is no PasswordEncoder mapped for the id "null"的錯
        /*auth.inMemoryAuthentication().withUser("admin").password("123456").roles("ADMIN");
      auth.inMemoryAuthentication().withUser("dba").password("123456").roles("DBA");*/
}

Ⅱ、重載configure(AuthenticationManagerBuilder)方法

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
    //使用內存添加用戶名及登陸密碼和身份,使用指定編碼器對密碼進行編碼
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN");
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("dba").password(new BCryptPasswordEncoder().encode("123456")).roles("DBA");

    //Spring Security 5.0之前的寫法,5.0之後,如果沒有指定密碼編碼器,將會報There is no PasswordEncoder mapped for the id "null"的錯
    /*auth.inMemoryAuthentication().withUser("admin").password("123456").roles("ADMIN");
    auth.inMemoryAuthentication().withUser("dba").password("123456").roles("DBA");*/
}
  • 配置攔截路徑
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            //配置攔截路徑以及認證通過的身份,此處攔截任意/admin/**路徑,必須以ADMIN身份登錄
            .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
            //配置攔截路徑以及認證通過的身份,此處指定只有訪問/dba/**的GET請求會被攔截認證,可使用DBA和ADMIN身份訪問
            .antMatchers(HttpMethod.GET,"/dba/**").access("hasAnyRole('ROLE_DBA','ROLE_ADMIN')")
            //表明除了上述路徑需要攔截認證外,其餘路徑全部不進行認證
            .anyRequest().permitAll()
            //add()方法用於連接各種配置指令
           .and() 
           //當重寫configure(HttpSecurity http)方法後,將失去Spring Security的默認登錄頁,可以使用formLogin()重新啓用
           .formLogin();
}

其中各關鍵方法源碼如下:

antMatchers()---指定攔截規則

//攔截目標路徑數組
public C antMatchers(String... antPatterns) {
   return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
}

//攔截指定訪問方式的目標路徑
public C antMatchers(HttpMethod method, String... antPatterns) {
   return chainRequestMatchers(RequestMatchers.antMatchers(method, antPatterns));
}

//攔截應用內所有目標訪問方式的請求
public C antMatchers(HttpMethod method) {
   return antMatchers(method, new String[] { "/**" });
}

access()----指定攔截通過的條件

//允許該攔截通過的條件
public ExpressionInterceptUrlRegistry access(String attribute) {
   if (not) {
      attribute = "!" + attribute;
   }
   interceptUrl(requestMatchers, SecurityConfig.createList(attribute));
   return ExpressionUrlAuthorizationConfigurer.this.REGISTRY;
}

hasRole()-----是否具有某個角色身份

//等同於hasRole
public final boolean hasAuthority(String authority) {
   return hasAnyAuthority(authority);
}
//等同於hasAnyRole
public final boolean hasAnyAuthority(String... authorities) {
   return hasAnyAuthorityName(null, authorities);
}

//當前用戶是否擁有指定角色
public final boolean hasRole(String role) {
   return hasAnyRole(role);
}

//多個角色是一個以逗號進行分隔的字符串。如果當前用戶擁有指定角色中的任意一個則返回true
public final boolean hasAnyRole(String... roles) {
   return hasAnyAuthorityName(defaultRolePrefix, roles);
}
  • 自定義登陸頁

我們看到FormLoginConfigurer()的源碼:

public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
   return getOrApply(new FormLoginConfigurer<>());
}

如果不想做過多配置,自定義的登陸頁面應該:

  • form的action應該提交到"/login"
  • 包含username的輸入域且name屬性爲username
  • 包含password的輸入域且name屬性爲password

loginPage.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登陸頁面</title>
</head>
<body>
    <form method="post" action="../login">
        <label>登陸名:</label><input type="text" name="username">
        <label>密碼:</label><input type="password" name="password">
        <button type="submit">提交</button>
    </form>
</body>
</html>

上述配置改爲:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            //配置攔截路徑以及認證通過的身份,此處攔截任意/admin/**路徑,必須以ADMIN身份登錄
            .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
            //配置攔截路徑以及認證通過的身份,此處指定只有訪問/dba/**的GET請求會被攔截認證,可使用DBA和ADMIN身份訪問
            .antMatchers(HttpMethod.GET,"/dba/**").access("hasAnyRole('ROLE_DBA','ROLE_ADMIN')")
            //表明除了上述路徑需要攔截認證外,其餘路徑全部不進行認證
            .anyRequest().permitAll()
            //add()方法用於連接各種配置指令
           .and() 
           //當重寫configure(HttpSecurity http)方法後,將失去Spring Security的默認登錄頁,可以使用formLogin()重新啓用
           .formLogin()
           //將登錄頁指向視圖名爲loginPage的視圖
           .loginPage("/loginPage");
}

創建LoginController作爲視圖指向

@Controller
public class LoginController {

    @RequestMapping("/loginPage")
    public String login(){
        return "loginPage";
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章