前言
- 在SpringSecurity中存在多种认证方式,除了表单认证,还有记住我认证,JASS认证,基本认证,摘要认证等等
- 基本认证:是一种将用户名/密码经过Base64编码后,放在请求头的Authorization属性中,再交由服务端校验
![[SpringSecurity5.6.2源码分析十八]:BasicAuthenticationFilter](https://static.blogweb.cn/article/131eaaee6a4a4f03b99ee4a48b3c3c11.webp)
1. HttpBasicConfigurer
- HttpBasicConfigurer作为BasicAuthenticationFilter的配置类
- 我们不要同时开启多种认证方式,ExceptionTranslationFilter中的createDefaultDeniedHandler(...)方法给出了答案
- 当我们配置了多种认证方式后就会有多个身份认证入口点(AuthenticationEntryPoint), 多个身份认证人口点又会被封装为一个委托机制的类,但是这里有一个保底的默认的身份认证人口点是取得是所有身份认证入口点的第一个,也就是defaultEntryPointMappings对象
- 而这个defaultEntryPointMappings又是一个LinkedHashMap,LinkedHashMap是HashMap的一个扩展维护一个
双向链表
,保证了顺序
- 换句话说默认的身份认证入口点是看谁先put到这个LinkedHashMap中决定的
1.1 init(...)
- init(...)很简单就是调用了registerDefaults(...)方法
- registerDefaults(...):注册参数默认值
private void registerDefaults(B http) {
ContentNegotiationStrategy contentNegotiationStrategy = http.getSharedObject(ContentNegotiationStrategy.class);
if (contentNegotiationStrategy == null) {
contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
}
MediaTypeRequestMatcher restMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy,
MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON,
MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML, MediaType.MULTIPART_FORM_DATA,
MediaType.TEXT_XML);
restMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
//第一个请求匹配器
MediaTypeRequestMatcher allMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.ALL);
allMatcher.setUseEquals(true);
RequestMatcher notHtmlMatcher = new NegatedRequestMatcher(
new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.TEXT_HTML));
//第二个请求匹配器
RequestMatcher restNotHtmlMatcher = new AndRequestMatcher(
Arrays.<RequestMatcher>asList(notHtmlMatcher, restMatcher));
RequestMatcher preferredMatcher = new OrRequestMatcher(
Arrays.asList(X_REQUESTED_WITH, restNotHtmlMatcher, allMatcher));
//注册到ExceptionTranslationFilter中
registerDefaultEntryPoint(http, preferredMatcher);
//注册到LogoutFilter中去
registerDefaultLogoutSuccessHandler(http, preferredMatcher);
}
1.2 configure(...)
public void configure(B http) {
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
//创建过滤器,并设置局部认证管理器
BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter(authenticationManager,
this.authenticationEntryPoint);
if (this.authenticationDetailsSource != null) {
basicAuthenticationFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource);
}
//设置记住我服务
RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);
if (rememberMeServices != null) {
basicAuthenticationFilter.setRememberMeServices(rememberMeServices);
}
basicAuthenticationFilter = postProcess(basicAuthenticationFilter);
http.addFilter(basicAuthenticationFilter);
}
2. BasicAuthenticationFilter
- doFilterInternal(...):和UsernamePasswordAuthenticationFilter一样都是借助AuthenticationManager进行认证的
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
//通过解析request中的某些参数转为认证对象
UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);
if (authRequest == null) {
this.logger.trace("Did not process authentication request since failed to find "
+ "username and password in Basic Authorization header");
chain.doFilter(request, response);
return;
}
String username = authRequest.getName();
this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));
//确定是否需要认证
if (authenticationIsRequired(username)) {
//调用认证管理器进行认证
Authentication authResult = this.authenticationManager.authenticate(authRequest);
//重新设置线程级别的安全上下文
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
//添加记住我令牌
this.rememberMeServices.loginSuccess(request, response, authResult);
//执行认证成功后的操作
onSuccessfulAuthentication(request, response, authResult);
}
}
catch (AuthenticationException ex) {
//先清空线程级别安全上下文
SecurityContextHolder.clearContext();
this.logger.debug("Failed to process authentication request", ex);
//删除记住我令牌
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, ex);
//是否需要跳过异常
if (this.ignoreFailure) {
chain.doFilter(request, response);
}
else {
this.authenticationEntryPoint.commence(request, response, ex);
}
return;
}
chain.doFilter(request, response);
}
- convert(...):通过解析request中的某些参数转为认证对象
public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) {
//用户名+密码是放在Authorization中的
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null) {
return null;
}
header = header.trim();
//没有Basic开头,就返回空
if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) {
return null;
}
//Basic64不能为空
if (header.equalsIgnoreCase(AUTHENTICATION_SCHEME_BASIC)) {
throw new BadCredentialsException("Empty basic authentication token");
}
//去除前面的Basic+空格
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded = decode(base64Token);
//解密
String token = new String(decoded, getCredentialsCharset(request));
//确定用户名和密码的分隔符在哪
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
//封装为认证对象
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(token.substring(0, delim),
token.substring(delim + 1));
result.setDetails(this.authenticationDetailsSource.buildDetails(request));
return result;
}
- authenticationIsRequired(...):确定是否需要认证
private boolean authenticationIsRequired(String username) {
// 只有在用户名不匹配线程级别安全上下文中的用户名,或者用户未经过认证时才重新进行认证
Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
if (existingAuth == null || !existingAuth.isAuthenticated()) {
return true;
}
// 当是用户用户名和密码进行认证的,且用户名不同的时候,进行认证
if (existingAuth instanceof UsernamePasswordAuthenticationToken && !existingAuth.getName().equals(username)) {
return true;
}
// 处理匿名认证对象(AnonymousAuthenticationToken)已经存在的异常情况
// 这种情况不应该经常发生,因为BasicProcessingFilter在过滤器链中比AnonymousAuthenticationFilter更早
// 尽管如此,同时出现AnonymousAuthenticationToken和基本认证,应该表明需要使用BASIC协议进行重新身份认证
// 这种行为也与表单认证和摘要认证一致,如果检测到各自的报头,它们都强制重新进行身份认证(并在此过程中替换/任何现有的AnonymousAuthenticationToken)
return (existingAuth instanceof AnonymousAuthenticationToken);
}