spring boot 之 spring security 配置

Spring Security簡介

以前項目都是用shiro,可是時過境遷,spring security變得愈來愈流行。spring security的前身是Acegi, acegi 我也玩過,那都是5年的事情了! 現在spring security已經發布了不少個版本,已經到了5.x.x 了。其新功能也增長了很多, 咱們來看看吧!html

spring security實際上是獨立於 spring boot 的。即便是 spring security 的註解, 也跟boot 關係不大, 那都是他們自帶的。可是我這裏仍是把他歸類爲boot,由於我是使用boot來作測試的。 java

 spring security 配置很是靈活,可是正是這種靈活性,是依賴於其底層須要強大的設計,和良好的API 支持, 基本是把複雜性包裝了在底層。 spring security提供了所謂的流式API, 也就是能夠經過點(.)符號,連續的進行配置。固然,這裏的流式API跟java8的流式API仍是不一樣的。git

若是咱們運行官方example,咱們發現,挺好的啊, 原來 spring security 這麼靈活易用啊! 靈活是沒錯的,可是是否易用就看狀況了, 對熟悉的人,固然易用。對於新手,其實到處是坑! 由於不熟悉的話,基本上只能複製,可是一改動那麼就發現各類問題。github

官方示例

怎麼配置就很少說了, 網上大把資料。對於security的form 登陸的配置,官方標準關鍵部分是這樣的:web

import org.springframework.beans.factory.annotation.Autowired;
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.core.userdetails.User;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // @formatter:off
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }
    // @formatter:on

    // @formatter:off
    @Autowired
    public void configureGlobal(
            AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER"));
    }
    // @formatter:on
}

thymeleaf 模板:正則表達式

<html xmlns:th="http://www.thymeleaf.org">
    <head th:include="layout :: head(title=~{::title},links=~{})">
        <title>Please Login</title>
    </head>
    <body th:include="layout :: body" th:with="content=~{::content}">
        <div th:fragment="content">
            <form name="f" th:action="@{/login}" method="post">
                <fieldset>
                    <legend>Please Login</legend>
                    <div th:if="${param.error}" class="alert alert-error">Invalid
                        username and password.</div>
                    <div th:if="${param.logout}" class="alert alert-success">You
                        have been logged out.</div>
                    <label for="username">Username</label> <input type="text"
                        id="username" name="username" /> <label for="password">Password</label>
                    <input type="password" id="password" name="password" /> <label
                        for="remember-me">Remember Me?</label> <input type="checkbox"
                        id="remember-me" name="remember-me" />
                    <div class="form-actions">
                        <button type="submit" class="btn">Log in</button>
                    </div>
                </fieldset>
            </form>
        </div>
    </body>
</html>

 官方github還有不少示例,咱們能夠都拉下來看看。但我本文的意圖是解釋下 spring security的如何配置,以及其各類坑。spring

 

關鍵概念和API

配置的關鍵莫過於HttpSecurity ,WebSecurity 和AuthenticationManagerBuilder。關鍵中的關鍵是HttpSecurity , 其關鍵api 有:數據庫

servletApi 配置SecurityContextHolderAwareRequestFilter
anonymous 匿名登陸控制
cors 增長CorsFilter,提供 跨域資源共享( CORS )機制。它容許 Web 應用服務器進行跨域訪問控制。 這個和 crsf 長得有些像
logout 登出配置
openidLogin 增長OpenIDAuthenticationFilter ,配置外部openid 服務器
addFilterBefore
addFilter
mvcMatcher
exceptionHandling 增長ExceptionTranslationFilter,對認證受權等異常進行處理
formLogin form登入認證配置
sessionManagement 配置session 管理
antMatcher
requiresChannel 增長ChannelProcessingFilter過濾器,也就是安全通道,和https 相關
requestMatcher
userDetailsService 設置用戶詳情數據
setSharedObject 全局共享數據配置
httpBasic httpBasic基礎認證
portMapper 端口映射
authorizeRequests
authenticationProvider 設置authentication提供者
securityContext
rememberMe 增長RememberMeAuthenticationProvider過濾器配置
csrf 增長CsrfFilter過濾器,防止csrf攻擊
requestMatchers
regexMatcher
headers 增長HeaderWriterFilter過濾器, 其餘它並非攔截過濾做用。而是將一些請求頭寫到response 響應頭中
requestCache 增長RequestCacheAwareFilter過濾器,對request 進行緩存處理
addFilterAt
addFilterAfter
x509 增長X509AuthenticationFilter過濾器,提供x509 認證支持:從X509證書中提取用戶名等
jee 增長J2eePreAuthenticatedProcessingFilter過濾器,提供j2ee 認證支持apache

 (大體說明下關鍵的api,而忽略簡單api)api

其中 antMatcher requestMatchers regexMatcher 功能是相似的。 默認httpsecurity 攔截全部的請求, 若是配置了這個以後,那麼以後攔截指定的 url, 它和authorizeRequests 返回的ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry 提供的antMatcher 等類似方法的做用是不太同樣的, 必定不能搞混! 這個配置呢,經常用於配置多個 httpSecurity,具體參見官方文檔。

 

可見,關鍵在於使用 HttpSecurity 這個API, 我找到一份中文說明,可是又迷失在了茫茫的網絡之中了。

 

它提供了不少的接口,簡單說一下個人理解:

 

authorizeRequests 返回一個ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry  而後咱們就能夠進行對各類url 的進行權限配置。 注意, 它是配置權限的。requestMatchers 配置一個request Mather數組,參數爲RequestMatcher 對象,其match 規則自定義。

antMatchers 配置一個request Mather 的 string數組,參數爲 ant 路徑格式, 直接匹配url。

mvcMatchers 同上,可是用於匹配 @RequestMapping 的value

regexMatchers 同上,可是爲正則表達式

anyRequest 匹配任意url,無參。 

上面的各個url 匹配方法都還有一個重載的對一個request method 參數的方法。 注意 這些url 是有順序的,這個順序就是他們出現的順序,必定不要搞錯。因此anyRequest 最好是配置到最後面,不然就容易踩坑了。

配置url 匹配規則的同時, 咱們就能夠配置其權限,常見的有:

hasAnyRole 是否有(參數數組中的)任一角色
hasRole 是否有某個角色
hasAuthority 是否有某個權限
hasAnyAuthority 是否有(參數數組中的)任一權限
hasIpAddress ip是否匹配參數
permitAll 容許全部狀況,即至關於沒作任何security限制
denyAll 拒絕全部狀況。 這狀況比較奇怪, 若是拒絕全部狀況的話, 那的存在有什麼意義?
anonymous 能夠以匿名身份登陸
authenticated 必需要進行身份驗證
fullyAuthenticated 進行嚴格身份驗證,即不能使用緩存/cookie之類的
rememberMe 能夠cookie 登陸?

這些個權限,其實還好理解,暫很少說。後文再分析。

 

開始登入 login 

spring security提供了不少種登陸的方式, 常見的有 基於Authentication請求頭的httpBasic, 基於表單 的 formLogin。 前者是比較少用的,我主要分析下formLogin,formLogin 是提供了一個 FormLoginConfigurer ,其能夠配置的部分爲:

loginPage 若是是表單登陸,那麼至少要一個loginpage 吧,可是這個也不是必須的,若是不配置這個,那麼系統自動生成一個。
usernameParameter 
passwordParameter
failureForwardUrl 登陸失敗後就回forward到參數指定的url, 這個url
successForwardUrl 登陸成功後forward到參數指定的url 

父類AbstractAuthenticationFilterConfigurer提供的配置的:

permitAll 容許loginPage, loginProcessingUrl, failureUrl 被任何狀況訪問到

defaultSuccessUrl 登陸成功後默認的url

loginProcessingUrl form 表單應該提交 security框架能夠處理的url, 默認是/login
failureHandler 登陸失敗處理器
successHandler 登陸成功處理器
failureUrl 登陸失敗系統轉向的url ,默認是
this.loginPage + "?error"。這個有些坑,由於他默認是沒有權限的! 咱們必須給它額外配置一個適當的登陸權限。不然是跳轉不過去的。由於會跳轉到登陸頁面

authenticationDetailsSource

上面的配置大部分是不能重複配置的。固然,也許咱們能夠設置多個相同配置,可是其實只有最後一個生效。除此以外,部分功能相近的配置會有覆蓋效果。好比 若是配置了failureHandler ,那麼 failureUrl 配置就失效了。 successHandler 也是這樣。failureHandler 和 failureUrl 是 last config win。

上面的配置都是能夠不用配置的,由於他們都有默認值,具體哪些默認值就不說了。須要注意的是 successForwardUrl,若是不配置,那麼登陸成功後默認就跳轉到 orgiin url , 也就是被跳轉至loginPage 前 咱們嘗試訪問的那個 url。

另外, defaultSuccessUrl的意思有些難以理解,我至今有些疑問,它和successForwardUrl 的區別是? 

 

開始登出 logout

登出比較簡單點,咱們使用 httpSecurity提供的 logout()方法便可。它返回一個LogoutConfigurer ,主要的配置有:

addLogoutHandler 增長登出處理器,它和logoutSuccessHandler的區別是它不能forward或redirect request, 可是logoutSuccessHandler能夠並且是應該的。
clearAuthentication 
invalidateHttpSession 
logoutUrl 登出url , 默認是/logout, 它能夠是一個ant path url 
logoutRequestMatcher 登出url matcher。這個比較有意思,它讓咱們能夠靈活配置logoutUrl 。 若是說logoutUrl 只是一個ant path url 的話,那麼它就能夠是多個RequestMatcher。
logoutSuccessUrl 登出成功後跳轉的 url
permitAll 容許  logoutSuccessUrl logoutUrl  和logoutRequestMatcher 
deleteCookies 刪除cookie
logoutSuccessHandler 登出成功處理器,設置後會把logoutSuccessUrl  置爲null
defaultLogoutSuccessHandlerFor 只有logoutSuccessHandler爲null 的時候,它纔會生效。
permitAll 容許全部

 

其中defaultLogoutSuccessHandlerFor  是比較難理解的,它的定義是這樣的:

    public LogoutConfigurer<H> defaultLogoutSuccessHandlerFor(LogoutSuccessHandler handler, RequestMatcher preferredMatcher) {
        Assert.notNull(handler, "handler cannot be null");
        Assert.notNull(preferredMatcher, "preferredMatcher cannot be null");
        this.defaultLogoutSuccessHandlerMappings.put(preferredMatcher, handler);
        return this;
    }

其實就是被添加到了一個map,它能夠配置屢次。 我理解是,它應該是和logoutRequestMatcher 配合使用的。 logoutRequestMatcher能夠配置多個logout url, defaultLogoutSuccessHandlerFor 恰好能夠對那些url 再次作個匹配, 匹配成功後執行對應的LogoutSuccessHandler。若是匹配不到,那麼就直接 SimpleUrlLogoutSuccessHandler 直接 sendRedirect 到 logoutSuccessUrl 

 clearAuthentication invalidateHttpSession 我暫時不太理解。 測試過, 沒有達到預期, 多是理解錯誤。

 

記住我 remember-me

 咱們經常須要作登陸入口給提供一個「記住我"的checkbox,以方便用戶下次登陸,將用戶名直接顯示在登陸框,密碼顯示在密碼框,而後咱們能夠再也不輸入用戶密碼了! 可是, security的remember-me 功能好像不是這個做用, 我也是醉了!

具體用法是,

1 先在form 表單增長以下內容:

<input type="checkbox" name="remember-me" />

注意,這裏的name ,必須是remember-me,而不能是rememberMe,或其餘之類的。 不然就不會生效!

2 而後配置一個 UserDetailsService,配置 uds 有多種方式, 以下:

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            UserDetails userDetails = User.withUsername("admin").password("admin").roles("user").build();
            UserDetailsService userDetailsService = new InMemoryUserDetailsManager(Collections.singleton(userDetails));// 簡單起見,這裏使用內存方式
            auth.userDetailsService(userDetailsService);
        }

另外,咱們還能夠經過@Bean方式來配置。 至於爲何須要一個uds, 那是由於, security須要調用 它的 loadUserByUsername 方法, 而後返回一個Authentication , 而後就能夠不用輸入用戶密碼了。 

3 而後httpsecurity的最後:

                    .and()
                    .rememberMe() // 這個做用是配置一個 RememberMeAuthenticationFilter ,必須

 

官方的說法是:

Remember-me or persistent-login authentication refers to web sites being able to remember the identity of a principal between sessions. This is typically accomplished by sending a cookie to the browser, with the cookie being detected during future sessions and causing automated login to take place. Spring Security provides the necessary hooks for these operations to take place, and has two concrete remember-me implementations. One uses hashing to preserve the security of cookie-based tokens and the other uses a database or other persistent storage mechanism to store the generated tokens.

Note that both implementations require a UserDetailsService. If you are using an authentication provider which doesn’t use a UserDetailsService (for example, the LDAP provider) then it won’t work unless you also have a UserDetailsService bean in your application context.

個人理解, form登陸的時候,若是咱們提供了一個名叫remember-me 的參數,並且若是配置了RememberMeAuthenticationFilter , 那麼這個filter 就會嘗試自動登錄autoLogin, 。具體來講, 若是登陸成功,那麼AbstractAuthenticationProcessingFilter 會調用RememberMeServices 的loginSuccess 方法, 而後 將successfulAuthentication相關信息組裝成一個 cookie ,寫到瀏覽器。cookie的名字是 remember-me, 默認和那個form 的參數名字是同樣的, 它的時間默認是 14天。 而後, 咱們下次登陸的時候, security 先從 request中獲取名爲 remember-me 的cookie, 而後decode 它 爲一個數組, 提取user name, 而後經過UserDetailsService  獲取用戶信息, 獲取以後在比對下數組的第二部分是否一致。比對的時候,還有些麻煩。若是是 TokenBasedRememberMeServices ,那麼須要先獲取UserDetails,而後從新計算 expectedTokenSignature ; 若是是PersistentTokenBasedRememberMeServices, 它須要一個PersistentTokenRepository, 有兩個實現,要麼是 從內存: InMemoryTokenRepositoryImpl (默認) 或者 數據庫 JdbcTokenRepositoryImpl 中讀取。 若是是jdbc ,那麼表是 必須persistent_logins 。 

 

坑爹的是, 若是咱們經過logout  註銷。那麼這個cookie 就被刪除了。 因而乎, 我試過,這個remember-me的做用僅限於 不logout  而後關閉瀏覽器, 而後確實能夠不用輸入用戶名密碼。可是, 除此以外的做用不大... 

 

此處參考:

http://www.cnblogs.com/yjmyzz/p/remember-me-sample-in-spring-security3.html

http://www.cnblogs.com/fenglan/p/5913324.html

若是沒有配置 UserDetailsService,那麼:

java.lang.IllegalStateException: UserDetailsService is required.
    at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$UserDetailsServiceDelegator.loadUserByUsername(WebSecurityConfigurerAdapter.java:455) ~[spring-security-config-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.onLoginSuccess(TokenBasedRememberMeServices.java:182) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.loginSuccess(AbstractRememberMeServices.java:294) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:318) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:240) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    

 

另外, 調試登陸的時候, 咱們能夠發現大體有哪些過濾器:

originalChain = {ApplicationFilterChain@6975} 
 filters = {ApplicationFilterConfig[10]@7357} 
  0 = {ApplicationFilterConfig@7359} "ApplicationFilterConfig[name=metricsFilter, filterClass=org.springframework.boot.actuate.autoconfigure.MetricsFilter]"
  1 = {ApplicationFilterConfig@7360} "ApplicationFilterConfig[name=characterEncodingFilter, filterClass=org.springframework.boot.web.filter.OrderedCharacterEncodingFilter]"
  2 = {ApplicationFilterConfig@7361} "ApplicationFilterConfig[name=hiddenHttpMethodFilter, filterClass=org.springframework.boot.web.filter.OrderedHiddenHttpMethodFilter]"
  3 = {ApplicationFilterConfig@7362} "ApplicationFilterConfig[name=httpPutFormContentFilter, filterClass=org.springframework.boot.web.filter.OrderedHttpPutFormContentFilter]"
  4 = {ApplicationFilterConfig@7363} "ApplicationFilterConfig[name=requestContextFilter, filterClass=org.springframework.boot.web.filter.OrderedRequestContextFilter]"
  5 = {ApplicationFilterConfig@7364} "ApplicationFilterConfig[name=springSecurityFilterChain, filterClass=org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1]"
  6 = {ApplicationFilterConfig@7365} "ApplicationFilterConfig[name=webRequestLoggingFilter, filterClass=org.springframework.boot.actuate.trace.WebRequestTraceFilter]"
  7 = {ApplicationFilterConfig@7366} "ApplicationFilterConfig[name=oauth2ClientContextFilter, filterClass=org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter]"
  8 = {ApplicationFilterConfig@7367} "ApplicationFilterConfig[name=applicationContextIdFilter, filterClass=org.springframework.boot.web.filter.ApplicationContextHeaderFilter]"
  9 = {ApplicationFilterConfig@7368} "ApplicationFilterConfig[name=Tomcat WebSocket (JSR356) Filter, filterClass=org.apache.tomcat.websocket.server.WsFilter]"
 pos = 6
 n = 10
 servlet = {DispatcherServlet@7358} 
 servletSupportsAsync = true
additionalFilters = {ArrayList@7045}  size = 13
 0 = {WebAsyncManagerIntegrationFilter@6972} 
 1 = {SecurityContextPersistenceFilter@6971} 
 2 = {HeaderWriterFilter@6970} 
 3 = {CsrfFilter@6958} 
 4 = {LogoutFilter@6957} 
 5 = {UsernamePasswordAuthenticationFilter@6956} 
 6 = {RequestCacheAwareFilter@6955} 
 7 = {SecurityContextHolderAwareRequestFilter@6954} 
 8 = {RememberMeAuthenticationFilter@6948} 
 9 = {AnonymousAuthenticationFilter@7163} 
 10 = {SessionManagementFilter@7387} 
 11 = {ExceptionTranslationFilter@7388} 
 12 = {FilterSecurityInterceptor@7389} 

 

 

 

參考:

https://docs.spring.io/spring-security/site/docs/5.0.0.RELEASE/reference/htmlsingle/

http://www.cnblogs.com/softidea/p/6243200.html

http://www.cnblogs.com/davidwang456/p/4549344.html

相關文章
相關標籤/搜索