• SpringSecurity系列一:08 oauth2认证:ClientCredentialsTokenEndpointFilter 过滤器


    第一步

    Spring Security对于获取TOKEN的请求(/oauth/token),需要认证client_id和client_secret。认证client_id和client_secret可以有2种方式,一种是通过 ClientCredentialsTokenEndpointFilter,另一种是通过BasicAuthenticationFilter。

    ClientCredentialsTokenEndpointFilter 继承自 AbstractAuthenticationProcessingFilter,调用授权接口获取token值的请求(/oauth/token)需要认证client_id和client_secret。该请求会被 AbstractAuthenticationProcessingFilter 过滤器拦截,执行父类的doFilter() 方法:

    public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
        implements ApplicationEventPublisherAware, MessageSourceAware {
    
        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
    
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
    
            // 判断是否需要认证:判断url是否为与配置的获取access token的url进行匹配
            if (!requiresAuthentication(request, response)) {
                chain.doFilter(request, response);
                return;
            }
    
            Authentication authResult;
            try {
                // 调用实现类中的attemptAuthentication方法认证
                authResult = attemptAuthentication(request, response);
                if (authResult == null) {
                    return;
                }
                // session存储
                sessionStrategy.onAuthentication(authResult, request, response);
            }catch (InternalAuthenticationServiceException 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);
        }
    
        protected boolean requiresAuthentication(HttpServletRequest request,
                                                 HttpServletResponse response) {
            return requiresAuthenticationRequestMatcher.matches(request);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    requiresAuthentication(request, response) 最终会调用 ClientCredentialsTokenEndpointFilter 中内部类 ClientCredentialsRequestMatcher#matches 方法:

    @Deprecated
    public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
        // ...
        
        // 内部类
        protected static class ClientCredentialsRequestMatcher implements RequestMatcher {
            
            // /oauth/token
            private String path;
    
            public ClientCredentialsRequestMatcher(String path) {
                this.path = path;
            }
    
            // 判断请求的 url 与获取 access token 的默认 url(/oauth/token)是否一致
            // 判断 client_id 是否为空
            public boolean matches(HttpServletRequest request) {
                // 请你路径uri : /ngsoc/AUTH/oauth/token
                String uri = request.getRequestURI();
                int pathParamIndex = uri.indexOf(59);
                if (pathParamIndex > 0) {
                    uri = uri.substring(0, pathParamIndex);
                }
    			// ngsoc
                String clientId = request.getParameter("client_id");
                if (clientId == null) {
                    return false;
                } else {
                    // 服务路径 request.getContextPath() : /ngosc/AUTH
                    // path : /oauth/token
                    return "".equals(request.getContextPath()) ? uri.endsWith(this.path) : uri.endsWith(request.getContextPath() + this.path);
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    第二步

    当认证请求需要被认证时,authResult = attemptAuthentication(request, response); 就会调用子类ClientCredentialsTokenEndpointFilter#attemptAuthentication 方法,在方法中将参数中的client_id和client_sercet封装成Authentication对象UsernamePasswordAuthenticationToken,然后交给AuthenticationManager的实现类去认证。

    @Deprecated
    public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
        
        private AuthenticationEntryPoint authenticationEntryPoint;
        private boolean allowOnlyPost;
        
        public Authentication attemptAuthentication(HttpServletRequest request, 
                                                    HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
            // 如果不是post请求,抛出异常
            if (this.allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
                throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[]{"POST"});
            } else {
                // 从请求参数client_id中获取clientId 
                String clientId = request.getParameter("client_id"); // ngsoc
                // 从请求参数client_secret中获取clientSecret
                String clientSecret = request.getParameter("client_secret"); // ngsoc
                
                // 从SecurityContextHolder中获取认证成功的认证用户信息,如果不为空,直接返回
                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                if (authentication != null && authentication.isAuthenticated()) {
                    return authentication;
                } else if (clientId == null) {
                    throw new BadCredentialsException("No client credentials presented");
                } else {
                    if (clientSecret == null) {
                        clientSecret = "";
                    }
                    clientId = clientId.trim();
                    // 将clientId和clientSecret封装为UsernamePasswordAuthenticationToken对象
                    UsernamePasswordAuthenticationToken authRequest 
                        = new UsernamePasswordAuthenticationToken(clientId, clientSecret);
                    // 将UsernamePasswordAuthenticationToken交给AuthenticationManager的子类认证
                    return this.getAuthenticationManager().authenticate(authRequest);
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    第三步

    AuthenticationManager接口源码:

    public interface AuthenticationManager {
       // 身份认证
       // 请求数据:待认证的 Authentication
       // 响应数据:认证成功的 Authentication
       Authentication authenticate(Authentication authentication) throws AuthenticationException;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    AuthenticationManager接口的默认实现类是ProviderManager,因此this.getAuthenticationManager().authenticate(authRequest) 最终会调用ProviderManager#authenticate方法完成认证。

    public class ProviderManager implements AuthenticationManager, MessageSourceAware,
    InitializingBean {
    
        // AuthenticationProvider列表
        private List<AuthenticationProvider> providers = Collections.emptyList();
        // 父类的 AuthenticationManager
        private AuthenticationManager parent;
    
        public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
            Class<? extends Authentication> toTest = authentication.getClass();
            Authentication result = null;
            Authentication parentResult = null;
            // 遍历AuthenticationProvider列表
            for (AuthenticationProvider provider : getProviders()) {
                // 判断当前遍历 AuthenticationProvider 是否支持Authentication对象的认证
                if (!provider.supports(toTest)) {
                    continue;
                }
    
                try {
                    // 如果支持,将使用该AuthenticationProvider完成认证
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        copyDetails(authentication, result);
                        break;
                    }
                }
    			// ....
            }
    
            if (result == null && parent != null) {
                // Allow the parent to try.
                try {
                    result = parentResult = parent.authenticate(authentication);
                } catch (ProviderNotFoundException e) {
                }catch (AuthenticationException e) {
                    lastException = parentException = e;
                }
            }
    		// ...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    说明:

    在 Spring Seourity 中,允许系统同时⽀持多种不同的认证⽅式,例如同时⽀持⽤户名/密码认证、 ReremberMe 认证、⼿机号码动态认证等,⽽不同的认证⽅式对应了不同的 AuthenticationProvider,所以⼀个完整的认证流程可能由多个AuthenticationProvider 来提供。

    多个AuthenticationProvider将组成⼀个列表,这个列表将由ProviderManager 代理。换句话说,在ProviderManager 中存在⼀个AuthenticationProvider列表,在ProviderManager中遍历列表中的每⼀个AuthenticationProvider去执⾏身份认证,最终得到认证结果。

    ProviderManager 本身也可以再配置⼀个 AuthenticationManager 作为parent,这样当ProviderManager 认证失败之后,就可以进⼊到 parent 中再次进⾏认证。理论上来说, ProviderManager 的 parent 可以是任意类型的AuthenticationManager,但是通常都是由ProviderManager 来扮演 parent 的⻆⾊,也就是 ProviderManager 是ProviderManager 的 parent。

    默认情况下,ProviderManager的AuthenticationProvider列表中包含两个实现类:AnoymousAuthenticationProvider 和DaoAuthenticationProvider。

    for循环内第一次得到AnoymousAuthenticationProvider,执行AnonymousAuthenticationProvider#supports方法判断该类是否支持UsernamePasswordAuthenticationToken类型的认证,结果不支持,代码如下:

    public class AnonymousAuthenticationProvider implements AuthenticationProvider,
          MessageSourceAware {
    
       public boolean supports(Class<?> authentication) {
          return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    for循环内第二次得到DaoAuthenticationProvider,该类继承自AbstractUserDetailsAuthenticationProvider类,会调用AbstractUserDetailsAuthenticationProvider#supports方法判断该类是否支持UsernamePasswordAuthenticationToken类型的认证,结果支持。

    public abstract class AbstractUserDetailsAuthenticationProvider implements
        AuthenticationProvider, InitializingBean, MessageSourceAware {
        
        public boolean supports(Class<?> authentication) {
            return (UsernamePasswordAuthenticationToken.class
                    .isAssignableFrom(authentication));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    因此 result = provider.authenticate(authentication) 最终会调用AbstractUserDetailsAuthenticationProvider#authenticate方法对UsernamePasswordAuthenticationToken对象完成认证,在该方法中根据clientId获取数据源中存储的用户user,然后判断user是否禁用、过期、锁定、密码是否一致等,若都满足条件则验证通过。

    public abstract class AbstractUserDetailsAuthenticationProvider implements
        AuthenticationProvider, InitializingBean, MessageSourceAware {
    
        public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
            // Determine username
            String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();
    
            boolean cacheWasUsed = true;
            // 从缓存中根据clientId获取UserDetails对象
            UserDetails user = this.userCache.getUserFromCache(username);
    
            // 如果缓存中获取不到
            if (user == null) {
                cacheWasUsed = false;
    
                try {
                    // 从数据源中获取
                    user = retrieveUser(username,
                                        (UsernamePasswordAuthenticationToken) authentication);
                }catch (UsernameNotFoundException notFound) {
                    // 如果根据clientId从数据源中获取UserDetails用户详情,如果为空,认证失败抛出异常
                    logger.debug("User '" + username + "' not found");
                    if (hideUserNotFoundExceptions) {
                        throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                    }
                    else {
                        throw notFound;
                    }
                }
            }
            try {
                // 检查账户是否锁定、启用、过期等
                pr eAuthenticationChecks.check(user);
                //检查凭据[密码]是否非空、以及存储密码与输入密码是否一致
                additionalAuthenticationChecks(user,
                   (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (AuthenticationException exception) {
                if (cacheWasUsed) {
                    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();
            }
    
            // 创建Authentication[身份认证信息]
            return createSuccessAuthentication(principalToReturn, authentication, user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    到这儿认证流程就结束了,但是我们可以继续往下看一下,底层如何根据username获取客户端用户信息的。

    第四步

    retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); 获取用户信息会调用DaoAuthenticationProvider#retrieveUser方法,该方法中,会调用UserDetailsService接口实现类的loadUserByUsername方法根据clientId获取客户端详情信息,代码如下:

    public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
        
        protected final UserDetails retrieveUser(String username,
     	UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
            prepareTimingAttackProtection();
            try {
                // 调用UserDetailsService接口实现类的loadUserByUsername方法根据username获取用户信息
                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);
            }
        }
        
    	protected UserDetailsService getUserDetailsService() {
    		return userDetailsService;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    public interface UserDetailsService {
       UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
    }
    
    • 1
    • 2
    • 3
    第五步

    UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); 会调用ClientDetailsUserDetailsService#loadUserByUsername方法,获取客户端详情信息:

    @Deprecated
    public class ClientDetailsUserDetailsService implements UserDetailsService {
        private final ClientDetailsService clientDetailsService;
        private String emptyPassword = "";
    
        public ClientDetailsUserDetailsService(ClientDetailsService clientDetailsService) {
            this.clientDetailsService = clientDetailsService;
        }
    
        public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
            this.emptyPassword = passwordEncoder.encode("");
        }
    
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            ClientDetails clientDetails;
            try {
                // 虎获取客户端详情
                clientDetails = this.clientDetailsService.loadClientByClientId(username);
            } catch (NoSuchClientException var4) {
                throw new UsernameNotFoundException(var4.getMessage(), var4);
            }
    
            String clientSecret = clientDetails.getClientSecret();
            if (clientSecret == null || clientSecret.trim().length() == 0) {
                clientSecret = this.emptyPassword;
            }
    		// 将客户端信息 ClientDetails 封装成UserDetails并返回
            return new User(username, clientSecret, clientDetails.getAuthorities());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    第六步

    在该方法中会调用ClientDetailsService接口实现类BaseClientDetails#loadClientByClientId方法获取ClientDetails信息:

    @Deprecated
    public interface ClientDetailsService {
        ClientDetails loadClientByClientId(String var1) throws ClientRegistrationException;
    }
    
    • 1
    • 2
    • 3
    • 4
    public interface ClientDetails extends Serializable {
    
    	String getClientId();//客户端id
    	
    	Set<String> getResourceIds();//此客户端可以访问的资源。如果为空,则调用者可以忽略
    	
    	boolean isSecretRequired();//验证此客户端是否需要secret
    	
    	String getClientSecret();//获取客户端的secret
    	
    	boolean isScoped();//此客户端是否仅限于特定范围
    	
    	Set<String> getScope();//此客户端的范围。如果客户端未确定作用域,则为空
    	
    	Set<String> getAuthorizedGrantTypes();//此客户端被授权的授权类型
    	
    	Set<String> getRegisteredRedirectUri();//此客户端的预定义重定向redirect_url
    	
    	Collection<GrantedAuthority> getAuthorities();//权限集合
    	
    	Integer getAccessTokenValiditySeconds();//访问令牌有效期
    	
    	Integer getRefreshTokenValiditySeconds();//刷新令牌有效期
    
    	boolean isAutoApprove(String scope);//测试客户端是否需要特定范围的用户批准
        
    	Map<String, Object> getAdditionalInformation();//额外的信息
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    BaseClientDetails [
        clientId=ngsoc, 
        clientSecret={bcrypt}$2a$10$Dcn01QtYjhoXeeX0LPsn/.DBiiosgsFFKHVC1/tQiWk5dht1TgtKy, 
        scope=[], 
        resourceIds=[], 
        authorizedGrantTypes=[password, refresh_token, client_credentials], 
        registeredRedirectUris=[], 
        authorities=[], 
        accessTokenValiditySeconds=43200, 
        refreshTokenValiditySeconds=86400, 
        additionalInformation={}
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  • 相关阅读:
    源代码防泄密是什么?
    使用spring-boot-starter-jdbc访问MySQL大部分程序员都不一定会
    数据库小白入门最简易版
    国密复习
    Axure RP 9 for mac 高保真原型图 - 案例18 【导航栏-展开、收起】导入元件库
    设了止损就万事大吉了?白银漫谈(一)
    程序员级别分析,看看你是哪个级别
    [LeetCode周赛复盘] 第 311 场周赛20220918
    React笔记(九)RTK
    【从零开始学微服务】04.微服务架构的特点
  • 原文地址:https://blog.csdn.net/qq_42764468/article/details/126851639