美文网首页
SpringSecurity开发基于表单的认证(四)

SpringSecurity开发基于表单的认证(四)

作者: 云师兄 | 来源:发表于2017-12-03 17:51 被阅读112次

    认证流程源码级详解

    • 认证处理流程说明
    • 认证结果如何在多个请求之间共享
    • 获取认证用户信息

    认证处理流程说明

    image.png

    分析

    上述这个流程中,当我们使用表单登录,填写完正确的账号和密码登录后,首先会执行到UsernamePasswordAuthenticationFilter类的attemptAuthentication方法中。
    其中UsernamePasswordAuthenticationFilter这个类继承于AbstractAuthenticationProcessingFilter这个过滤器,它的doFIlter方法中调用了attemptAuthentication方法,这个抽象方法具体在UsernamePasswordAuthenticationFilter类中实现,并且UsernamePasswordAuthenticationFilter这个过滤器首先也会调用继承的doFilter方法,这个doFilter方法实现如下:

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
            if (!requiresAuthentication(request, response)) {
                chain.doFilter(request, response);
                return;
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Request is to process authentication");
            }
            Authentication authResult;
            try {
                authResult = attemptAuthentication(request, response);
                if (authResult == null) {
                    // return immediately as subclass has indicated that it hasn't completed
                    // authentication
                    return;
                }
                sessionStrategy.onAuthentication(authResult, request, response);
            }
            catch (InternalAuthenticationServiceException failed) {
                logger.error(
                        "An internal error occurred while trying to authenticate the user.",
                        failed);
                unsuccessfulAuthentication(request, response, failed);
                return;
            }
            catch (AuthenticationException failed) {
                // Authentication failed
                unsuccessfulAuthentication(request, response, failed);
                return;
            }
            // Authentication success
            if (continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
            successfulAuthentication(request, response, chain, authResult);
        }
    

    从这个过滤器的doFilter方法可以看出authResult = attemptAuthentication(request, response);这句话实现的就是登陆认证,如果认证成功后才会执行过滤器链中后续的过滤器,当最终控制器执行完后才会执行认证成功的操作:successfulAuthentication(request, response, chain, authResult);。下面我们先从这个attemptAuthentication方法一个个来讲起。
    attemptAuthentication这个方法的实现如下:

    public Authentication attemptAuthentication(HttpServletRequest request,
                HttpServletResponse response) throws AuthenticationException {
            if (postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                        "Authentication method not supported: " + request.getMethod());
            }
    
            String username = obtainUsername(request);
            String password = obtainPassword(request);
    
            if (username == null) {
                username = "";
            }
    
            if (password == null) {
                password = "";
            }
    
            username = username.trim();
    
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
    
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
    
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    

    在这个方法中获取request请求中的账号和密码,并根据账号和密码生成一个token。这个authRequest 对象的构造函数如下:

        public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
            super(null);
            this.principal = principal;
            this.credentials = credentials;
            setAuthenticated(false);
        }
    

    setAuthenticated(false);说明此时这个token的authenticated属性仍然是false,因为此时还没开始用户认证。认证在最后一句:

    return this.getAuthenticationManager().authenticate(authRequest);
    

    对应着上述流程中接下来要执行的AuthenticationManager接口,它的多个实现类分别对应着不同的认证方式。此处介绍其中一个实现类ProviderManager,它的authenticate方法实现如下:

    public Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
            Class<? extends Authentication> toTest = authentication.getClass();
            AuthenticationException lastException = null;
            Authentication result = null;
            boolean debug = logger.isDebugEnabled();
    
            for (AuthenticationProvider provider : getProviders()) {
                if (!provider.supports(toTest)) {
                    continue;
                }
    
                if (debug) {
                    logger.debug("Authentication attempt using "
                            + provider.getClass().getName());
                }
    
                try {
                    result = provider.authenticate(authentication);
    
                    if (result != null) {
                        copyDetails(authentication, result);
                        break;
                    }
                }
                    ......
    

    在这个方法中 getProviders()方法获取的是所有的AuthenticationProvider,即当前系统所支持的所有认证方式,然后对其进行遍历来判断当前登录的认证方式是否支持(support方法)。当找到支持的认证方式后,调用这个具体处理认证的AuthenticationProvider对象的authenticate方法进行认证,用户认证的逻辑就在其中实现。AuthenticationProvider时一个接口,此处采用其中一个实现类DaoAuthenticationProvider来进行讲解,这个类继承于AbstractUserDetailsAuthenticationProvider类,主要认证的authenticate方法也是后者实现的,具体代码如下:

    public Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
            ......
            UserDetails user = this.userCache.getUserFromCache(username);
    
            if (user == null) {
                cacheWasUsed = false;
                try {
                    user = retrieveUser(username,
                            (UsernamePasswordAuthenticationToken) authentication);
                }
    
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
    
                postAuthenticationChecks.check(user);
    
                return createSuccessAuthentication(principalToReturn, authentication, user);
            ......
    

    这个抽象类的retrieveUser方法又是由它的实现类DaoAuthenticationProvider来实现的:

    protected final UserDetails retrieveUser(String username,
                UsernamePasswordAuthenticationToken authentication)
                throws AuthenticationException {
            UserDetails loadedUser;
    
            try {
                loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            }
            catch (UsernameNotFoundException notFound) {
                if (authentication.getCredentials() != null) {
                    String presentedPassword = authentication.getCredentials().toString();
                    passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
                            presentedPassword, null);
                }
                throw notFound;
            }
              ......
    

    这个方法中的loadedUser = this.getUserDetailsService().loadUserByUsername(username);这句很熟悉,在之前自定义用户信息的时候我们编写了一个UserDetailsService接口实现类:

    @Component
    public class MyUserDetailsService implements UserDetailsService{
        
        private Logger logger =LoggerFactory.getLogger(getClass());
        
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            logger.info("登录用户名:"+username);
            //根据用户名查找用户信息
            //return new User(username,"123456",AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
            String password =passwordEncoder.encode("123456");
            System.out.println("数据库密码是:"+password);
            return new User(username, passwordEncoder.encode("123456"), true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        }
    }
    

    所以这句loadedUser = this.getUserDetailsService().loadUserByUsername(username);就是获取用户信息的。
    当获取到用户信息后,接下来我们接着讲解AbstractUserDetailsAuthenticationProvider类中authenticate方法继续执行的代码:

    preAuthenticationChecks.check(user);
    additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
    postAuthenticationChecks.check(user);
    

    这三个check方法分别检查了isAccountNonLocked,isEnabled,isAccountNonExpired,isCredentialsNonExpired这几个userdetail对象的属性,这个我们在之前已经讲过了,此处不再阐述。
    最后当所有检查都通过后最终执行:

    return createSuccessAuthentication(principalToReturn, authentication, user);
    

    这个createSuccessAuthentication方法执行如下:

        public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
                Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principal = principal;
            this.credentials = credentials;
            super.setAuthenticated(true); // must use super, as we override
        }
    

    上面又生成了一个token,这个token和我们之前在UsernamePasswordAuthenticationFilter类的attemptAuthentication方法中生成的token不同之处在于刚开始生成的token对象的authenticated属性是false,而此处为true,说明已经认证通过。

    接下来我们对认证成功后的流程进行讲解:


    image.png

    从上面可以看到,当认证成功后,先将认证成功的认证信息(token)放到SecurityContext中,之后可以使用SecurityContextHolder对象来获取SecurityContext。SecurityContext对象的作用了保证了token的唯一性(加了Hashcod等),而SecurityContextHolder类是ThreadLocal类的一个封装,由于一个完整的请求和响应都在一个线程中,在线程的不同位置都可以使用SecurityContextHolder读取线程中的SecurityContext。
    注意:最后一个SecurityContextPersistenceFilter是请求首先访问的过滤器,也是响应访问的最后一个过滤器,当请求进来的时候首先检查session中是否有SecurityContext,有就放到线程里;出去的时候将线程里的SecurityContext放到session中。这样就保证了不同的请求通过同一个session拿到同一个认证信息,即解决了认证结果如何在多个请求之间共享的问题。
    我们再回过头来回到AbstractAuthenticationProcessingFilter这个过滤器它的doFIlter方法中,认证成功后最后执行的是successfulAuthentication(request, response, chain, authResult);
    这个方法的实现如下:

        protected void successfulAuthentication(HttpServletRequest request,
                HttpServletResponse response, FilterChain chain, Authentication authResult)
                throws IOException, ServletException {
            if (logger.isDebugEnabled()) {
                logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                        + authResult);
            }
            SecurityContextHolder.getContext().setAuthentication(authResult);
            rememberMeServices.loginSuccess(request, response, authResult);
            // Fire event
            if (this.eventPublisher != null) {
                eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                        authResult, this.getClass()));
            }
            successHandler.onAuthenticationSuccess(request, response, authResult);
        }
    

    上面的SecurityContextHolder.getContext().setAuthentication(authResult);就是我们返回流程中的SecurityContextHolder。

    上述代码中认证成功successHandler是SavedRequestAwareAuthenticationSuccessHandler类的对象,这个类我们之前讲过,曾继承它建立一个处理认证成功的处理类,回忆如下:

    @Component
    public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
        private Logger logger = LoggerFactory.getLogger(getClass());
        private LoginType loginType = LoginType.JSON;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private SecurityProperties securityProperties;
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                Authentication authentication) throws IOException, ServletException {
            //Authentication接口封装认证信息
            logger.info("登录成功");
            if(loginType.equals(securityProperties.getBrowser().getLoginType())){
                response.setContentType("application/json;charset=UTF-8");
                //将authentication认证信息转换为json格式的字符串写到response里面去
                response.getWriter().write(objectMapper.writeValueAsString(authentication));
            }
            else{
                super.onAuthenticationSuccess(request, response, authentication);
            }
        }
    }
    

    所以如果我们定义了上面这个MyAuthenticationSuccessHandler类,最终执行时successHandler.onAuthenticationSuccess(request, response, authResult);中的successHandler就是我们自己写的MyAuthenticationSuccessHandler对象。

    获取认证用户信息

    下面我们添加一个restful api,使用SecurityContextHolder获得认证信息:

        @GetMapping("/me")
        public Object getCurrentUser(){
            return SecurityContextHolder.getContext().getAuthentication();
        }
    

    首先表单登录成功后将SecurityContext认证信息保存到线程中,返回的时候保存到session中,再次访问http://localhost:8080:/user/me的时候页面返回认证信息:

    {"authorities":[{"authority":"admin"}],"details":{"remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"AE0BEC5D6FA979F789C1A9A6A288EE70"},"authenticated":true,"principal":{"password":null,"username":"yby","authorities":[{"authority":"admin"}],"accountNonExpired":true,"accountNonLocked":true,"credentialsNonExpired":true,"enabled":true},"credentials":null,"name":"yby"}
    

    上面这个restful api也可以写作:

        @GetMapping("/me")
        public Object getCurrentUser(Authentication authentication){
            return authentication;
        }
    

    效果都是一样的。

    相关文章

      网友评论

          本文标题:SpringSecurity开发基于表单的认证(四)

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