在web应用中,非常流行以基本认证和摘要认证作为备选的认证机制。其中基本认证常用来对无状态的客户端进行认证,例如结合基于form的认证形式我们既能给用浏览器访问的用户提供认证也能给用web-service(restful)进行访问的用户提供认证。因为基本认证时用户名和密码都是以文本形式传递到服务端的,在安全级别高的应用中,基本认证要在传输过程中进行加密,例如采用https的形式。
一、基本认证
1.过滤器BasicAuthenticationFilter
和基本认证相关的逻辑主要在这个filter,当filter收到请求时按照如下逻辑执行
HTTP/1.1 401 X-Content-Type-Options nosniff X-XSS-Protection 1; mode=block Cache-Control no-cache, no-store, max-age=0, must-revalidate Pragma no-cache Expires 0 X-Frame-Options DENY Set-Cookie JSESSIONID=493B4D42EC4A2D5AA85C7521288F54F9; Path=/; HttpOnly WWW-Authenticate Basic realm="Realm" Content-Type text/html;charset=ISO-8859-1 Content-Language zh-CN Content-Length 344 Date Tue, 06 Mar 2018 01:42:28 GMT Proxy-Connection Keep-alive
浏览器接收到信息后弹出框提示用户输入用户名和密码,之后浏览器将用户名和密码编码后发送到服务器,例如:
GET /index.html HTTP/1.1 Host localhost:8080 Cache-Control max-age=0 Authorization Basic dXNlcjpwYXNzd29yZA== Upgrade-Insecure-Requests 1 User-Agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36 Accept text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding gzip, deflate, br Accept-Language zh-CN,zh;q=0.9,en;q=0.8 Cookie JSESSIONID=493B4D42EC4A2D5AA85C7521288F54F9
【dXNlcjpwYXNzd29yZA==】对应的值就是用户名和密码加密后的信息。
认证过程如下:
2.由上面的内容可以看到这种认证方式的认证逻辑非常简单,开发者也不用考虑登录页面如何显示,开发方式非常简便。但是采用这种认证方式请求头信息在传输过程中是明码传输的,采用的用户名密码加密方式为BASE-64,其解码过程非常简单,网络上很容易搜索到编解码的源码,所以很容易被破解,所以其认证技术并不是很安全一般都是在嵌入式设备上使用,实际应用中最好能结合tls技术对传递的信息进行加密,提高安全性。
3.在spring boot应用中启用basic认证也很简单
@Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .disable() .authorizeRequests() .anyRequest().authenticated() .and() .httpBasic(); }
调用HttpSecurity的httpBasic()方法即可,具体这个方法是如何将BasicAuthenticationFilter追加到servletContext中的,可参考前面文章核心filter的讲解,此处不在展开。
二、摘要认证
1.摘要认证主要是为了解决基本认证中密码容易被破解的问题,通过将认证信息加密处理来提升安全性。
在企业级应用开发中最好也不要考虑使用摘要认证,他只是相对基本认证的安全性有提升,但是还是有安全漏洞。最明显的问题是你必须将密码以明文、编码后、MD5处理中的一种形式保存下来,而这些算法都是可逆的算法,所以都是不安全的。相反我们应该采用单一方向的hash算法(bCrypt、PBKDF2、SCrypt)来保护我们的密码。只有更安全的认证方式在某些外部因素制约下不能采用时才考虑采用这种认证方式。
2.摘要认证的核心是服务端产生的一个人不可理解的字符-“nonce”,采用如下算法生成
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key)) expirationTime: nonce过期的时间点,以毫秒为单位 key:一个私有的key防止nonce被修改
因为客户端在传递给服务器用户凭证时已经经过了加密,又不能恢复出来password信息,所以我们必须拿着原始密码经过相同的hash算法得出结果后再和客户端的结果进行比较,这就是为什么我们必须明文存储用户密码的原因。
3.DigestAuthenticationFilter
和摘要认证相关的逻辑都在这个Filter中,主要的处理逻辑如下:
HTTP/1.1 401 X-Content-Type-Options nosniff X-XSS-Protection 1; mode=block Cache-Control no-cache, no-store, max-age=0, must-revalidate Pragma no-cache Expires 0 X-Frame-Options DENY Set-Cookie JSESSIONID=AE5A3F9F3ED75248B801954C58850A39; Path=/; HttpOnly WWW-Authenticate Digest realm="security", qop="auth", nonce="MTUyMDMyMjk3NDIzOTpjOGZiMzYyY2MzNTQ1ZGQwN2UwNzE1YzE2YjExNWQwMg==" Content-Type text/html;charset=ISO-8859-1 Content-Language zh-CN Content-Length 344 Date Tue, 06 Mar 2018 07:51:14 GMT Proxy-Connection Keep-alive
之后浏览器回弹框提示用户输入用户名和密码,之后浏览器将用户名和密码编码后发送到服务器
GET /index.html HTTP/1.1 Host localhost:8080 Cache-Control max-age=0 Authorization Digest username="user", realm="security", nonce="MTUyMDMyMjk3NDIzOTpjOGZiMzYyY2MzNTQ1ZGQwN2UwNzE1YzE2YjExNWQwMg==", uri="/index.html", response="87007805d864a0f07f0bbf0e915fbe68", qop=auth, nc=00000001, cnonce="645f7f377909cc62" Upgrade-Insecure-Requests 1 User-Agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36 Accept text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding gzip, deflate, br Accept-Language zh-CN,zh;q=0.9,en;q=0.8 Cookie JSESSIONID=AE5A3F9F3ED75248B801954C58850A39
认证的流程图和基本认证一样,唯一区别是Filter换成了DigestAuthenticationFilter,这里就不再画流程了。
4.spring boot中配置degist认证方式
@Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .disable() .authorizeRequests() .anyRequest().authenticated() .and() .exceptionHandling() .authenticationEntryPoint(digestEntryPoint()).and() .addFilter(digestFilter()); } @Bean public DigestAuthenticationEntryPoint digestEntryPoint() { DigestAuthenticationEntryPoint entryPoint = new DigestAuthenticationEntryPoint(); entryPoint.setKey("chengf"); entryPoint.setRealmName("security"); return entryPoint; } @Bean public DigestAuthenticationFilter digestFilter() throws Exception { DigestAuthenticationFilter digestFilter = new DigestAuthenticationFilter(); digestFilter.setAuthenticationEntryPoint(digestEntryPoint()); digestFilter.setUserDetailsService(userDetailsService()); return digestFilter; }
完整的示例代码参考: 源码