HTTP 认证
HTTP 认证
通过HTTP请求头来提供认证信息,而不是通过表单登录。有HTTP Basic authentication HTTP Digest authentication
HTTP Basic authentication
HTTP Basic authentication将用户的登录用户名 密码经过Base64编码之后,放在请求头的Authorization字段中,从而完成用户身份的认证。
- 客户端发送请求
- 服务端收到请求后,发现用户还没有认证,返回状态码401 WWW-Authenticate响应头则定义了使用何种验证方式去完成身份认证,最简单最常见的是HTTP基本认证(Basic),还有Bearer(Oauth2.0 认证),Digest(HTTP摘要认证)
- 客户端收到服务端响应后,将用户名密码使用Base64编码后,放在请求头中,再次发送请求
- 服务端解析Authorization字段,完成用户身份的校验,最后将资源返回给客户端。
这种认证方式很少使用,因为有安全问题,HTTP基本认证没有对传输的凭证信息进行加密,仅仅只是进行了Base64编码。
通过httpBasic()方法即可开启HTTP基本认证。
Security实现HTTP基本认证分为两部分:
- 对未认证的请求发出质询
- 解析携带认证信息的请求。
对未认证的请求发出质询
httpBasic()方法开启了HTTP基本认证的配置,具体配置通过HttpBasicConfigurer完成。HttpBasicConfigurer的init方法中调用registerDefaultEntryPoint完成失败请求处理类AuthenticationEntryPoint的配置
private void registerDefaultEntryPoint(B http, RequestMatcher preferredMatcher) {
ExceptionHandlingConfigurer<B> exceptionHandling = http
.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionHandling == null) {
return;
}
exceptionHandling.defaultAuthenticationEntryPointFor(
postProcess(this.authenticationEntryPoint), preferredMatcher);
}
对exceptionHandling配置的最终目的是配置异常过滤器ExceptionTranslationFilter,authenticationEntryPoint是代理对象,在HttpBasicConfigurer构造方法中创建,具体代理的是BasicAuthenticationEntryPoint,
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.addHeader("WWW-Authenticate", "Basic realm=\"" + realmName + "\"");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
响应头添加WWW-Authenticate字段,然后发送错误响应,响应码401
总结:未认证的请求,在经过Spring Security过滤器链时会抛出异常,异常在ExceptionTranslationFilter过滤器链中调用BasicAuthenticationEntryPoint的commence方法进行处理。
解析携带认证信息的请求
HttpBasicConfigurer的configure方法中,向Spring Security过滤器链中添加了BasicAuthenticationFilter过滤器
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
final boolean debug = this.logger.isDebugEnabled();
try {
UsernamePasswordAuthenticationToken authRequest = authenticationConverter.convert(request);
if (authRequest == null) {
chain.doFilter(request, response);
return;
}
String username = authRequest.getName();
if (debug) {
this.logger
.debug("Basic Authentication Authorization header found for user '"
+ username + "'");
}
if (authenticationIsRequired(username)) {
Authentication authResult = this.authenticationManager
.authenticate(authRequest);
if (debug) {
this.logger.debug("Authentication success: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
}
catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
if (debug) {
this.logger.debug("Authentication request for failed!", failed);
}
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, failed);
if (this.ignoreFailure) {
chain.doFilter(request, response);
}
else {
this.authenticationEntryPoint.commence(request, response, failed);
}
return;
}
chain.doFilter(request, response);
}
- 调用authenticationConverter.convert方法,对请求头中的Authorization字段进行解析,经过Base64解码后的用户名密码用冒号隔开,然后构造出UsernamePasswordAuthenticationToken的实例authRequest
- 如果authRequest为null,说明请求头中没有包含认证信息,直接指向接下来的过滤器,最终通过ExceptionTranslationFilter过滤器链中调用BasicAuthenticationEntryPoint的commence方法进行处理。如果不为null,说明请求携带了认证信息,那么对认证信息进行校验。
- 具体校验为从authRequest中提取出用户名,然后调用authenticationIsRequired方法判断是否需要认证,不需要认证执行下一个过滤器,需要认证则进行认证。authenticationIsRequired是从SecurityContextHolder中渠道当前登录对象,判断是否已经登录过。
- 调用authenticationManager.authenticate(authRequest)方法完成用户认证,将用户信息存入SecurityContextHolder中
HTTP Digest authentication
需要自定义配置
@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());
}
DigestAuthenticationEntryPoint digestAuthenticationEntryPoint() {
DigestAuthenticationEntryPoint entryPoint = new DigestAuthenticationEntryPoint();
entryPoint.setNonceValiditySeconds(3600);
entryPoint.setRealmName("myrealm");
entryPoint.setKey("javaboy");
return entryPoint;
}
DigestAuthenticationFilter digestAuthenticationFilter() throws Exception {
DigestAuthenticationFilter filter = new DigestAuthenticationFilter();
filter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint());
filter.setUserDetailsService(userDetailsServiceBean());
filter.setPasswordAlreadyEncoded(true);
return filter;
}
@Override
@Bean
public UserDetailsService userDetailsServiceBean() throws Exception {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("javaboy").password("e7ecfd3f08e6960f154e1ff29079fbd3").roles("admin").build());
return manager;
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
- 提供DigestAuthenticationEntryPoint实例,当用户发起一个没有认证的请求时,需要该实例进行处理。
- 创建DigestAuthenticationFilter实例,添加到Spring Security过滤器链中。
质询
HTTP摘要认证的质询由DigestAuthenticationEntryPoint的commence方法完成
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
HttpServletResponse httpResponse = response;
// compute a nonce (do not use remote IP address due to proxy farms)
// format of nonce is:
// base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
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()));
// qop is quality of protection, as defined by RFC 2617.
// we do not use opaque due to IE violation of RFC 2617 in not
// representing opaque on subsequent requests in same session.
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());
}
和HTTP基本认证一样,不同的是WWW-Authenticate字段值
- Digest表示使用HTTP摘要认证
- realm表示服务端返回的标识访问资源的安全域
- qop表示服务端返回的保护级别,auth表示只进行身份认证;auth-int表示除了身份认证还要校验内容完整性
- nonce为生成的随机数 生成规则:先对过期时间和key组成的字符串计算出消息摘要signatureValue,再对过期时间和signatureValue进行Base64编码。
- stale表示当nonce过期了包含该标记,stale=true表示客户端不必再次弹出输入框,只需要带上已有认证信息,重新发起认证请求即可。
客户端处理
客户端收到请求后,输入用户名密码,然后客户端生成response,用户密码经过各种MD5运算后,包含在response中,服务端拿到这些参数后,根据用户名去数据库中查询用户密码,然后进行MD5运算,将结果和response对比。
请求解析
过滤器DigestAuthenticationFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Digest ")) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug(
"Digest Authorization header received from user agent: " + header);
}
DigestData digestAuth = new DigestData(header);
try {
digestAuth.validateAndDecode(this.authenticationEntryPoint.getKey(),
this.authenticationEntryPoint.getRealmName());
}
catch (BadCredentialsException e) {
fail(request, response, e);
return;
}
// Lookup password for presented username
// NB: DAO-provided password MUST be clear text - not encoded/salted
// (unless this instance's passwordAlreadyEncoded property is 'false')
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(digestAuth.getUsername());
String serverDigestMd5;
try {
if (user == null) {
cacheWasUsed = false;
user = this.userDetailsService
.loadUserByUsername(digestAuth.getUsername());
if (user == null) {
throw new AuthenticationServiceException(
"AuthenticationDao returned null, which is an interface contract violation");
}
this.userCache.putUserInCache(user);
}
serverDigestMd5 = digestAuth.calculateServerDigest(user.getPassword(),
request.getMethod());
// If digest is incorrect, try refreshing from backend and recomputing
if (!serverDigestMd5.equals(digestAuth.getResponse()) && cacheWasUsed) {
if (logger.isDebugEnabled()) {
logger.debug(
"Digest comparison failure; trying to refresh user from DAO in case password had changed");
}
user = this.userDetailsService
.loadUserByUsername(digestAuth.getUsername());
this.userCache.putUserInCache(user);
serverDigestMd5 = digestAuth.calculateServerDigest(user.getPassword(),
request.getMethod());
}
}
catch (UsernameNotFoundException notFound) {
fail(request, response,
new BadCredentialsException(this.messages.getMessage(
"DigestAuthenticationFilter.usernameNotFound",
new Object[] { digestAuth.getUsername() },
"Username {0} not found")));
return;
}
// If digest is still incorrect, definitely reject authentication attempt
if (!serverDigestMd5.equals(digestAuth.getResponse())) {
if (logger.isDebugEnabled()) {
logger.debug("Expected response: '" + serverDigestMd5
+ "' but received: '" + digestAuth.getResponse()
+ "'; is AuthenticationDao returning clear text passwords?");
}
fail(request, response,
new BadCredentialsException(this.messages.getMessage(
"DigestAuthenticationFilter.incorrectResponse",
"Incorrect response")));
return;
}
// To get this far, the digest must have been valid
// Check the nonce has not expired
// We do this last so we can direct the user agent its nonce is stale
// but the request was otherwise appearing to be valid
if (digestAuth.isNonceExpired()) {
fail(request, response,
new NonceExpiredException(this.messages.getMessage(
"DigestAuthenticationFilter.nonceExpired",
"Nonce has expired/timed out")));
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Authentication success for user: '" + digestAuth.getUsername()
+ "' with response: '" + digestAuth.getResponse() + "'");
}
Authentication authentication = createSuccessfulAuthentication(request, user);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
chain.doFilter(request, response);
}
太长了 不一一分析了。。
和HTTP基本认证相比 最大亮点就是不明文传输用户密码。客户端对密码进行MD5运算,并将运算所需参数以及运算结果发送给服务端,服务端再去校验数据是否正确。
HTTP摘要认证比较复杂 使用并不多。。
- 点赞
- 收藏
- 关注作者
评论(0)