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

2017-12-03  本文已影响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;
    }

效果都是一样的。

上一篇下一篇

猜你喜欢

热点阅读