0
点赞
收藏
分享

微信扫一扫

3.认证流程分析

小铺有酒一两不够 2022-02-15 阅读 82
javaspring

文章目录

认证流程分析

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ku0s8gOL-1644905516397)(evernotecid://95126015-5853-4CC7-AE3B-105960046EC5/appyinxiangcom/15766490/ENResource/p724)]@w=600

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B8pHXo2i-1644905516397)(evernotecid://95126015-5853-4CC7-AE3B-105960046EC5/appyinxiangcom/15766490/ENResource/p725)]@w=600

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()
                // 省略
    }
}
举报

相关推荐

0 条评论