spring boot 之 spring security 配置


Spring Security簡介

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

spring security其實是獨立於 spring boot 的。即使是 spring security 的注解, 也跟boot 關系不大, 那都是他們自帶的。但是我這里還是把他歸類為boot,因為我是使用boot來做測試的。 

 spring security 配置非常靈活,但是正是這種靈活性,是依賴於其底層需要強大的設計,和良好的API 支持, 基本是把復雜性包裝了在底層。 spring security提供了所謂的流式API, 也就是可以通過點(.)符號,連續的進行配置。當然,這里的流式API跟java8的流式API還是不同的。

如果我們運行官方example,我們發現,挺好的啊, 原來 spring security 這么靈活易用啊! 靈活是沒錯的,但是是否易用就看情況了, 對熟悉的人,當然易用。對於新手,其實處處是坑! 因為不熟悉的話,基本上只能復制,但是一改動那么就發現各種問題。

官方示例

怎么配置就不多說了, 網上大把資料。對於security的form 登錄的配置,官方標准關鍵部分是這樣的:

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的如何配置,以及其各種坑。

 

關鍵概念和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 認證支持

 (大致說明下關鍵的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


免責聲明!

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



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