Spring Boot 中如何實現 HTTP 認證?

HttpBasic 認證有必定的侷限性與安全隱患,所以在實際項目中使用並很少,可是,有的時候爲了測試方便,開啓 HttpBasic 認證能方便不少。前端

所以鬆哥今天仍是來和你們簡單聊一聊 Spring Security 中的 HttpBasic 認證。java

本文是 Spring Security 系列第 29 篇,閱讀前面文章有助於更好理解本文:算法

  1. 挖一個大坑,Spring Security 開搞!
  2. 鬆哥手把手帶你入門 Spring Security,別再問密碼怎麼解密了
  3. 手把手教你定製 Spring Security 中的表單登陸
  4. Spring Security 作先後端分離,咱就別作頁面跳轉了!通通 JSON 交互
  5. Spring Security 中的受權操做原來這麼簡單
  6. Spring Security 如何將用戶數據存入數據庫?
  7. Spring Security+Spring Data Jpa 強強聯手,安全管理只有更簡單!
  8. Spring Boot + Spring Security 實現自動登陸功能
  9. Spring Boot 自動登陸,安全風險要怎麼控制?
  10. 在微服務項目中,Spring Security 比 Shiro 強在哪?
  11. SpringSecurity 自定義認證邏輯的兩種方式(高級玩法)
  12. Spring Security 中如何快速查看登陸用戶 IP 地址等信息?
  13. Spring Security 自動踢掉前一個登陸用戶,一個配置搞定!
  14. Spring Boot + Vue 先後端分離項目,如何踢掉已登陸用戶?
  15. Spring Security 自帶防火牆!你都不知道本身的系統有多安全!
  16. 什麼是會話固定攻擊?Spring Boot 中要如何防護會話固定攻擊?
  17. 集羣化部署,Spring Security 要如何處理 session 共享?
  18. 鬆哥手把手教你在 SpringBoot 中防護 CSRF 攻擊!so easy!
  19. 要學就學透徹!Spring Security 中 CSRF 防護源碼解析
  20. Spring Boot 中密碼加密的兩種姿式!
  21. Spring Security 要怎麼學?爲何必定要成體系的學習?
  22. Spring Security 兩種資源放行策略,千萬別用錯了!
  23. 鬆哥手把手教你入門 Spring Boot + CAS 單點登陸
  24. Spring Boot 實現單點登陸的第三種方案!
  25. Spring Boot+CAS 單點登陸,如何對接數據庫?
  26. Spring Boot+CAS 默認登陸頁面太醜了,怎麼辦?
  27. 用 Swagger 測試接口,怎麼在請求頭中攜帶 Token?
  28. Spring Boot 中三種跨域場景總結

1.什麼是 HttpBasic

Http Basic 認證是 Web 服務器和客戶端之間進行認證的一種方式,最初是在 HTTP1.0 規範(RFC 1945)中定義,後續的有關安全的信息能夠在 HTTP 1.1 規範(RFC 2616)和 HTTP 認證規範(RFC 2617)中找到。spring

HttpBasic 最大的優點在於使用很是簡單,沒有複雜的頁面交互,只須要在請求頭中攜帶相應的信息就能夠認證成功,並且它是一種無狀態登陸,也就是 session 中並不會記錄用戶的登陸信息。數據庫

HttpBasic 最大的問題在於安全性,由於用戶名/密碼只是簡單的經過 Base64 編碼以後就開始傳送了,很容易被工具嗅探到,進而暴露用戶信息。後端

Spring Security 中既支持基本的 HttpBasic 認證,也支持 Http 摘要認證,Http 摘要認證是在 HttpBasic 認證的基礎上,提升了信息安全管理,可是代碼複雜度也提升了很多,因此 Http 摘要認證使用並很少。跨域

這裏,鬆哥將和你們分享 Spring Security 中的這兩種認證方式。瀏覽器

2.HttpBasic 認證

咱們先來看實現,再來分析它的認證流程。安全

首先建立一個 Spring Boot 項目,引入 Web 和 Spring Security 依賴,以下:服務器

接下來建立一個測試接口:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

再開啓 HttpBasic 認證:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .httpBasic();
    }
}

最後再在 application.properties 中配置基本的用戶信息,以下:

spring.security.user.password=123
spring.security.user.name=javaboy

配置完成後,啓動項目,訪問 /hello 接口,此時瀏覽器中會有彈出框,讓咱們輸入用戶名/密碼信息:

此時咱們查看請求響應頭,以下:

能夠看到,瀏覽器響應了 401,同時還攜帶了一個 WWW-Authenticate 響應頭,這個是用來描述認證形式的,若是咱們使用的是 HttpBasic 認證,默認響應頭格式如圖所示。

接下來咱們輸入用戶名密碼,點擊 Sign In 進行登陸,登陸成功後,就能夠成功訪問到 /hello 接口了。

咱們查看第二次的請求,以下:

你們能夠看到,在請求頭中,多了一個 Authorization 字段,該字段的值爲 Basic amF2YWJveToxMjM=

amF2YWJveToxMjM= 是一個通過 Base64 編碼以後的字符串,咱們將該字符串解碼以後發現,結果以下:

String x = new String(Base64.getDecoder().decode("amF2YWJveToxMjM="), "UTF-8");

解碼結果以下:

能夠看到,這就是咱們的用戶名密碼信息。用戶名/密碼只是通過簡單的 Base64 編碼以後就開始傳遞了,因此說,這種認證方式比較危險⚠️。

咱們再來稍微總結一下 HttpBasic 認證的流程:

  1. 瀏覽器發出請求,說要訪問 /hello 接口。
  2. 服務端返回 401,表示未認證。同時在響應頭中攜帶 WWW-Authenticate 字段來描述認證形式。
  3. 瀏覽器收到 401 響應以後,彈出對話框,要求用戶輸入用戶名/密碼,用戶輸入完用戶名/密碼以後,瀏覽器會將之進行 Base64 編碼,編碼完成後,發送到服務端。
  4. 服務端對瀏覽器傳來的信息進行解碼,並校驗,當沒問題的時候,給客戶端做出響應。

大體的流程就是這樣。

3.Http 摘要認證

Http 摘要認證與 HttpBasic 認證基本兼容,可是要複雜不少,這個複雜不只體如今代碼上,也體如今請求過程當中。

Http 摘要認證最重要的改進是他不會在網絡上發送明文密碼。它的整個認證流程是這樣的:

  1. 瀏覽器發出請求,說要訪問 /hello 接口。
  2. 服務端返回 401,表示未認證,同時在響應頭中攜帶 WWW-Authenticate 字段來描述認證形式。不一樣的是,此次服務端會計算出一個隨機字符串,一同返回前端,這樣能夠防止重放攻擊(所謂重放攻擊就是別人嗅探到你的摘要信息,把摘要當成密碼一次次發送服務端,加一個會變化的隨機字符串,生成的摘要信息就會變化,就能夠防止重放攻擊),以下:

同時,服務端返回的字段還有一個 qop,表示保護級別,auth 表示只進行身份驗證;auth-int 表示還要校驗內容。

nonce 是服務端生成的隨機字符串,這是一個通過 Base64 編碼的字符串,通過解碼咱們發現,它是由過時時間和密鑰組成的。在之後的請求中 nonce 會原封不動的再發回給服務端。

  1. 客戶端選擇一個算法,根據該算法計算出密碼以及其餘數據的摘要,以下:

能夠看到,客戶端發送到服務端的數據比較多。

  • nonce 就是服務端發來的隨機字符串。
  • response 是生成的摘要信息。
  • nc 表示請求此時,能夠防止重放攻擊。
  • cnonce 表示客戶端發送給服務端的隨機字符串。
  1. 服務端根據客戶端發送來的用戶名,能夠查詢出用戶密碼,再根據用戶密碼能夠計算出摘要信息,再將摘要信息和客戶端發送來的摘要信息進行對比,就能確認用戶身份。

這就是整個流程。

一言以蔽之,本來的用戶密碼被摘要信息代替了,爲了安全,摘要信息會根據服務端返回的隨機字符串而發生變化,服務端根據用戶密碼,一樣算出密碼的摘要信息,再和客戶端傳來的摘要信息進行對比,沒問題的話,用戶就算認證成功了。固然,在此基礎上還加了一些過時限制、重放攻擊防範機制等。

好了,那這個在 Spring Security 代碼中該怎麼實現呢?

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .csrf()
                .disable()
                .exceptionHandling()
                .authenticationEntryPoint(digestAuthenticationEntryPoint())
                .and()
                .addFilter(digestAuthenticationFilter());
    }

    @Bean
    DigestAuthenticationEntryPoint digestAuthenticationEntryPoint() {
        DigestAuthenticationEntryPoint entryPoint = new DigestAuthenticationEntryPoint();
        entryPoint.setKey("javaboy");
        entryPoint.setRealmName("myrealm");
        entryPoint.setNonceValiditySeconds(1000);
        return entryPoint;
    }
    @Bean
    DigestAuthenticationFilter digestAuthenticationFilter() {
        DigestAuthenticationFilter filter = new DigestAuthenticationFilter();
        filter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint());
        filter.setUserDetailsService(userDetailsService());
        return filter;
    }

    @Override
    @Bean
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("javaboy").password("123").roles("admin").build());
        return manager;
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

}

配置無非就是兩方面,一方面是服務端隨機字符串的生成,另外一方面就是客戶端摘要信息的校驗。

  1. 首先提供 DigestAuthenticationEntryPoint 的實例,配置服務端隨機數生成的一寫參數,例如 nonce 有效期(多長時間會變),realm 的名字,以及生成 nonce 時所須要的 key。nonce 的具體生成邏輯在 DigestAuthenticationEntryPoint#commence 方法中:
public void commence(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException authException) throws IOException {
    HttpServletResponse httpResponse = response;
    long expiryTime = System.currentTimeMillis() + (nonceValiditySeconds * 1000);
    String signatureValue = DigestAuthUtils.md5Hex(expiryTime + ":" + key);
    String nonceValue = expiryTime + ":" + signatureValue;
    String nonceValueBase64 = new String(Base64.getEncoder().encode(nonceValue.getBytes()));
    String authenticateHeader = "Digest realm=\"" + realmName + "\", "
            + "qop=\"auth\", nonce=\"" + nonceValueBase64 + "\"";
    if (authException instanceof NonceExpiredException) {
        authenticateHeader = authenticateHeader + ", stale=\"true\"";
    }
    if (logger.isDebugEnabled()) {
        logger.debug("WWW-Authenticate header sent to user agent: "
                + authenticateHeader);
    }
    httpResponse.addHeader("WWW-Authenticate", authenticateHeader);
    httpResponse.sendError(HttpStatus.UNAUTHORIZED.value(),
        HttpStatus.UNAUTHORIZED.getReasonPhrase());
}

在這段代碼中,首先獲取到過時時間,而後給過時時間和 key 一塊兒計算出消息摘要,再將 nonce 和消息摘要共同做爲 value,計算出一個 Base64 編碼字符,再將該編碼字符寫回到前端。

  1. 配置 DigestAuthenticationFilter 過濾器,主要用來處理前端請求。過濾器的源碼比較長,我這裏就不貼出來了,一個核心的思路就是從前端拿到用戶請求的摘要信息,服務端也根據一直的信息算出來一個摘要,再根據傳過來的摘要信息進行比對,進而確認用戶身份。

配置完成後,重啓服務端進行測試。

測試效果其實和 HttpBasic 認證是同樣的,全部的變化,只是背後的實現有所變化而已,用戶體驗是同樣的。

4.小結

Http 摘要認證的效果雖然比 HttpBasic 安全,可是其實你們看到,整個流程下來解決的安全問題其實仍是很是有限。並且代碼也麻煩了不少,所以這種認證方式並未普遍流行開來。

Http 認證小夥伴們做爲一個瞭解便可,裏邊的有一些思想仍是挺有意思的,能夠激發咱們解決其餘問題的思路,例如對於重放攻擊的的解決辦法,咱們若是想本身防護重放攻擊,就能夠參考這裏的實現思路。

好啦,小夥伴們若是有收穫,記得點個在看鼓勵下鬆哥哦~

相關文章
相關標籤/搜索