文章目录
认证流程分析
3.1登录流程分析
3.1.1AuthenticationManager
// 默认使用的实现是ProviderManager
public interface AuthenticationManager {
// 对传入的Authentication对象进行身份认证,此时传入的参数只有用户名/密码等简单的属性。
// 如果认证成功,返回的Authentication的属性会得到完全填充,包括用户所具备的角色信息
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
3.1.2AuthenticationProvider
/**
* 针对不同的身份类型执行具体的身份认证。例如,DaoAuthenticationProvider用来支持用户名/密码登录认证,
* RememberMeAuthenticationProvider用来支持记住我的认证。
*/
public interface AuthenticationProvider {
// 执行具体的身份认证
Authentication authenticate(Authentication authentication) throws AuthenticationException;
// 判断当前的AuthenticationProvider是否支持对应的身份类型
boolean supports(Class<?> authentication);
}
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
// 不启用缓存对象
private UserCache userCache = new NullUserCache();
// 是否强制将Principal对象当成字符串来处理,默认为false
private boolean forcePrincipalAsString = false;
// 是否隐藏用户名查找失败的异常,默认为true,这样在用户名查找失败抛出UsernameNotFoundException异常时,
// 将其自动隐藏,转而通过一个BadCredentialsException异常来代替,起到一定的迷惑作用
protected boolean hideUserNotFoundExceptions = true;
// 用于做用户状态检查,在用户认证过程中,需要检查用户状态是否正常,例如是否被锁定、过期、可用等
private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
// 负责在密码校验成功后,检查密码是否过期
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 获取用户名
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
// 从缓存加载用户
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 从数据库加载用户,相关实现在DaoAuthenticationProvider中
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
try {
// 进行用户状态检查
this.preAuthenticationChecks.check(user);
// 进行密码校验,相关实现在DaoAuthenticationProvider中
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
// 检查密码是否过期
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 创建一个认证后的UsernamePasswordAuthenticationToken对象并返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
}
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
// 用户查找失败时的默认密码,主要是为了避免旁道攻击(Side-channel attack)。如果根据用户名查找用户失败,
// 就直接抛出异常而不进行密码对比,那么黑客经过大量测试,就会发现有的请求耗费时间明显小于其他请求,那么进而得出,
// 该请求的用户名是一个不存的用户名,这样就可以获取到系统信息
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
}
3.1.3ProviderManager
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
// 当前认证过程抛出的异常
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
// 发布登录成功事件需要parentResult为null。如果parentResult不为null,表示在parent中已经认证成功了,
// 认证成功的事件也已经在parent中发布出去了,这样会导致认证成功的事件重复发布
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
}
3.1.4AbstractAuthenticationProcessingFilter
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 判断当前请求是否是登录认证请求,如果不是,则直接继续走剩余的过滤器
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 获取一个经过认证后的Authentication对象,具体实现在子类中,例如UsernamePasswordAuthenticationFilter
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
// 处理session并发问题
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
// 判断请求是否还需要继续向下走,默认是false
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (this.requiresAuthenticationRequestMatcher.matches(request)) {
return true;
}
if (this.logger.isTraceEnabled()) {
this.logger
.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
}
return false;
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 向SecurityContextHolder中存入用户信息
SecurityContextHolder.getContext().setAuthentication(authResult);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
// 处理cookie
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
// 发布认证成功事件,这个事件类型是InteractiveAuthenticationSuccessEvent,表示通过一些自动交互的方式认证成功,
// 例如通过remember-me的方式登录
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 调用认证成功的回调方法
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
// 从SecurityContextHolder中清除数据
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
// 处理cookie
this.rememberMeServices.loginFail(request, response);
// 调用认证失败的回调方法
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
}
3.2配置多个数据源
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Primary
UserDetailsService userDetailsService01() {
return new InMemoryUserDetailsManager(User.builder().username("javaboy").password("{noop}123").roles("admin").build());
}
@Bean
UserDetailsService userDetailsService02() {
return new InMemoryUserDetailsManager(User.builder().username("sang").password("{noop}123").roles("user").build());
}
@Override
@Bean // 额外注意
public AuthenticationManager authenticationManagerBean() throws Exception {
DaoAuthenticationProvider dao1 = new DaoAuthenticationProvider();
dao1.setUserDetailsService(userDetailsService01());
DaoAuthenticationProvider dao2 = new DaoAuthenticationProvider();
dao2.setUserDetailsService(userDetailsService02());
return new ProviderManager(dao1, dao2);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
// 省略
}
}
3.3添加登录验证码
@Configuration
public class KaptchaConfig {
@Bean
Producer kaptcha() {
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "150");
properties.setProperty("kaptcha.image.height", "50");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
properties.setProperty("kaptcha.textproducer.char.length", "4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
@Controller
public class HelloController {
@Autowired
Producer producer;
@GetMapping("/vc.jpg")
public void getVerifyCode(HttpServletResponse response, HttpSession session) throws IOException {
response.setContentType("image/jpeg");
String text = producer.createText();
// 将生成的验证码内容存入到session,方便之后进行判断
session.setAttribute("kaptcha", text);
BufferedImage image = producer.createImage(text);
try (ServletOutputStream outputStream = response.getOutputStream()) {
ImageIO.write(image, "jpg", outputStream);
}
}
}
public class KaptchaAuthenticationProvider extends DaoAuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String kaptcha = request.getParameter("kaptcha");
String sessionKaptcha = (String) request.getSession().getAttribute("kaptcha");
if (kaptcha != null && kaptcha.equals(sessionKaptcha)) {
// 如果验证码输入正确,则继续执行父类的authenticate方法进行下一步的验证
return super.authenticate(authentication);
}
throw new AuthenticationServiceException("验证码输入错误");
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 1. 配置UserDetailsService配置数据源
@Bean
UserDetailsService userDetailsService01() {
return new InMemoryUserDetailsManager(User.builder().username("javaboy").password("{noop}123").roles("admin").build());
}
// 2. 提供AuthenticationProvider实例并配置UserDetailsService
@Bean
AuthenticationProvider kaptchaAuthenticationProvider() {
KaptchaAuthenticationProvider provider = new KaptchaAuthenticationProvider();
provider.setUserDetailsService(userDetailsService01());
return provider;
}
// 3. 重写authenticationManagerBean方法,提供一个自己的ProviderManager并使用自定义的AuthenticationProvider实例
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return new ProviderManager(kaptchaAuthenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/vc.jpg").permitAll()
.anyRequest().authenticated()
// 省略
}
}