spring security之web應用安全

1、什麼是web應用安全,爲了安全咱們要作哪些事情?

保護web資源不受侵害(資源:用戶信息、用戶財產、web數據信息等)
對訪問者的認證、受權,指定的用戶才能夠訪問資源
訪問者的信息及操做獲得保護(xss csrf sql注入等)html

開發中咱們須要注意的項:
1. 【高危】網絡關鍵數據傳輸加密
1. 【高危】站點使用https方式部署
2. 【高危】文件傳輸時,過濾與業務無關的文件類型
3. 【高危】接口開發,應預防泄露敏感數據
4. 【高危】預防url中帶url跳轉參數
5. 【中危】預防CSRF攻擊
6. 【中危】預防短信惡意重發
7. 【中危】預防暴力破解圖片驗證碼
8. 【低危】經過httponly預防xss盜取cookie信息
9. 【低危】設置http協議安全的報文頭屬性前端

......nginx

2、爲何要聊spring security?

spring security在不少安全防禦上很容易實現
理解spring security的抽象有助於養成面向對象思惟
能夠爲理解spring security oauth2作鋪墊web

3、先搞清楚兩大概念:認證、受權

Application security boils down to two more or less independent problems: authentication (who are you?) and authorization (what are you allowed to do?).(摘自spring官網 《Spring Security Architecture》ajax

簡單點出發,認證和受權能夠理解爲登陸和權限驗證redis

認證

首先試想一下咱們本身實現登錄,一般須要作些什麼?

1.輸入用戶名密碼、驗證碼提交給後端
2.用戶名密碼與數據庫的進行匹配驗證
3.驗證前能夠先把用戶信息經過用戶名查出來,看看用戶狀態是否可用等
4.傳遞到後端的密碼可能須要加密後再與數據庫裏的密碼進行匹配
5.數據庫裏的密碼多是加鹽存儲的,這樣咱們傳遞進來的密碼還要進行加鹽加密,加鹽通常都是用戶表裏的數據,即仍是可能要提早經過用戶名查詢用戶信息(用戶名加鹽能夠不用提早查庫)
6.登陸成功後,能夠把用戶信息存儲到cookie,下次直接提取cookie信息進行登陸(即remember-me登陸)安全係數稍低,例如電商網站也可經過cookie登陸查看訂單列表,可是下單支付時仍是要從新登陸
7.可能網站有多個登陸入口,多種登陸方式,用戶名密碼方式、短信驗證碼方式等
8.登陸成功後自動跳轉到主頁或者提示成功信息,登陸失敗跳轉到失敗頁或者提示失敗信息
9.退出登陸功能,清空sessionspring

簡單的進行抽象

 

認證攔截器、用戶信息服務

【認證攔截器】咱們用戶名密碼的認證能夠作成一個filter也能夠作成一個servlet
1.提取用戶名密碼參數信息
2.經過用戶名獲取用戶信息,判斷用戶是否可用等
3.經過密碼加密器把密碼參數進行加密而後與查出來的密碼進行匹配,判斷是否定證經過
4.認證成功或者認證失敗的後續處理sql

【用戶信息服務】經過用戶名獲取用戶信息,可是這個只是一個方法,須要提供一個userservice來存放數據庫

 

問題:後端

這裏只是單單考慮用戶名密碼登陸,若是如今要作手機號驗證碼登陸呢?再加一個認證攔截器?
這樣日後會衍生出不少個攔截器,若是是filter實現方式的話就會有多個filter,若是是servlet實現方式則會有多個servlet,若是從作成公用的中間件來提供使用的話,怎樣纔是最好的方式?

認證提供者

若是是作成中間件的話,佔用用戶的多個地址無疑是一個缺點
若是認證攔截器只使用一個,而後把認證這塊的業務進行打包,抽象出一個【認證提供者】,其子類有【用戶名密碼認證提供者 】、【手機號驗證碼認證提供者】供咱們使用,手機號驗證碼認證提供者一樣可使用【用戶信息服務】【認證失敗處理器】【認證成功處理器】
這樣是否會更好?

 

可是認證攔截器這裏要作判斷,要經過請求參數來判斷使用哪一種認證提供者

認證管理者

咱們乾脆把這個事情也抽象出來,讓【認證管理者】去作這個事情,以下:
認證攔截器只須要一個認證管理者,認證管理者能夠有0-n個認證提供者,爲何能夠0個呢,由於認證管理者自己也能夠幹認證這個事情,只不過他能夠交給對應的認證提供者也能夠本身幹這個事情

 

與spring security對號入座

受權

什麼是受權(權限驗證)?

 受權即判斷用戶是否有訪問某資源的權限,資源對於咱們web應用來講就是url,每個controller裏的action對應的request mapping的url

資源:web應用的每個url

權限:用戶可以訪問某個資源的憑證,能夠是一個變量字符,也能夠是角色名,是用戶與資源相關聯的中間產物

 

試想一下咱們本身實現權限驗證,一般須要作些什麼?

1.【受權攔截器】攔截須要受權的資源【受保護的資源】
2.【公開資源】放行靜態資源文件和不用受權的頁面,例如登陸界面
3.有些資源能夠某個角色可以訪問,有些資源須要某個權限才能夠訪問
4.有些資源remember-me登陸的能訪問,有些資源必須從新輸入密碼登陸才能訪問,例5.如電商網站查看訂單就不用從新登陸,下單就須要,即劃分了不一樣的安全級別
6.web界面上菜單按鈕的顯示與隱藏控制 

受權攔截器

攔截全部請求仍是隻攔截受保護的資源請求?

方案一:只攔截受保護的資源請求
【受權攔截器】怎麼攔截【受保護的資源】不攔截【公開資源】呢?
這還不簡單,在項目啓動時把受保護的資源動態添加到filter-mapping不就好了?
相似以下僞代碼:

 

這樣【公開資源】不被攔截、【受保護的資源】被攔截,一箭雙鵰!
可是問題來了,受權應用上線後運行正常,一段時間後,咱們須要增長受保護的資源,好比子應用上線了,子應用是物理上另一個單獨的應用,經過nginx掛載在同域名/module1目錄下,這時數據庫增長資源配置後,可是filter不生效,由於filter在應用啓動的時候已經註冊了,這裏無法增長urlpattern了,最簡單的辦法只能是重啓受權應用

 

方案二:攔截全部資源請求
【受權攔截器】攔截全部請求/*,當請求過來時,只須要判斷當前請求是不是【公開資源】(公開資源能夠動態從配置取也能夠從數據庫去取),是則直接放行,不在公開資源範圍則走受權流程
兩個方案對比來講,在不考慮性能消耗的狀況下(也消耗不了多少性能),無疑方案二更安全更適合擴展

spring security也是採用的方案二,在攔截全部請求後,能夠動態的加載受保護的資源配置,再進行處理

受權攔截器攔截到資源請求後,要作的就是受權
1.經過當前請求的資源獲取權限列表
2.獲取用戶的權限,咱們須要從session持久化的地方去取用戶的權限信息,有統一的地方去存取,後面咱們會講到,不在這裏展開

循環資源的權限
循環用戶的權限

判斷用戶是否擁有該資源的權限

資源對應權限是1對1

那麼咱們的系統就簡單不少
直接經過資源獲取到權限,而後判斷用戶是否有該權限則能夠判斷是否受權經過

資源對應權限是1對0

在用戶登陸狀況下,咱們是受權經過仍是拒絕呢,這個取決於咱們本身,能夠經過配置去設定,spring security固然也是支持咱們這麼作的
FilterSecurityInterceptor.setRejectPublicInvocations(true) 默認是false

資源對應權限是1對多

 

 

 那麼受權這裏咱們應該須要這麼幹:

int hasAuthorities=0 循環資源的權限(5個) 循環用戶的權限,用戶有該資源權限則hasAuthorities +1

最後獲得的結果是:
資源對應權限個數是5
用戶擁有權限個數是2
究竟是能訪問呢仍是不能訪問呢,即受權結果是經過仍是不經過?
仔細看上圖其實發現ROLE_開頭的有兩個,是同一類型的權限,其餘3個是不一樣類型,按道理一個正經常使用戶便是管理員又是新聞編輯角色的可能幾乎不可能,正常來講一個用戶只有一個角色,其餘類型的權限也同理,若是按權限類型來分,應該是4類權限,那麼用戶就擁有2類,應該是 4:2纔對

所裏這裏涉及到兩個問題:
資源的權限應該按分類來進行計數(即ROLE開頭的歸爲一類,無論資源擁有幾個,只要用戶有一個都計數1)
權限的分類:角色、操做、IP、認證模式等
 
受權的決策
一票經過,即用戶擁有一類權限即經過
全票經過,即用戶擁有全部權限分類才經過
少數服從多數,即用戶擁有的權限分類必須大於沒有的分類
 
例如對應上面的4:2,
一票經過:經過
全票經過:拒絕
少數服從多數:2=2 咱們能夠設置相同時的處理邏輯,經過或拒絕,spring security默認相同是經過

受權決策者、受權投票者

受權的決策咱們交給【受權決策者】,投票咱們交給【受權投票者】

 

與spring security對號入座

 

 恭喜,你已經搞清楚filter chain中最關鍵的兩個filter了

Security filter chain: [
...
AuthenticationProcessingFilter
...
FilterSecurityInterceptor
]

4、spring security的filter chain

filter chain的概念

關於filter chain的概念咱們就不作多的解釋,最下面的加載流程圖裏也有說明

 

 

 

 

@EnableWebSecurity(debug = true)

把debug日誌打出來後,每次請求均可以看到完整的filterchain,方便咱們去理解和吸取

Security filter chain: [ WebAsyncManagerIntegrationFilter SecurityContextPersistenceFilter HeaderWriterFilter LogoutFilter AuthenticationProcessingFilter RequestCacheAwareFilter SecurityContextHolderAwareRequestFilter AnonymousAuthenticationFilter SessionManagementFilter ExceptionTranslationFilter FilterSecurityInterceptor ]

如何使用filter chain中的filter

主要仍是增長本身的實現,或者基於默認實現作一些配置 《Spring Security - Adding In Your Own Filters》

授之以漁比授之以魚更加劇要,因此這裏只是簡單的列舉一些使用的例子,具體的原理仍是要到源碼中去本身品味摸索,每一個filter本身的奧妙須要讀者本身去體會

AuthenticationProcessingFilter

登錄(認證 Authentication)
AuthenticationProcessingFilter =》默認UsernamePasswordAuthenticationFilter 或者配置本身實現的filter,登陸成功後會存儲到session,若是是使用的spring-session-redis則會存儲到redis

FilterSecurityInterceptor

權限驗證(受權Authorization)
FilterSecurityInterceptor=》替換成本身實現的filter 若是沒有則使用該filter

RememberMeAuthenticationFilter

protected void configure(HttpSecurity http) throws Exception { http.addFilterAt(rememberMeAuthenticationFilter(), RememberMeAuthenticationFilter.class) } private String REMEMBER_ME_KEY = "3a87d426-0789-46b1-91d9-61d1f953db17"; private RememberMeServices rememberMeServices() { return new TokenBasedRememberMeServices(REMEMBER_ME_KEY, customUserDetailService()) {{ setAlwaysRemember(true);//不須要前端傳遞參數 remember-me=(true/on/yes/1 四個值均可以) }}; } //這裏能夠跟認證的filter公用一個認證管理者(認證管理者會判斷當前authenticationRequest去判斷適用哪一個provider),也能夠建一個新的,而後只添加rememberme認證的provider
private RememberMeAuthenticationFilter rememberMeAuthenticationFilter() throws Exception { return new RememberMeAuthenticationFilter(this.customAuthenticationManager(), this.rememberMeServices()); } private AuthenticationManager customAuthenticationManager() throws Exception { CustomDaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(this.customUserDetailService()); authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder()); List<AuthenticationProvider> providers = new ArrayList<>(); providers.add(authenticationProvider); providers.add(new RememberMeAuthenticationProvider(REMEMBER_ME_KEY)); return new ProviderManager(providers); }

 

RequestCacheAwareFilter

在security調用鏈中用戶可能在沒有登陸的狀況下訪問被保護的頁面,這時候用戶會被跳轉到登陸頁,登陸以後,springsecurity會自動跳轉到以前用戶訪問的保護的頁面

SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler

會先從requestCache去取,若是有上面的操做(例如未登陸訪問某頁面,會記錄到session),就會取session得到url,而後跳轉過去,若是requestcache取不到,就會執行

super.onAuthenticationSuccess,即SimpleUrlAuthenticationSuccessHandler的跳轉到登陸成功頁

若是有些業務數據是寫在登陸成功頁(例如寫cookie),那麼若是requestcache有數據,則不會重定向到登陸成功頁,會直接跳轉到上次未登陸訪問的頁面url,到目標頁後則取不到登陸成功頁寫的數據,那麼就會有問題

 想要關閉訪問緩存?能夠

1、全局配置裏禁用掉

http.requestCache().requestCache(new NullRequestCache())

2、設置成功處理handler直接使用SimpleUrlAuthenticationSuccessHandler,

而不是SavedRequestAwareAuthenticationSuccessHandler

CustomUsernamePasswordAuthenticationFilter mu = new CustomUsernamePasswordAuthenticationFilter(); mu.setAuthenticationSuccessHandler(new SavedRequestAwareAuthenticationSuccessHandler(){{ setDefaultTargetUrl("/login/success"); }};);

把這裏登陸成功的處理handler改成以下SimpleUrlAuthenticationSuccessHandler,simpleurl就不會去取requestCache

mu.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler(){{ setDefaultTargetUrl("/login/success"); }};);

ConcurrentSessionFilter

protected void configure(HttpSecurity http) throws Exception { http .addFilterAt(new ConcurrentSessionFilter(this.sessionRegistry()),ConcurrentSessionFilter.class) } @Bean public SessionRegistry sessionRegistry(){ //若是是分佈式系統,多臺機器,這裏還要改爲SpringSessionBackedSessionRegistry使用springsession存儲,而不是存儲在內存裏
    return new SessionRegistryImpl(); } private CustomUsernamePasswordAuthenticationFilter loginFilter() throws Exception { CustomUsernamePasswordAuthenticationFilter mu = new CustomUsernamePasswordAuthenticationFilter(); List<SessionAuthenticationStrategy> list=new ArrayList(); //從內存取全部sessionid,並過時掉訪問時間最先的
    list.add(new ConcurrentSessionControlAuthenticationStrategy(this.sessionRegistry()));//策略的前後順序沒有關係,spring會幫咱們作好邏輯 //保存當前sessionid至內存
    list.add(new RegisterSessionAuthenticationStrategy(this.sessionRegistry())); mu.setSessionAuthenticationStrategy(new CompositeSessionAuthenticationStrategy(list)); ... }

CsrfFilter

csrffilter默認不會攔截的請求類型:TRACE HEAD GET OPTIONS

protected void configure(HttpSecurity http) throws Exception { http.csrf().disable();//註釋掉默認的
    http.addFilterAt(new CsrfFilter(csrfTokenRepository()),CsrfFilter.class); } //這裏security默認開啓csrf配置也是同樣的,須要注意分佈式環境時token的存儲問題
@Bean public CsrfTokenRepository csrfTokenRepository() { return new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository()); } //本身的loginfilter默認SessionAuthenticationStrategy是null,因此本身實現filter須要註冊上去,若是是security默認的認證filter則會自動注入進去strategy不用咱們操心
private CustomUsernamePasswordAuthenticationFilter loginFilter() throws Exception { CustomUsernamePasswordAuthenticationFilter mu = new CustomUsernamePasswordAuthenticationFilter(); List<SessionAuthenticationStrategy> list=new ArrayList(); //登陸成功後從新生成csrf token,不然登陸成功後token也不會變
    list.add(new CsrfAuthenticationStrategy(csrfTokenRepository())); mu.setSessionAuthenticationStrategy(new CompositeSessionAuthenticationStrategy(list)); ... }

首先得get請求一個頁面,後臺纔會把token存到session供後面post時使用,不過這個csrftoken在訪問第一個get頁面後生成後都不會再改變了,須要注意這一點;

只有每次登陸成功後纔會變!

AuthenticationProcessingFilter裏面的SessionAuthenticationStrategy包含 CsrfAuthenticationStrategy. 會去設置新的csrftoken

 如何使用token

csrfToken=((CsrfToken)ApplicationContextUtil.getBean(CsrfTokenRepository.class)).loadToken(request) csrfToken.getHeaderName() csrfToken.getParameterName() csrfToken.getToken() 或 ((CsrfToken)request.getAttribute("_csrf")).getHeaderName() ((CsrfToken)request.getAttribute("_csrf")).getParameterName() ((CsrfToken)request.getAttribute("_csrf")).getToken() 或 <meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<script> var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); $.ajaxSetup({ beforeSend: function (xhr) { if(header && token ){ xhr.setRequestHeader(header, token); } }} ); </script>

BasicAuthenticationFilter

該filter在ConcurrentSessionFilter後面,說明他不會走同時登陸次數限制的邏輯
構造UsernamePasswordAuthenticationToken而後調用authenticationManager進行身份認證
屬性裏有RememberMeServices,說明能夠走rememberme cookie自動登陸邏輯

LogoutFilter

logoutfilter 注意,只能post請求才能夠
該filter會調用logouthandlers.logout
把 remembermeservices裏的cookie設置過時
把 csrftokenrepository token設置爲null
把 session.invalidate SecurityContext.setAuthentication((Authentication)null) SecurityContextHolder.clearContext();

ExceptionTranslationFilter

關於受權的全部異常拋出統一都是在ExceptionTranslationFilter
包括認證異常、受權異常
認證異常:指的是匿名或者未認證的用戶訪問了須要認證的資源
受權異常:當前用戶沒有訪問該資源的權限

protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint).accessDeniedHandler(customAccessDeniedHandler); } @Component public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { protected final Log logger = LogFactory.getLog(getClass()); @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException ex) throws IOException, ServletException { logger.warn("請從新登陸後訪問,"+ex.getMessage()); logger.warn(JSONObject.toJSON(ex)); RespEntity respEntity = RespUtil.toRespEntity(RespUtil.ACCESS_DENIED, "請從新登陸後訪問",null); response.setCharacterEncoding("UTF-8"); response.setContentType("text/plain;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.println( JSONObject.toJSONString(respEntity)); } } @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { protected final Log logger = LogFactory.getLog(getClass()); @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { logger.warn("請從新登陸後訪問,"+accessDeniedException.getMessage()); logger.warn(JSONObject.toJSON(accessDeniedException)); RespEntity respEntity = RespUtil.toRespEntity(RespUtil.ACCESS_DENIED, "請從新登陸後訪問", null); response.setCharacterEncoding("UTF-8"); response.setContentType("text/plain;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.println( JSONObject.toJSONString(respEntity)); } }

關於session-fixation attacks

在登陸成功後要更換sessionid,默認的認證filter會幫咱們加進去

private CustomUsernamePasswordAuthenticationFilter loginFilter() throws Exception { CustomUsernamePasswordAuthenticationFilter mu = new CustomUsernamePasswordAuthenticationFilter(); List<SessionAuthenticationStrategy> list=new ArrayList(); //登陸成功後更換新的sessionid list.add(new ChangeSessionIdAuthenticationStrategy()); mu.setSessionAuthenticationStrategy(new CompositeSessionAuthenticationStrategy(list)); ... }

默認的認證filter的session認證strategy有4個(會隨着開啓csrf  concurrentsession而增長strategy,不開則不加)

模擬:

創建一個springboot站點(不使用spring security)

@RestController public class TestController { @GetMapping("/login") public String login(@RequestParam(name = "userName",required = false) String userName, HttpServletRequest request) { request.getSession().setAttribute("userName",userName); return "sessionid:"+request.getSession().getId()+";userName:"+userName; } @GetMapping("/user") public String getOrder( HttpServletRequest request) { return "sessionid:"+request.getSession().getId()+";userName:"+request.getSession().getAttribute("userName"); } }

1.攻擊者先訪問 login地址,獲得sessionid

2.被攻擊者訪問地址

http://localhost:8080/login;jsessionid=520F92C885F099E997DA55D9D0F450BE

3.被攻擊者訪問地址後模擬get登陸(後面附帶參數)

http://localhost:8080/login;jsessionid=520F92C885F099E997DA55D9D0F450BE?userName=tianjun

 4.攻擊者能夠以用戶正常認證方式進行操做和竊取用戶信息

 

5、源碼相關

重要的仍是搞清楚如何進行抽象,爲何這樣去抽象?

以下是spring bean加載流程(右鍵新標籤打開可查看大圖)

相關文章
相關標籤/搜索