美文网首页Spring Security
Spring技术实战-Spring Security原理以及实战

Spring技术实战-Spring Security原理以及实战

作者: 洛神灬殇 | 来源:发表于2021-11-26 20:28 被阅读0次

    前提介绍

    本篇文章给大家带来的内容是关于Spring Security原理的介绍,对实际业务开发会有一定的参考价值,希望对你有所帮助。所谓知彼知己方能百战百胜,用Spring Security来满足我们的需求最好了解其原理,这样才能随意拓展,本篇文章主要记录 Spring Security 的基本运行流程。

    过滤器的原理机制

    Spring Security基本都是通过过滤器来完成配置的身份认证、权限认证以及登出。

    Spring Security在Servlet的过滤链(filter chain)中注册了一个过滤器 FilterChainProxy,它会把请求代理到Spring Security自己维护的多个过滤链,每个过滤链会匹配一些URL,如果匹配则执行对应的过滤器,过滤链是有顺序的,一个请求只会执行第一条匹配的过滤链。

    Spring Security 的配置本质上就是新增、删除、修改过滤器。


    image

    默认情况下系统帮我们注入的这15个过滤器,分别对应配置不同的需求。

    认证控制过滤器UsernamePasswordAuthenticationFilter

    接下来我们重点是分析下 UsernamePasswordAuthenticationFilter 这个过滤器,他是用来使用用户名和密码登录认证的过滤器,但是很多情况下我们的登录不止是简单的用户名和密码,又可能是用到第三方授权登录,这个时候我们就需要使用自定义过滤器,当然这里不做详细说明,只是说下自定义过滤器怎么注入。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(...);
        ...
     }
    

    身份认证流程

    在开始身份认证流程之前我们需要了解下几个基本概念:

    SecurityContextHolder

    SecurityContextHolder存储SecurityContext对象。SecurityContextHolder是一个存储代理,有三种存储模式分别是:

    • MODE_THREADLOCAL:SecurityContext 存储在线程中。
    • MODE_INHERITABLETHREADLOCAL:SecurityContext 存储在线程中,但子线程可以获取到父线程中的 SecurityContext。
    • MODE_GLOBAL:SecurityContext 在所有线程中都相同。

    SecurityContextHolder默认使用MODE_THREADLOCAL模式,SecurityContext 存储在当前线程中。

    调用SecurityContextHolder时不需要显示的参数传递,在当前线程中可以直接获取到SecurityContextHolder对象。

    SecurityContext context = SecurityContextHolder.getContext();
    Authentication authentication = context.getAuthentication();
    

    Authentication

    Authentication即验证,表明当前用户是谁。什么是验证,比如,一组用户名和密码就是验证,当然错误的用户名和密码也是验证,只不过 Spring Security会校验失败。

    Authentication接口
    public interface Authentication extends Principal, Serializable {
           Collection getAuthorities();
           Object getCredentials();
           Object getDetails();
           Object getPrincipal();
           boolean isAuthenticated();
           void setAuthenticated(boolean isAuthenticated);
    }
    
    • getAuthorities():获取用户权限,一般情况下获取到的是用户的角色信息。

    • getCredentials():获取证明用户认证的信息,通常情况下获取到的是密码等信息,不过登录成功就会被移除。

    • getDetails():获取用户的额外信息,比如 IP 地址、经纬度等

    • getPrincipal():获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (暂时理解为,当前应用用户对象的扩展)

    • isAuthenticated():获取当前 Authentication 是否已认证

    • setAuthenticated:设置当前 Authentication 是否已认证

    AuthenticationManager ProviderManager AuthenticationProvider

    其实这三者很好区分:

    • AuthenticationManager:主要就是为了完成身份认证流程。
    • ProviderManager:AuthenticationManager 接口的具体实现类
      • ProviderManager 里面有个记录AuthenticationProvider对象的集合属性 providers
    • AuthenticationProvider接口类里有两个方法
    public interface AuthenticationProvider {
        //实现具体的身份认证逻辑,认证失败抛出对应的异常
        Authentication authenticate(Authentication authentication)
                throws AuthenticationException;
        //该认证类是否支持该 Authentication 的认证
        boolean supports(Class authentication);
    }
    

    接下来就是遍历 ProviderManager 里面的 providers 集合,找到和合适的 AuthenticationProvider 完成身份认证。

    UserDetailsService UserDetails

    在 UserDetailsService 接口中只有一个简单的方法

    public interface UserDetailsService {
        //根据用户名查到对应的 UserDetails 对象
        UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
    }
    

    执行认证流程

    慢慢分析在运行到 UsernamePasswordAuthenticationFilter过滤器的时候。

    首先是进入其父类AbstractAuthenticationProcessingFilter的doFilter()方法中

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {
        ...
        //首先配对是不是配置的身份认证的URI,是则执行下面的认证,不是则跳过
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        ...
        Authentication authResult;
        try {
            //关键方法, 实现认证逻辑并返回 Authentication, 由其子类 UsernamePasswordAuthenticationFilter 实现
            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) {
            //认证失败调用
            unsuccessfulAuthentication(request, response, failed);
            return;
       }catch (AuthenticationException failed) {
            //认证失败调用
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        // Authentication success
        if (continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }
        //认证成功调
        successfulAuthentication(request, response, chain, authResult);
    }
    

    认证失败处理逻辑

    protected void unsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response, AuthenticationException failed)throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        ...
        rememberMeServices.loginFail(request, response);
        //该 handler 处理失败界面跳转和响应逻辑
        failureHandler.onAuthenticationFailure(request, response, failed);
    }
    

    默认配置的失败处理 handler是 SimpleUrlAuthenticationFailureHandler,可自定义。

    public class SimpleUrlAuthenticationFailureHandler implements
            AuthenticationFailureHandler {
        ...
        public void onAuthenticationFailure(HttpServletRequest request,
                HttpServletResponse response, AuthenticationException exception)throws IOException, ServletException {
            //没有配置失败跳转的URL则直接响应错误
            if (defaultFailureUrl == null) {
                logger.debug("No failure URL set, sending 401 Unauthorized error");
                response.sendError(HttpStatus.UNAUTHORIZED.value(),
                    HttpStatus.UNAUTHORIZED.getReasonPhrase());
            }else {
                //否则
                //缓存异常
                saveException(request, exception);
                //根据配置的异常页面是重定向还是转发进行不同方式跳转
                if (forwardToDestination) {
                    logger.debug("Forwarding to " + defaultFailureUrl);
                    request.getRequestDispatcher(defaultFailureUrl)
                            .forward(request, response);
                }else {
                    logger.debug("Redirecting to " + defaultFailureUrl);
                    redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
                }
            }
        }
        //缓存异常,转发则保存在request里面,重定向则保存在session里面
        protected final void saveException(HttpServletRequest request,
                AuthenticationException exception) {
            if (forwardToDestination) {        
            request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
            }else {
                HttpSession session = request.getSession(false);
                if (session != null || allowSessionCreation) {    
               request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,exception);
                }
            }
        }
    }
    

    这里做下小拓展:用系统的错误处理handler,指定认证失败跳转的URL,在MVC里面对应的URL方法里面可以通过key从request或session里面拿到错误信息,反馈给前端。

    认证成功处理逻辑

    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
        ...
        //这里要注意很重要,将认证完成返回的Authentication保存到线程对应的 `SecurityContext` 中
        SecurityContextHolder.getContext().setAuthentication(authResult);
        rememberMeServices.loginSuccess(request, response, authResult);
        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }
        //该 handler 就是为了完成页面跳转
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }
    

    这里默认配置的成功处理 handler 是 SavedRequestAwareAuthenticationSuccessHandler,里面的代码就不做具体展开了,反正是跳转到指定的认证成功之后的界面,可自定义。

    身份认证详情

    public class UsernamePasswordAuthenticationFilter extends
            AbstractAuthenticationProcessingFilter {
        ...
        public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
        public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
        private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
        private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
        private boolean postOnly = true;
        ...
    
        //开始身份认证逻辑
        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();
            //先用前端提交过来的 username 和 password 封装一个简易的 AuthenticationToken
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
            //具体的认证逻辑还是交给 AuthenticationManager 对象的 authenticate(..) 方法完成,接着往下看
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
    

    由源码断点跟踪得知,最终解析是由 AuthenticationManager 接口实现类 ProviderManager 来完成

    public class ProviderManager implements AuthenticationManager, MessageSourceAware,InitializingBean {
        ...
        private List providers = Collections.emptyList();
        ...
        public Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
            ....
            //遍历所有的 AuthenticationProvider, 找到合适的完成身份验证
            for (AuthenticationProvider provider : getProviders()) {
                if (!provider.supports(toTest)) {
                    continue;
                }
                ...
                try {
                    //进行具体的身份验证逻辑, 这里使用到的是 DaoAuthenticationProvider, 具体逻辑记着往下看
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        copyDetails(authentication, result);
                        break;
                    }
                }catch
                ...
            }
            ...
            throw lastException;
        }
    }
    

    DaoAuthenticationProvider

    DaoAuthenticationProvider继承自 AbstractUserDetailsAuthenticationProvider实现AuthenticationProvider接口

    public abstract class AbstractUserDetailsAuthenticationProvider implements
            AuthenticationProvider, InitializingBean, MessageSourceAware {
        ...
        private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
        private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
        ...
        public Authentication authenticate(Authentication authentication)
                throws AuthenticationException {
            ...
            // 获得提交过来的用户名
            String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
            //根据用户名从缓存中查找 UserDetails
            boolean cacheWasUsed = true;
            UserDetails user = this.userCache.getUserFromCache(username);
            if (user == null) {
                cacheWasUsed = false;
                try {
                    //缓存中没有则通过 retrieveUser(..) 方法查找 (看下面 DaoAuthenticationProvider 的实现)
                    user = retrieveUser(username,
                            (UsernamePasswordAuthenticationToken) authentication);
                }catch
                ...
            }
            try {
                //比对前的检查,例如账户以一些状态信息(是否锁定, 过期...)
                preAuthenticationChecks.check(user);
                //子类实现比对规则 (看下面 DaoAuthenticationProvider 的实现)
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }catch (AuthenticationException exception) {
                if (cacheWasUsed) {
                    // 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);
                    preAuthenticationChecks.check(user);
                    additionalAuthenticationChecks(user,
                            (UsernamePasswordAuthenticationToken) authentication);
                }else {
                    throw exception;
                }
            }
            postAuthenticationChecks.check(user);
            if (!cacheWasUsed) {
                this.userCache.putUserInCache(user);
            }
            Object principalToReturn = user;
            if (forcePrincipalAsString) {
                principalToReturn = user.getUsername();
            }
            //根据最终user的一些信息重新生成具体详细的 Authentication 对象并返回
            return createSuccessAuthentication(principalToReturn, authentication, user);
        }
        //具体生成还是看子类实现
        protected Authentication createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) {
            // Ensure we return the original credentials the user supplied,
            // so subsequent attempts are successful even with encoded passwords.
            // Also ensure we return the original getDetails(), so that future
            // authentication events after cache expiry contain the details
            UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                    principal, authentication.getCredentials(),
                    authoritiesMapper.mapAuthorities(user.getAuthorities()));
            result.setDetails(authentication.getDetails());
            return result;
        }
    }
    

    接下来我们来看下 DaoAuthenticationProvider 里面的三个重要的方法,比对方式、获取需要比对的 UserDetails 对象以及生产最终返回 Authentication 的方法。

    public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
        ...
        //密码比对
        @SuppressWarnings("deprecation")
        protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)
                throws AuthenticationException {
            if (authentication.getCredentials() == null) {
                logger.debug("Authentication failed: no credentials provided");
                throw new BadCredentialsException(messages.getMessage(
     "AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "Bad credentials"));
            }
            String presentedPassword = authentication.getCredentials().toString();
            //通过 PasswordEncoder 进行密码比对, 注: 可自定义
            if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(messages.getMessage(
    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "Bad credentials"));
            }
        }
        //通过 UserDetailsService 获取 UserDetails
        protected final UserDetails retrieveUser(String username,
                UsernamePasswordAuthenticationToken authentication)
                throws AuthenticationException {
            prepareTimingAttackProtection();
            try {
                //通过 UserDetailsService 获取 UserDetails
                UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
                if (loadedUser == null) {
                    throw new InternalAuthenticationServiceException(
                            "UserDetailsService returned null, which is an interface contract violation");
                }
                return loadedUser;
            }
            catch (UsernameNotFoundException ex) {
                mitigateAgainstTimingAttack(authentication);
                throw ex;
            }
            catch (InternalAuthenticationServiceException ex) {
                throw ex;
            }
            catch (Exception ex) {
                throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
            }
        }
        //生成身份认证通过后最终返回的 Authentication, 记录认证的身份信息
        @Override
        protected Authentication createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) {
            boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
            if (upgradeEncoding) {
                String presentedPassword = authentication.getCredentials().toString();
                String newPassword = this.passwordEncoder.encode(presentedPassword);
                user = this.userDetailsPasswordService.updatePassword(user, newPassword);
            }
            return super.createSuccessAuthentication(principal, authentication, user);
        }
    }
    

    相关文章

      网友评论

        本文标题:Spring技术实战-Spring Security原理以及实战

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