前言:
在web应用中,非常流行以基本认证和摘要认证作为备选的认证机制。其中基本认证常用来对无状态的客户端进行认证,例如结合基于form的认证形式我们既能给用浏览器访问的用户提供认证也能给用web-service(restful)进行访问的用户提供认证。因为基本认证时用户名和密码都是以文本形式传递到服务端的,在安全级别高的应用中,基本认证要在传输过程中进行加密,例如采用https的形式。
一、基本认证
1.过滤器BasicAuthenticationFilter
和基本认证相关的逻辑主要在这个filter,当filter收到请求时按照如下逻辑执行
[*]从request的header中获取属性Authorization对应的数据,如果Authorization对应的value不是以【Basic 】开头,表示不是用basic形式认证的,跳过当前过滤器
[*]获取【Basic 】后的数据,用Base64进行解码,解码后以【:】进行分割,第一部分是用户名,第二部分是密码
[*]用上一步获取到的用户名和密码组装成UsernamePasswordAuthenticationToken,之后调用authenticationManager.authenticate方法进行认证
[*]如果认证成功将认证后组装好的Authentication对象放入SecurityContextHolder中并调用rememberMeServices.loginSuccess服务将用户名和密码编码后存入cookie中,执行记住用户功能
[*]如果认证失败,清除SecurityContextHolder中的认证对象,调用rememberMeServices.loginFail清除cookie信息,调用BasicAuthenticationEntryPoint的commence方法,设置response的头部,追加WWW-Authenticate信息,设置返回码为401
例如
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==】对应的值就是用户名和密码加密后的信息。
认证过程如下:
[img]http://dl2.iteye.com/upload/attachment/0129/0471/e0dc1006-acdc-30e4-afba-6b67f86dc942.png[/img]
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.摘要认证主要是为了解决基本认证中密码容易被破解的问题,通过将认证信息加密处理来提升安全性。
[b]
在企业级应用开发中最好也不要考虑使用摘要认证,他只是相对基本认证的安全性有提升,但是还是有安全漏洞。最明显的问题是你必须将密码以明文、编码后、MD5处理中的一种形式保存下来,而这些算法都是可逆的算法,所以都是不安全的。相反我们应该采用单一方向的hash算法(bCrypt、PBKDF2、SCrypt)来保护我们的密码。只有更安全的认证方式在某些外部因素制约下不能采用时才考虑采用这种认证方式。
[/b]
2.摘要认证的核心是服务端产生的一个人不可理解的字符-“nonce”,采用如下算法生成
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime: nonce过期的时间点,以毫秒为单位
key:一个私有的key防止nonce被修改
[i]因为客户端在传递给服务器用户凭证时已经经过了加密,又不能恢复出来password信息,所以我们必须拿着原始密码经过相同的hash算法得出结果后再和客户端的结果进行比较,这就是为什么我们必须明文存储用户密码的原因[/i]。
3.DigestAuthenticationFilter
和摘要认证相关的逻辑都在这个Filter中,主要的处理逻辑如下:
[*]从request的header中获取属性Authorization对应的数据,如果Authorization对应的value不是以【Digest 】开头,表示不是用basic形式认证的,跳过当前过滤器
[*]获取【Digest 】后的数据组装成DigestData
[*]利用生成nonce对应的key和realm对DigestData进行校验
[*]校验成功后调用userDetailsService.loadUserByUsername获取用户信息
[*]将获取到的用户的密码经过MD5算发获取到的结果和客户端传递过来的结果进行比对,一致则认证成功
[*]认证成功后将用户信息封装成Authentication,存入到SecurityContextHolder中
[*]如果上述步骤中的验证不成功,则认为认证失败,继续执行后续过滤器
在后续过滤器中因为没有正常认证,最终ExceptionTranslationFilter会捕获到异常,调用DigestAuthenticationEntryPoint的commence方法,设置response header信息,返回客户端一个401,例如
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;
}
完整的示例代码参考:[url=https://github.com/fengyilin/spring-security-sample/tree/master]源码[/url]