本文结合macro大神mall项目以及以下三篇优秀文章总结而成:
(1)Spring Security 工作原理概览
(2)spring security——基本介绍(一)
(3)Security身份认证之UserDetailsService
一、SpringSecurity过滤器链
用户发送请求会经过Security过滤器链,通过UsernamePassword
AuthenticationFilter时检查username和password是否在数据库中。
二、身份认证流程图
三、身份认证代码跟踪
1.AbstractAuthenticationProcessingFilter
身份认证时首先经过该过滤器执行doFilter方法,其中 调用 requiresAuthentication方法判断是否需要验证,如需要则会调用验证方法(核心)attemptAuthentication,此时有三种结果:
- 返回Null,表示身份验证不完整。假设子类做了一些必要的工作(如重定向)来继续处理验证,方法将立即返回。假设后一个请求将被这种方法接收,其中返回的Authentication对象不为空。
- 返回一个 Authentication 对象。校验成功,调用successfulAuthentication方法。
- 验证时发生 AuthenticationException。验证失败,调用unsuccessfulAuthentication方法。
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
//......
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
this.unsuccessfulAuthentication(request, response, var9);
return;
}
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
this.successfulAuthentication(request, response, chain, authResult);
}
}
//......
}
2.UsernamePasswordAuthenticationFilter
由上一步可知,校验核心在于调用了attemptAuthentication方法,但是AbstractAuthenticationProcessingFilter类中并没有实现,而是交由其子类UsernamePasswordAuthenticationFilter实现。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//......
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
//......
}
发现该方法将request中提取的username和password封装成了token,并调用了getAuthenticationManager方法,委托AuthenticationManager接口进行验证(authenticate方法)。
3.ProviderManager
默认情况下注入 Spring 容器的 AuthenticationManager 是 ProviderManager。ProviderManager实现了AuthenticationManager接口,。ProviderManager管理了许多AuthenticationProvider (接口),其验证逻辑是通过getProviders方法得到一系列AuthenticationProvider ,进行验证(authenticate方法),直到有一个验证成功,不继续验证。不难看出其核心代码是
Authentication result = provider.authenticate(authentication);
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
//......
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//......
Iterator var8 = this.getProviders().iterator();
while(var8.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var8.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var13) {
this.prepareException(var13, authentication);
throw var13;
} catch (AuthenticationException var14) {
lastException = var14;
}
}
}
//......
}
//......
}
4.AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider类实现了AuthenticationProvider,实现了authenticate方法。
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
//......
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("User '" + username + "' not found");
if (this.hideUserNotFoundExceptions) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
throw var6;
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
//......
}
//......
}
其核心代码为:
UserDetails user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
该方法交由其子类DaoAuthenticationProvider实现。
5.DaoAuthenticationProvider
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
//......
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
//......
}
其核心代码为:
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
发现调用了UserDetailsService接口进行具体身份验证逻辑,用户可以自定义配置其实现类以及UserDetails的实现类(拓展除了username和password以外的其他字段,如permissions、roles)。
四、自定义实现并配置UserDetailsService接口
1.自定义实现与安全配置
- configure(HttpSecurity httpSecurity):用于配置需要拦截的url路径、jwt过滤器及出异常后的处理器;
- configure(AuthenticationManagerBuilder auth):用于配置UserDetailsService及PasswordEncoder;
- UserDetailsService:SpringSecurity定义的核心接口,用于根据用户名获取用户信息,需要自行实现;
- UserDetails:SpringSecurity定义用于封装用户信息的类(主要是用户信息和权限),需要自行实现;
- JwtAuthenticationTokenFilter:在用户名和密码校验前添加的过滤器,如果有jwt的token,会自行根据token信息进行登录,可校验token是否过期。
- UserDetailsService中使用了业务层逻辑,详情请自行学习macro大神mall项目。
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//......
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
// 自定义权限拦截器JWT过滤器
registry.
.and()
.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//......
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
@Bean
public UserDetailsService userDetailsService() {
//获取登录用户信息
return username -> {
UmsAdmin admin = adminService.getAdminByUsername(username);
if (admin != null) {
List<UmsPermission> permissionList = adminService.getPermissionList(admin.getId());
return new AdminUserDetails(admin,permissionList);
}
throw new UsernameNotFoundException("用户名或密码错误");
};
}
//......
}