美文网首页
铁犀牛学习笔记(一)

铁犀牛学习笔记(一)

作者: 言夕枣 | 来源:发表于2018-11-21 15:58 被阅读0次

问题:Ironrhino用户登录中的密码校验流程、OAuth认证、token校验实现过程

用户登录认证过程总结:

  1. 用户使用用户名和密码进行登录;
  2. Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken;
  3. 将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证;
  4. AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的Authentication 对象;
  5. 通过调用 SecurityContextHolder.getContext().setAuthentication(...)AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext

Spring Security已经实现了用户的认证和授权功能,铁犀牛在此基础上,扩展了动态验证码登录的方式。

代码1显示了在用户输入用户名和密码点击登录后的过程,UsernamePasswordAuthenticationToken会对用户名和密码进行封装,然后传递到AuthenticationManager的方法authenticate中。AuthenticationManager 是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法 authenticate(),该方法只接收一个代表认证请求的 Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象进行返回。

代码1: LoginAction.java

public class LoginAction extends BaseAction {
    ...
    public String execute() throws Exception {
        ...
        ...
        UsernamePasswordAuthenticationToken attempt = new UsernamePasswordAuthenticationToken(username, password);
        WebAuthenticationDetailsSource wads = ReflectionUtils.getFieldValue(usernamePasswordAuthenticationFilter,
                "authenticationDetailsSource");
        attempt.setDetails(wads.buildDetails(request));
        authResult = authenticationManager.authenticate(attempt);
        ...
    }
}

在 Spring Security 中,AuthenticationManager 的默认实现是 ProviderManager,代码2展示的是ProviderManager部分代码。

它不直接自己处理认证请求,而是委托给其所配置的 AuthenticationProvider 列表,然后会依次使用每一个 AuthenticationProvider 进行认证,如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,之后的 AuthenticationProvider 将不再继续认证。然后直接以该 AuthenticationProvider 的认证结果作为ProviderManager 的认证结果。如果所有的 AuthenticationProvider 的认证结果都为 null,则表示认证失败,将抛出一个 ProviderNotFoundException

代码2: ProviderManager.java

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    ...
    private List<AuthenticationProvider> providers;
    ...
    ...
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        ...
        Iterator e = this.getProviders().iterator();
        while (e.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider) e.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 (AccountStatusException arg10) {
                    this.prepareException(arg10, authentication);
                    throw arg10;
                } catch (InternalAuthenticationServiceException arg11) {
                    this.prepareException(arg11, authentication);
                    throw arg11;
                } catch (AuthenticationException arg12) {
                    lastException = arg12;
                }
            }
        }
        ...
        if (result != null) {
            ...
        } else {
            if (lastException == null) {
                lastException = new ProviderNotFoundException(
                        this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()},
                                "No AuthenticationProvider found for {0}"));
            }
            this.prepareException((AuthenticationException) lastException, authentication);
            throw lastException;
        }
    }
    ...
}

AbstractUserDetailsAuthenticationProvider实现了AuthenticationProvider 接口,在authenticate方法中首先会根据username去加载对应的UserDetails实例(UserDetails 接口包含了用户的信息,包括用户名、密码、权限、是否启用、是否被锁定、是否过期等);然后运行到this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication),将UserDetails实例中的密码与UsernamePasswordAuthenticationToken用户输入认证密码进行对比,可继续查看AbstractUserDetailsAuthenticationProvider子类DaoAuthenticationProvider

代码3: AbstractUserDetailsAuthenticationProvider.java

public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    ...
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                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 arg5) {
                this.logger.debug("User \'" + username + "\' not found");
                if (this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages
                            .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }

                throw arg5;
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }
        try {
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
        }
        ...
    }   
    ...         
}       

DaoAuthenticationProviderProviderManagerAuthenticationProvider接口的默认实现类,而DaoAuthenticationProvider也是不直接操作数据库的,它把任务委托给了UserDetailService;前面在父类AbstractUserDetailsAuthenticationProvider中,authenticate()方法是先加载UserDetails实例,这部分在retrieveUser()方法中实现,可以从代码4中看到UserDetails 是通过 UserDetailsServiceloadUserByUsername() 方法进行加载的,Spring Security 提供了UserDetailsService 的默认实现类JdbcDaoImpl 来支持从数据库中加载用户信息。

在我们自己的项目中只需要实现UserDetailsService接口,并重写loadUserByUsername() 方法返回一个UserDetails 对象。

代码4: DaoAuthenticationProvider.java

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    ...
    private PasswordEncoder passwordEncoder;
    private UserDetailsService userDetailsService;
    ...
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages
                        .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }
    ...
    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        this.prepareTimingAttackProtection();
        try {
            UserDetails ex = this.getUserDetailsService().loadUserByUsername(username);
            if (ex == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            } else {
                return ex;
            }
        } catch (UsernameNotFoundException arg3) {
            this.mitigateAgainstTimingAttack(authentication);
            throw arg3;
        } catch (InternalAuthenticationServiceException arg4) {
            throw arg4;
        } catch (Exception arg5) {
            throw new InternalAuthenticationServiceException(arg5.getMessage(), arg5);
        }
    }
    ...
}

继续查看additionalAuthenticationChecks()方法中的代码,最终将两个密码的对比进入了PasswordEncodermatches()方法中。
铁犀牛框架将PasswordEncoder默认实现修改为org.ironrhino.core.spring.security.password.MixedPasswordEncoder

<bean id="passwordEncoder" class="org.ironrhino.core.spring.security.password.MixedPasswordEncoder" />

MixedPasswordEncoder提供了多种加密方式,根据密码的长度进行判断。

matches(...)方法的返回结果就是认证结果,在这里完成了最终的密码校验。

代码5: MixedPasswordEncoder.java

public class MixedPasswordEncoder implements PasswordEncoder {
    ...
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (rawPassword == null || encodedPassword == null)
            return false;
        switch (encodedPassword.length()) {
        case 32:
            return matches(Hex.decode(encodedPassword), legacyDigest(rawPassword.toString()));
        case 48:
            byte[] digested = Hex.decode(encodedPassword);
            byte[] salt = EncodingUtils.subArray(digested, 0, saltGenerator.getKeyLength());
            return matches(digested, digest(rawPassword, salt, true));
        case 80:
            digested = Hex.decode(encodedPassword);
            salt = EncodingUtils.subArray(digested, 0, saltGenerator.getKeyLength());
            return matches(digested, digest(rawPassword, salt, false));
        default:
            return false;
        }
    }
    ...
}

验证码登录

在铁犀牛框架中,扩展了动态验证码验证的功能。新建类VerificationAuthenticationProvider继承了DaoAuthenticationProvider,并且重写了additionalAuthenticationChecks()方法。

在重写的additionalAuthenticationChecks()方法中先判断UserDetails对象是否实现了VerificationAware接口,若符合则进入验证码的对比逻辑,否则使用Spring Security默认实现(代码4)。

在我们的项目中,若要使用验证码登录,则需要定义的用户类实现VerificationAware接口。例如:

  • appserverCustomer类同时实现了UserDetailsVerificationAware,因此在additionalAuthenticationChecks中会进入验证码的流程;
  • marketserverEmployee只实现了UserDetails接口,因此在additionalAuthenticationChecks中会默认使用密码验证;

代码6: VerificationAuthenticationProvider.java

public class VerificationAuthenticationProvider extends DaoAuthenticationProvider {
    
    private VerificationManager verificationManager;
    private AuthenticationManager authenticationManager;
    ...
    @PostConstruct
    public void init() {
        List<AuthenticationProvider> providers = ((ProviderManager) authenticationManager).getProviders();
        Iterator<AuthenticationProvider> it = providers.iterator();
        while (it.hasNext()) {
            if (it.next() instanceof DaoAuthenticationProvider) {
                it.remove();
                break;
            }
        }
        providers.add(0, this);
    }
    
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (userDetails instanceof VerificationAware) {
            VerificationAware user = (VerificationAware) userDetails;
            if (user.isVerificationRequired()) {
                verificationManager.verify(user);
                if (verificationCodeQualified && !user.isPasswordRequired())
                    return; // skip check password
            }
        }
        super.additionalAuthenticationChecks(userDetails, authentication);
    }
}

从代码6可以看到,当使用验证码的方式时,会将VerificationAware对象传递给VerificationManagerverify()方法,在铁犀牛框架中,DefaultVerificationManagerVerificationManager的默认实现类,
从代码7可以看到,DefaultVerificationManagersend()方法主要是对UserDetails对象做了一些条件判断,若满足则调用VerificationService接口的send()方法;verify()方法则是将用户输入的验证码传给VerificationService接口的verify()方法,具体对验证码的管理便是在VerificationService中实现的。

代码7: DefaultVerificationManager.java

public class DefaultVerificationManager implements VerificationManager {
    ...
    @Override
    public void send(String username) throws ReceiverNotFoundException {
        UserDetails ud = userDetailsService.loadUserByUsername(username);
        String receiver = null;
        if (ud instanceof VerificationAware)
            receiver = ((VerificationAware) ud).getReceiver();
        if (StringUtils.isBlank(receiver))
            throw new ReceiverNotFoundException("No receiver found for: " + ud.getUsername());
        verificationService.send(receiver);
    }

    @Override
    public void verify(VerificationAware user) throws WrongVerificationCodeException {
        String username = user.getUsername();
        String receiver = user.getReceiver();
        if (StringUtils.isBlank(receiver))
            throw new ReceiverNotFoundException("No receiver found for: " + username);
        String verificationCode = RequestContext.getRequest().getParameter("verificationCode");
        if (verificationCode == null)
            verificationCode = RequestContext.getRequest().getParameter("password");
        boolean verified = verificationService.verify(receiver, verificationCode);
        if (!verified)
            throw new WrongVerificationCodeException("Wrong verification code: " + verificationCode);
    }
}

VerificationService只有send()verify()两个方法,分别是产生/缓存获取和对比验证码,而发送验证码的任务又委托给VerificationCodeNotifier接口来实现,因此在我们的项目中只需要实现VerificationCodeNotifier接口,重写send()方法。

代码8是VerificationService接口的默认实现类DefaultVerificationService的代码。

通常在输入手机号和验证码进行认证之前,要先获取验证码,因此会先触发send()方法;在send()方法中,先是尝试从缓存中获取,这里有对重复发送的控制;若缓存中没有则产生验证码,并且存入缓存;最后将验证码通过VerificationCodeNotifier接口的send()方法发送出去。

verify方法则直接将用户输入的验证码和缓存中取出相应的验证码进行对比,返回验证结果;同时有对验证次数的控制,当验证次数达到阈值后,会从缓存中删除验证码。

代码8: DefaultVerificationService.java

public class DefaultVerificationService implements VerificationService {
    ...
    private CacheManager cacheManager;
    private VerificationCodeGenerator verficationCodeGenerator;
    private List<VerificationCodeNotifier> verificationCodeNotifiers;
    ...
    public void send(String receiver, final String verficationCode) throws ReceiverNotFoundException {
        String codeToSend;
        if (verficationCode == null) {
            codeToSend = (String) cacheManager.get(receiver, CACHE_NAMESPACE);
            if (codeToSend != null && cacheManager.exists(receiver + SUFFIX_RESEND, CACHE_NAMESPACE)) {
                log.warn("{} is trying resend within cooldown time", receiver);
                return;
            }
        } else {
            codeToSend = verficationCode;
            cacheManager.put(receiver, codeToSend, expiry, TimeUnit.SECONDS, CACHE_NAMESPACE);
        }
        if (codeToSend == null || !reuse) {
            codeToSend = verficationCodeGenerator.generator(receiver, length);
            cacheManager.put(receiver, codeToSend, expiry, TimeUnit.SECONDS, CACHE_NAMESPACE);
        }
        for (VerificationCodeNotifier notifier : verificationCodeNotifiers)
            notifier.send(receiver, codeToSend);
        cacheManager.put(receiver + SUFFIX_RESEND, "", resendInterval, TimeUnit.SECONDS, CACHE_NAMESPACE);
    }

    public boolean verify(String receiver, String verificationCode) {
        boolean verified = verificationCode != null
                && verificationCode.equals(cacheManager.get(receiver, CACHE_NAMESPACE));
        if (!verified) {
            throttleService.delay("verification:" + receiver, verifyInterval, TimeUnit.SECONDS, verifyInterval / 2);
            Integer times = (Integer) cacheManager.get(receiver + SUFFIX_THRESHOLD, CACHE_NAMESPACE);
            if (times == null)
                times = 1;
            else
                times = times + 1;
            if (times >= maxAttempts) {
                cacheManager.delete(receiver, CACHE_NAMESPACE);
                cacheManager.delete(receiver + SUFFIX_THRESHOLD, CACHE_NAMESPACE);
            } else {
                cacheManager.put(receiver + SUFFIX_THRESHOLD, times, expiry, TimeUnit.SECONDS, CACHE_NAMESPACE);
            }
        } else {
            cacheManager.delete(receiver, CACHE_NAMESPACE);
            cacheManager.delete(receiver + SUFFIX_THRESHOLD, CACHE_NAMESPACE);
        }
        return verified;
    }
}

参考资料

token校验过程

  1. 所有的请求都会先经过OAuthHandlerhandle()方法,从url参数或请求头中获取到token,然后将token传递给OauthAuthorizationServiceget()方法返回一个OAuthAuthorization对象;
    1.1. 在OauthAuthorizationService的实现类OAuthAuthorizationServiceImpl中,首先会委托OAuthAuthorizationProvider去获取token信息(铁犀牛中未实现OAuthAuthorizationProvider接口),然后再尝试通过OauthManager获取;
    1.2. 铁犀牛中默认使用DefaultOAuthManager调用 AuthorizationManager从数据库中获取token信息;
  2. 判断上一步中返回的OAuthAuthorization对象的状态,若符合则认证成功,再使用userDetailsService.loadUserByUsername(...)根据用户名获取到对应的UserDetails对象;
  3. 判断用户的状态,满足要求则将UserDetails对象及权限信息封装到Authentication对象中;
  4. 通过调用 SecurityContextHolder.getContext().setAuthentication(...)Authentication 对象赋予给当前的 SecurityContext

相关文章

  • 铁犀牛学习笔记(一)

    问题:Ironrhino用户登录中的密码校验流程、OAuth认证、token校验实现过程 用户登录认证过程总结: ...

  • 数字人NURBS建模课堂:犀牛入门-3

    关于犀牛的基本概念的学习笔记。 电子文档下载

  • 老铁和犀牛

    老铁不姓铁,但酷爱铁人王进喜,人送“老铁”。老铁平生最大的爱好就是钻研电脑,那砖头厚的专业书看了不下十遍,练就了一...

  • 校本化HTTP-原生js

    这个笔记是我学习js犀牛书和一个师姐的慕课网学习笔记和js高级编程三个东西的总结 1,基本概念 超文本传输协议(H...

  • 新版溪流学习笔记要求发布

    再次将二班的9月15日学习的笔记仔细阅读之后,彻底被赋能了。 赋了什么能? 那就是作为一头犀牛,该如何做学习笔记。...

  • 2020.10.14心对话

    关于学习期间怎么安排犀牛的事情 我:如果要是你晚上没办法回家的话,我都打算把犀牛带到昆山去学习。 犀牛爸:我觉得还...

  • JavaScript学习目录之语言核心

    本学习笔记旨在帮助自己树立核心概念,参考书籍为犀牛书; JavaScript语言核心## 2词法结构 2.1字符集...

  • 20170405学习笔记

    20170405学习笔记 (1)Qingming brings rush of rail travel清明节带来铁...

  • 我的读书笔记之商业经济(一)《灰犀牛》等

    我的读书笔记之商业经济(一)《灰犀牛》等目录:《灰犀牛》《反脆弱》《一课经济学》《从0到1》《巴菲特之道》《聪明的...

  • 路江勇:危机本质思维与危机应对

    学习笔记 一、危机的本质思维 1、危机是什么? 危机=不确定性(黑天鹅)*不连续性(灰犀牛)。黑天鹅事件,是形容小...

网友评论

      本文标题:铁犀牛学习笔记(一)

      本文链接:https://www.haomeiwen.com/subject/wiscqqtx.html