SpringSocial 源码分析

SpringSocial 自动配置部分源码分析 本篇主要任务是从源码角度分析 SpringSecurity、SpringSocial 的自动配置。在使用 SpringSocial 的时候,我们经常需要配置 ServiceProvider、ConnectionFactory 等,那么在内部 SpringSocial 是怎么聚合这些配置,另外 SpringSocial 是如何与 SpringSecurity 进行整合的。只有知道了整个过程我们才能更... 阅读详情

鸽了两个月没写博客,快变鸽王了,( ´◔ ‸◔`)。今天终于把 SpringSocial 源码分析篇完结了,整理了一下,先发上来分享给大家。后面要加油了,(ง •̀_•́)ง。废话不多说,下面进入正文环节。

从上一章我们已经可以知道 SpringSocial 的整体结构与使用流程。这一章我们按使用流程走一遍源码,顺便验证一下整体结构。

认证流程

还是以上面这张流程图为主,我们先从 SocialAuthenticationFilter 开始,跟着流程把大体源码读一遍。

首先阅读源码很好用的一个方式就是跑代码来 Debug,因为自己一行一行的看的话,比较枯燥,而且也会漏掉一些东西。这里我们使用上一章的代码来 Debug 。从上图或者官网我们都可以知道入口在 SocialAuthenticationFilter 文件中。

下面开始我们漫长的代码分析,建议大家看的时候,打开自己的 IDE 跟着看,不然很容易晕车的。

第一步,我们从 SocialAuthenticationFilter 说起。先看下 SocialAuthenticationFilter 的继承结构 :

SpringSocialSource1-1

我们可以发现,SocialAuthenticationFilter 的父类是 AbstractAuthenticationProcessingFilter,在 SpringSecurity 体系中可以通过自定义 SpringSocialConfigurer 将 AbstractAuthenticationProcessingFilter 加入到过滤器链中,参与用户的认证流程。

对于 Filter 我们关注他的 doFilter 方法绝对没有问题。

// AbstractAuthenticationProcessingFilter.java

// 193 行
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 {
      // 1. 调用 attemptAuthentication
      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);
}

// 282 行
// 2. attemptAuthentication 是一个抽象方法,具体的实现交给子类
public abstract Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException, IOException,
ServletException;

标记 1 : 调用了 attemptAuthentication 方法。

标记 2 : 按标记 1 找到,我们可以发现该方法是一个抽象方法,具体的实现应该在子类中。在我们这里,具体的实现应该在 SocialAuthenticationFilter 中。


// SocialAuthenticationFilter.java

// 160 行
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
   if (detectRejection(request)) {
      if (logger.isDebugEnabled()) {
         logger.debug("A rejection was detected. Failing authentication.");
      }
      throw new SocialAuthenticationException("Authentication failed because user rejected authorization.");
   }
   
   Authentication auth = null;
   // 1. 获取系统中所有的 providerId (第三方系统在本系统中的唯一标识符)
   Set<String> authProviders = authServiceLocator.registeredAuthenticationProviderIds();
   // 2. 根据请求找到匹配当前环境的 providerId
   String authProviderId = getRequestedProviderId(request);
   // authProviders 不为空,authProviderId 不为空且 authProviders 包含 authProviderId
   // 变相校验 authProviderId 是一个合法的 providerId
   if (!authProviders.isEmpty() && authProviderId != null && authProviders.contains(authProviderId)) {
      // 3. 按 providerId 获取到对应的 SocialAuthenticationService
      SocialAuthenticationService<?> authService = authServiceLocator.getAuthenticationService(authProviderId);
      // 4. 调用 attemptAuthService 方法,获取 Authentication,这里注意一下 Authentication 接口
      // 是后面 SocialAuthenticationToken 的父接口
      auth = attemptAuthService(authService, request, response);
      if (auth == null) {
         throw new AuthenticationServiceException("authentication failed");
      }
   }
   return auth;
}

// 263 行
private Authentication attemptAuthService(final SocialAuthenticationService<?> authService, final HttpServletRequest request, HttpServletResponse response) 
			throws SocialAuthenticationRedirectException, AuthenticationException {
		// 5. 使用 AuthService 获取 SocialAuthenticationToken
		final SocialAuthenticationToken token = authService.getAuthToken(request, response);
		if (token == null) return null;
		
		Assert.notNull(token.getConnection());
		
		Authentication auth = getAuthentication();
		if (auth == null || !auth.isAuthenticated()) {
            // 6. 当前用户状态还没有认证过,使用 token 去认证,走 SpringSecurity 认证流程
			return doAuthentication(authService, request, token);
		} else {
            // 7. 已认证的话,校验当前用户是否创建过 Connection,没有则创建,创建完毕跳转到指定页面
			addConnection(authService, request, token, auth);
			return null;
		}		
	}	
标记 1 : 获取系统中所有的 providerId (每一个 providerId 对应一个第三方服务商)。

实际上调用的 SocialAuthenticationServiceRegistry 的 registeredAuthenticationProviderIds 方法

// SocialAuthenticationServiceRegistry.java

// 27 行
private Map<String, SocialAuthenticationService<?>> authenticationServices = new HashMap<String, SocialAuthenticationService<?>>();

// 41 行
/**
* Add a {@link SocialAuthenticationService} to this registry.
* @param authenticationService a SocialAuthenticationService to register
*/
public void addAuthenticationService(SocialAuthenticationService<?> authenticationService) {
		addConnectionFactory(authenticationService.getConnectionFactory());
		authenticationServices
    .put(authenticationService.getConnectionFactory().getProviderId(), 		
     	authenticationService);
	}

// 58 行
public Set<String> registeredAuthenticationProviderIds() {
   return authenticationServices.keySet();
}

authenticationServices 是一个 Map<String, SocialAuthenticationService<?>>,key 为 providerId,value 为该 providerId 在系统中对应的 SocialAuthenticationService。

我们跟踪一下其什么时候注入 key、value,我们发现其 addAuthenticationService 方法调用如下:

// SecurityEnabledConnectionFactoryConfigurer.java

// 40 行
public void addConnectionFactory(ConnectionFactory<?> connectionFactory) {
 // 这里调用 register 的 addAuthenticationService 方法
 registry.addAuthenticationService(wrapAsSocialAuthenticationService(connectionFactory);
}

// 49 行
private <A> SocialAuthenticationService<A> wrapAsSocialAuthenticationService(ConnectionFactory<A> cf) {
    if (cf instanceof OAuth1ConnectionFactory) {
        // 使用 OAuth1 协议时
        return new OAuth1AuthenticationService<A>((OAuth1ConnectionFactory<A>) cf);
    } else if (cf instanceof OAuth2ConnectionFactory) {
        // 使用 OAuth2 协议时,使用当前 ConnectionFactory 创建一个对应的 AuthenticationService
        // 注意这里可以通过 ConnectionFactoy 设置 service 的 scope
        final OAuth2AuthenticationService<A> authService = new OAuth2AuthenticationService<A>((OAuth2ConnectionFactory<A>) cf);
        authService.setDefaultScope(((OAuth2ConnectionFactory<A>) cf).getScope());
        return authService;
    }
    throw new IllegalArgumentException("The connection factory must be one of OAuth1ConnectionFactory or OAuth2ConnectionFactory");
	}

SecurityEnabledConnectionFactoryConfigurer 是 ConnectionFactoryConfigurer 的实现类,继续跟一下,看哪里调用的 addConnectionFactory 方法。这个时候我们发现通过 IDEA 的 Find Usages 功能找不到该方法被引用的地方。线索似乎在这里断掉了,这个时候不要急,我们尝试找一下 SecurityEnabledConnectionFactoryConfigurer 的 Find Usages,我们可以发现在 SocialConfiguration 的 connectionFactoryLocator 方法中使用了该类,把该类的一个实例化对象作为参数传入遍历调用的 SocialConfigurer 的 addConnectionFactories 方法中。

这里重点说明一下 SocialConfiguration 这个类。在这个类中,通过 IOC 注入收集了系统中所有 SocialConfigurer 的实现。并在 ConnectionFactoryLocator 构造时。遍历 SocialConfigurer 调用 addConnectionFactories 方法。

这里大概明白了 SpringSocial 的套路,用户自定义 SocialConfigurer,SocialConfiguration 的实现,来自定义设置(自定义的 ConnectionFactory、UserConnectionRepositoty 自定义设置等),这里进行全局收集,调用用户的实现,使其生效。配置主要围绕 ConnectionFactory、UserIdSource、UserConnectionRepository 三个方面,这和我们之前文章的分析不谋而合,整个框架还是围绕的 Connection 的产生与存储。

// SocialConfiguration.java

// 39 行
private List<SocialConfigurer> socialConfigurers;

// 46 行
// 使用 SpringIOC 将系统中所有的 SocialConfigurer 实现类注入进来
@Autowired
public void setSocialConfigurers(List<SocialConfigurer> socialConfigurers) {
    Assert.notNull(socialConfigurers, "At least one configuration class must implement SocialConfigurer (or subclass SocialConfigurerAdapter)");
    Assert.notEmpty(socialConfigurers, "At least one configuration class must implement SocialConfigurer (or subclass SocialConfigurerAdapter)");
    this.socialConfigurers = socialConfigurers;
}

// 54 行
// 如果开启了 securityEnabled 就是用 SecurityEnabledConnectionFactoryConfigurer,否则就是用
// DefaultConnectionFactoryConfigurer
@Bean
public ConnectionFactoryLocator connectionFactoryLocator() {
    if (securityEnabled) {
        SecurityEnabledConnectionFactoryConfigurer cfConfig = new SecurityEnabledConnectionFactoryConfigurer();
        for (SocialConfigurer socialConfigurer : socialConfigurers) {
            // 遍历注入的 SocialConfigurer 实现,调用其 addConnectionFactories 方法
            // 本质上是遍历用户实现的 SocialConfigurer 的实现类,使用户的配置生效 
            socialConfigurer.addConnectionFactories(cfConfig, environment);
        }
        return cfConfig.getConnectionFactoryLocator();
    } else {
        DefaultConnectionFactoryConfigurer cfConfig = new DefaultConnectionFactoryConfigurer();
        for (SocialConfigurer socialConfigurer : socialConfigurers) {
            socialConfigurer.addConnectionFactories(cfConfig, environment);
        }
        return cfConfig.getConnectionFactoryLocator();
    }
}

我们跟着看一下 SocialConfigurer 接口。

其实我们前面章节实践的时候,就会自定义它的子类 SocialConfigurerAdapter 与 SocialAutoConfigurerAdapter 的实现类,并分别实现 getUsersConnectionRepository 与 createConnectionFactory 方法来自定义 UserConnectionRepository 与 ConnectionFactory。

// SocialConfigurer.java

public interface SocialConfigurer {

   /**
    * Callback method to allow configuration of {@link ConnectionFactory}s.
    * @param connectionFactoryConfigurer A configurer for adding {@link ConnectionFactory} instances.
    * @param environment The Spring environment, useful for fetching application credentials needed to create a {@link ConnectionFactory} instance.
    */
   void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment);
}

具体看一下 SocialConfigurer 的实现类,看看在 addConnectionFactories 中到底做了些什么。

SpringSocialSource1-2

SocialConfigurer 的非内部类实现只有两个,SocialConfigurerAdapter 是抽象类,addConnectionFactories 正真的实现逻辑在 SocialAutoConfigurerAdapter 类中

// SocialAutoConfigurerAdapter .java

public abstract class SocialAutoConfigurerAdapter extends SocialConfigurerAdapter {
    public SocialAutoConfigurerAdapter() {
    }

    // 向 ConnectionFactoryConfigurer 中添加 ConnectionFactory
    public void addConnectionFactories(ConnectionFactoryConfigurer configurer, Environment environment) {
        // 这里调用 createConnectionFactory 是一个抽象方法
        configurer.addConnectionFactory(this.createConnectionFactory());
    }
	// 交给子类实现
    protected abstract ConnectionFactory<?> createConnectionFactory();
}

可以看到最终是在 SocialAutoConfigurerAdapter 的 addConnectionFactories 方法中添加 ConnectionFactory,我们可以自定义 SocialAutoConfigurerAdapter 的实现类,实现 createConnectionFactory 方法,将我们自定义的 ConnectionFactory 注册到 ConnectionFactoryConfigurer 中。

// SocialConfigurerAdapter.java

public abstract class SocialConfigurerAdapter implements SocialConfigurer {

   /**
    * Default implementation of {@link #addConnectionFactories(ConnectionFactoryConfigurer, Environment)}.
    * Implemented as a no-op, adding no connection factories.
    */
   public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
   }
   
   /**
    * Default implementation of {@link #getUserIdSource()}.
    * Returns null, indicating that this configuration class doesn't provide a UserIdSource (another configuration class must provide one, however).
    * @return null
    */
   public UserIdSource getUserIdSource() {
      return null;
   }
   
   /**
    * Default implementation of {@link #getUsersConnectionRepository(ConnectionFactoryLocator)} that creates an in-memory repository.
    */
   public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
      return new InMemoryUsersConnectionRepository(connectionFactoryLocator);
   }

}

我们重写 getUsersConnectionRepository 方法,可以自定 UsersConnectionRepository 的配置,默认使用的是基于内存的存储方式。

OK,到了这里饶了半天,我们终于把 标记 1 的逻辑跟踪完了,是不是有点晕车啊。其实前面这些分析可以终结为以下几点:

  • 获取系统中配置的所有 providerId
  • 是怎么通过 providerId 获取到 SocialAuthenticationService 的,他们的对应关系是什么时候初始化的。
    • 实际存储关系是一个 HashMap,以 key - value 形式进行存储的
    • 初始化时机是在 addConnectionFactory 时,通过 ConnectionFactory 构造 SocialAuthenticationService,然后 put 进 HashMap
  • 通过上面衍生出 ConnectionFactory 的初始化调用,最后一步步跟踪,发现是在 SocialConfiguration 中收集全局用户自定义的 SocialConfigurer 的实现,遍历调用其 addConnectionFactories 方法进行添加的。

下面收回我们的思绪,继续回到我们的 SocialAuthenticationFilter 中,简单看一下如何从请求中获取当前 providerId 的。

标记 2 : 从请求中获取 providerId

private String getRequestedProviderId(HttpServletRequest request) {
   String uri = request.getRequestURI();
   int pathParamIndex = uri.indexOf(';');

   if (pathParamIndex > 0) {
      // 去掉 ; 以及它后面的内容
      // strip everything after the first semi-colon
      uri = uri.substring(0, pathParamIndex);
   }

   // uri must start with context path
   // 去掉 contextPath (服务路径)
   uri = uri.substring(request.getContextPath().length());

   // remaining uri must start with filterProcessesUrl
   if (!uri.startsWith(filterProcessesUrl)) {
      return null;
   }

   // 去掉 filterProcessUrl,SocialAuthentication 的拦截 Url,默认是 /auth,可以自定义
   uri = uri.substring(filterProcessesUrl.length());

   // expect /filterprocessesurl/provider, not /filterprocessesurlproviderr
   // 最后去掉一个 /,获取到 providerId,所以这里可以得出默认路径应该是
   // /contextPath/filterprocessurl/providerId
   if (uri.startsWith("/")) {
      return uri.substring(1);
   } else {
      return null;
   }
}

标记 3 : 如果从请求中获取的 providerId 在系统中有找到,获取该 providerId 对应的 SocialAuthenticationService

标记 3 实际上分为两步:

  • 第一步看请求中的 providerId 在全局的 providerIds 中能否找到,相当于校验 providerId 是否合法。
  • 第二步通过 providerId 获取对应的 SocialAuthenticationService,这一步的具体逻辑我们在标记 1 中已经具体跟踪过了。

标记 4 : 调用 attemptAuthService 方法,尝试获取 Authentication

标记 4 主要是一个跳转,核心实现逻辑在标记 5 中。

标记 5 : 调用 SocialAuthenticationService 的 getAuthToken 方法获取 SocialAuthenticationToken

// OAuth2AuthenticationService.java

public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
   // 从请求中尝试获取授权码
   String code = request.getParameter("code");
   if (!StringUtils.hasText(code)) {
      // 请求中如果没有授权码,表示不是回调回来的,引导用户跳转到 Service Provider 对应的授权页面
      OAuth2Parameters params =  new OAuth2Parameters();
      // 设置回调地址,buildReturnToUrl 主要是对根据配置对当前请求地址进行参数拼接,默认不配置的话
      // 当前请求地址就是回调地址
      params.setRedirectUri(buildReturnToUrl(request));
      // 设置 scope
      setScope(request, params);
      // 设置 state
      params.add("state", generateState(connectionFactory, request));
      addCustomParameters(params);
      // 通过抛出异常的方式进行跳转, 在 AbstractAuthenticationProcessingFilter 的 doFilter 
      // 中捕获异常,进一步调用对应的 AuthenticationFailureHandler 的 onAuthenticationFailure
      // 方法,在该方法中判断,如果是 SocialAuthenticationRedirectException,则引导用户进行跳转,
      // 否则,调用默认委派类( SimpleUrlAuthenticationFailureHandler )
      // 的 onAuthenticationFailure 方法进行处理
      // buildAuthenticateUrl 方法会拼接 client_id 与 response_type 参数,如果使用的第三方还需要
      // 其他参数,可以重写 OAuthOperation 的 buildAuthenticateUrl 方法配合 properties 设置
      throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
   } else if (StringUtils.hasText(code)) {
      try {
         // 如果从请求中获取到了授权码,表示该请求是第三方服务回调回来的
         // 设置回调地址,buildReturnToUrl 主要是对根据配置对当前请求地址进行参数拼接,默认不配置的话
         // 当前请求地址就是回调地址
         String returnToUrl = buildReturnToUrl(request);
         // 调用 ConnectionFactory 对应的 ServiceProvider 的 OAuthOperations 进行
         // AccessToken 获取,并将其封装成 AccessGrant,exchangeForAccess 详讲....
         AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
         // TODO avoid API call if possible (auth using token would be fine)
         // 使用封装的 AccessGrant 创建 Connection,createConnection 详讲....
         Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
         // 使用 Connection 构建 AuthenticationToken 返回回去,继续 SpringSecurity 的认证流程
         return new SocialAuthenticationToken(connection, null);
      } catch (RestClientException e) {
         logger.debug("failed to exchange for access", e);
         return null;
      }
   } else {
      return null;
   }
}

标记 5 中主要是 getAuthToken 方法的实现逻辑,其中有两种情况:

  • 请求中存在授权码: 使用获取到的授权码,通过当前 OAuth2AuthenticationService 对应的 ConnectionFactory 请求获取当前用户在第三方服务商上的 Token,然后将这个 Token 最终封装为 Connection,最后将 Connection 封装成 SocialAuthenticationToken ( SpringSecurity 认证框架中的 Token )。
  • 请求中没有授权码: 根据当前 OAuth2AuthenticationService 找到对应第三方服务商的配置,引导用户跳转到其授权页面 (比如 QQ 登录页面)。

标记 6 : 当前用户状态还没有认证过,使用 token 去认证,走 SpringSecurity 认证流程

// SocialAuthenticationFilter.java

// 已经获取到了用户的 AccessToken,并将其封装成了 SocialAuthenticationToken,进行
// SpringSecurity 的认证流程
private Authentication doAuthentication(SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token) {
   try {
      // 判断当前的 SocialAuthenticationService 的模式是否支持认证
      if (!authService.getConnectionCardinality().isAuthenticatePossible()) return null;
      token.setDetails(authenticationDetailsSource.buildDetails(request));
      // SpringSecurity 的标准流程, 调用 AuthenticationManager 的 authenticate 方法进行认证,
      // 实质是调用 ProviderManager 的 authenticate 方法,遍历所有 AuthenticationProvider, 
      // 找到匹配的 SocialAuthenticationProvide,并调用其 authenticate 方法     
      Authentication success = getAuthenticationManager().authenticate(token);
      Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), "unexpected principle type");
      // 如果设置了 updateConnections,每次第三方认证后获取 Connection 后, 
      // 都对 Connection 进行更新,Connection 包含 
      // Connectionkey(providerId、providerUserId)、DisplayName、ProfileUrl、ImageUrl
      updateConnections(authService, token, success);          
      return success;
   } catch (BadCredentialsException e) {
      // 当前登录的第三方用户,还没有绑定本系统账号
      // connection unknown, register new user?
      if (signupUrl != null) {
         //  如果有配置注册引导页面,存储 ConnectionData 到 session 中,引导用户跳转到注册页面
         // store ConnectionData in session and redirect to register page
         sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
         // 专门用来跳转到的异常,上面未携带授权码时,也是抛出该类型异常
         throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
      }
      throw e;
   }
}

在标记 5 中我们获取到了 SpringSecurity 框架下的 Token,这时候我们会去根据该 Token 判断当前用户的认证状态。如果当前用户还没有认证过,我们会走标记 6 的逻辑。标记 6 的逻辑实质上走的就是基础的 SpringSecurity 认证逻辑了。

这里有几点要注意:

  • 认证完了之后会调用 updateConnection 对 Repository 中的 Connection 进行更新
  • 如果在 Repository 里面没有找到与第三方服务商的用户 ( 比如 QQ 用户 ) 对应的本系统用户且系统没有设置后端静默注册,这里会根据是否配置默认注册页面来做对应处理
    • 配置了注册页面,会通过特殊异常方式 ( 外面异常捕捉会对该类型异常做特殊处理 ),引导用户跳转到注册页面。
    • 未配置注册页面,会直接抛出异常,导致认证失败。

PS: 前面虽然跳转到第三方授权页面 ( 比如 QQ 登录页面进行了认证 ),但是在我们系统中,当前用户还没有认证

当前的 SocialAuthenticationService 的模式是否支持认证由下面枚举状态决定

// ConnectionCardinality (SocialAuthenticationService 状态集合)

public enum ConnectionCardinality {
   /**
    * only one connected providerUserId per userId and vice versa
    * 系统中一个 userId 对应一个第三方服务商的 providerUserId,反之亦然
    */
   ONE_TO_ONE(false, false),

   /**
    * many connected providerUserIds per userId, but only one userId per providerUserId
    * 系统中一个 userId 对应多个 providerUserId,但是一个 providerUserId 对应一个 userId
    */
   ONE_TO_MANY(false, true),

   /**
    * only one providerUserId per userId, but many userIds per providerUserId. 
    * Authentication of users not possible
    * 系统中一个 userId 对应一个 providerUserId,但是一个 providerUserId 对应多个 userId
    * 这种情况不能用来做用户认证
    */
   MANY_TO_ONE(true, false),

   /**
    * no restrictions. Authentication of users not possible
    * 系统中一个 userId 对应多个 providerUserId,一个 providerUserId 对应多个 userId (没有限制)
    * 这种情况不能用来做用户认证
    */
   MANY_TO_MANY(true, true);
   
   private final boolean multiUserId;
   private final boolean multiProviderUserId;

   private ConnectionCardinality(boolean multiUserId, boolean multiProviderUserId) {
      this.multiUserId = multiUserId;
      this.multiProviderUserId = multiProviderUserId;
   }

   /**
    * allow many userIds per providerUserId. If true, authentication is not possible
    * @return true if multiple local users are allowed per provider user ID
    */
   public boolean isMultiUserId() {
      return multiUserId;
   }

   /**
    * allow many providerUserIds per userId
    * @return true if users are allowed multiple connections to a provider
    */
   public boolean isMultiProviderUserId() {
      return multiProviderUserId;
   }

   public boolean isAuthenticatePossible() {
      return !isMultiUserId();
   }
}

标记 7 : 已认证的话,校验当前用户是否创建过 Connection,没有则创建,创建完毕跳转到指定页面

private void addConnection(final SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token, Authentication auth) {
   // already authenticated - add connection instead
   String userId = userIdSource.getUserId();
   Object principal = token.getPrincipal();
   if (userId == null || !(principal instanceof ConnectionData)) return;
   
   Connection<?> connection = addConnection(authService, userId, (ConnectionData) principal);
   if(connection != null) {
      String redirectUrl = authService.getConnectionAddedRedirectUrl(request, connection);
      if (redirectUrl == null) {
         // use default instead
         redirectUrl = connectionAddedRedirectUrl;
      }
      throw new SocialAuthenticationRedirectException(redirectUrl);
   }
}

到了这里,上面标记的几个点基本上都看完了,整个流程也差不多了。下面会根据上面的一些情况对 SpringSocial 的一些细节和 SpringSecurity 的后续认证流程进行一些补充说明。

标记 6 后续的 SpringSecurity 认证流分析

到了标记 6 所在步骤, SpringSecurity 的认证流程基本上如下:

  1. 调用 AuthenticationManager 的 authenticate 方法,将之前获取的 token 做为参数传递进去

  2. 上面的 authenticate 方法实质上会调用 ProviderManager ( AuthenticationManager 的实现类 ) 的 authenticate 方法。ProviderManager 顾名思义,它拥有系统中所有的 AuthenticationProvider,在 authenticate 方法中会对这些 provider 进行遍历,根据传递进来的 token 参数找到匹配的 provider 方法,并调用其 authenticate 方法。

    // ProviderManager.java

    public Authentication authenticate(Authentication authentication)
          throws AuthenticationException {
       Class<? extends Authentication> toTest = authentication.getClass();
       AuthenticationException lastException = null;
       Authentication result = null;
       boolean debug = logger.isDebugEnabled();
    
       // 遍历 provider
       for (AuthenticationProvider provider : getProviders()) {
          // 按传递进来的 token 进行过滤,找到匹配的
          if (!provider.supports(toTest)) {
             continue;
          }
    
          if (debug) {
             logger.debug("Authentication attempt using "
                   + provider.getClass().getName());
          }
    
          try {
             // 核心代码,调用匹配的 provider 的认证方法
             result = provider.authenticate(authentication);
    
             if (result != null) {
                copyDetails(authentication, result);
                break;
             }
          }
          catch (AccountStatusException e) {
             prepareException(e, authentication);
             // SEC-546: Avoid polling additional providers if auth failure is due to
             // invalid account status
             throw e;
          }
          catch (InternalAuthenticationServiceException e) {
             prepareException(e, authentication);
             throw e;
          }
          catch (AuthenticationException e) {
             lastException = e;
          }
       }
       ......
    }
    
  3. 在我们当前流程中,匹配的 provider 是 SocialAuthenticationProvider,我们来跟踪一下它 authenticate 方法的逻辑。

    // SocialAuthenticationProvider.java

    // 57 行
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
       Assert.isInstanceOf(SocialAuthenticationToken.class, authentication, "unsupported authentication type");
       Assert.isTrue(!authentication.isAuthenticated(), "already authenticated");
       SocialAuthenticationToken authToken = (SocialAuthenticationToken) authentication;
       String providerId = authToken.getProviderId();
       // 1. 从封装的 SocialAuthenticationToken 中获取 Connection
       Connection<?> connection = authToken.getConnection();
       // 2. 使用 Connection 从持久化(数据库)中获取对应的 UserId
       String userId = toUserId(connection);
       if (userId == null) {
          // 2.1 如果没有找到对应的UserId,抛出异常,在上面捕获后,如果配置了默认的注册页面,会引导用户
          // 跳转到注册页面进行注册,如果没有配置,则直接抛出异常
          throw new BadCredentialsException("Unknown access token");
       }
       // 3. 使用 userId 继续走 SpringSecurity 认证流程
       UserDetails userDetails = userDetailsService.loadUserByUserId(userId);
       if (userDetails == null) {
          throw new UsernameNotFoundException("Unknown connected account id");
       }
      
        // 4. 将查询出的用户信息与之前的 Connection 重新封装一个 SocialAuthenticationToken 返回
       return new SocialAuthenticationToken(connection, userDetails, authToken.getProviderAccountData(), getAuthorities(providerId, userDetails));
    }
    
    // 77 行,从数据中查找 Connection 对应的 userId
    protected String toUserId(Connection<?> connection) {
    		List<String> userIds = usersConnectionRepository.findUserIdsWithConnection(connection);
    		// only if a single userId is connected to this providerUserId
    		return (userIds.size() == 1) ? userIds.iterator().next() : null;
    	}
    

    上面的操作,总结起来就是如下几步:

    • 从 Token 中获取之前封装进去的 Connection。
    • 根据 Connection 从数据库中查询对应的 UserId (UserId 代表的是用户在本系统中的唯一标识,不一定是 Id,只要是唯一的就可以)。
    • 使用 UserId 继续走 SpringSecurity 的认证流程,会使用该 UserId 去数据库中查询用户信息,封装成 UserDetail 返回。

    我们这里着重跟踪一下第二步,使用 Connection 从数据库中查询出对应的 UserId,我们发现最终调用的是 usersConnectionRepository 的 findUserIdsWithConnection 方法,我们看下对应源码:

    // JdbcUsersConnectionRepository.java

    // 83 行
    public List<String> findUserIdsWithConnection(Connection<?> connection) {
       // 1. 获取 ConnectionKey,ConnectionKey 在创建 Connection 时,initKey 方法填充
       ConnectionKey key = connection.getKey();
       // 2. 从数据库中按 providerId 与 providerUserId 查询用户在本地系统中的唯一标识
       List<String> localUserIds = jdbcTemplate.queryForList("select userId from " + tablePrefix + "UserConnection where providerId = ? and providerUserId = ?", String.class, key.getProviderId(), key.getProviderUserId());
       // 3. 如果没有找到用户在本地系统中对应的唯一标识且给 repository 设置了 ConnectionSignUp
       if (localUserIds.size() == 0 && connectionSignUp != null) {
          // 4. 调用自己实现的 execute 方法,在本系统中注册用户,返回注册成功后用户在本系统中的唯一标识
          String newUserId = connectionSignUp.execute(connection);
          if (newUserId != null)
          {
             // 5. 使用用户在本系统中的唯一标示与 Connection 进行绑定操作
             createConnectionRepository(newUserId).addConnection(connection);
             return Arrays.asList(newUserId);
          }
       }
       return localUserIds;
    }
    

    上面的操作,继续总结一下:

    • 获取 Connection 的 Key,实质上 ConnectionKey 是对 providerId 与 providerUserId 的一个封装对象。
    • 使用 ConnectionKey 中的 providerId 与 providerUserId 从数据库中查询用户在本系统中的唯一标示。
    • 如果查询不到,表示该第三方用户还没有绑定过本系统用户
      • 如果 repository 设置了 ConnectionSignUp,会调用 ConnectionSignUp 的 execute 方法 ( 该方法也是需要我们自己实现,一般会在里面进行注册操作 ),帮用户进行注册。然后进行绑定。具体的配置方式,在下面讲账户关联数据库中会进行详细说明。
      • 如果没有设置,直接返回空集合。

然后我们来看一下,存储第三方用户账户与本系统用户账户关系的数据库

数据库表结构是 SpringSocial 设计好的,我们可以自定前缀,程序运行时会自动向数据库中创建,为了防止权限不够,可以在程序运行前,手动创建。

-- This SQL contains a "create table" that can be used to create a table that JdbcUsersConnectionRepository can persist
-- connection in. It is, however, not to be assumed to be production-ready, all-purpose SQL. It is merely representative
-- of the kind of table that JdbcUsersConnectionRepository works with. The table and column names, as well as the general
-- column types, are what is important. Specific column types and sizes that work may vary across database vendors and
-- the required sizes may vary across API providers. 

create table UserConnection (userId varchar(255) not null,
 providerId varchar(255) not null,
 providerUserId varchar(255),
 rank int not null,
 displayName varchar(255),
 profileUrl varchar(512),
 imageUrl varchar(512),
 accessToken varchar(512) not null,
 secret varchar(512),
 refreshToken varchar(512),
 expireTime bigint,
 primary key (userId, providerId, providerUserId));
create unique index UserConnectionRank on UserConnection(userId, providerId, rank);

对数据库的自定义设置

  • 自定义 jdbc 存储的表前缀

    • 自定义 SocialConfigurer 的实现类,重写 getUsersConnectionRepository 方法,在该方法中调用 setTablePrefix 方法给表设置自定义前缀。

    • 注意该 Config 的 Order 要设置为 1,因为系统中存在多个 SocialConfigurer 的实现类时,会取第一个实现类的 getUsersConnectionRepository 方法返回值作为默认实现。

    • 登录时发现未绑定,设置自动注册。调用 setConnectionSignUp 接入自定义的注册逻辑。

      // SocialConfig.java (自定义 SocialConfigurerAdapter 实现类)

      @Autowired(required = false)
      private ConnectionSignUp connectionSignUp;
      
      @Override
      public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
          // dataSource 数据源
          // Encryptors.noOpText() 加密方式 - 不加密
          // 默认建表 sql 与 JdbcUsersConnectionRepository 类在用一个目录下
          JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
          // 设置数据库表前缀,默认为 UserConnection,可以在这个基础上自定义前缀
          repository.setTablePrefix("hblolj_");
          // 自定义默认注册逻辑,社交登录后在数据库中(按服务商类型 + 用户在服务商的唯一标识)没有找到对	  // 应的用户信息,便会调用 ConnectionSignUp 的 execute 方法。我们可以在自定义的 execute 	 // 方法中为用户进行注册。
          if (connectionSignUp != null){
              repository.setConnectionSignUp(connectionSignUp);
          }
          return repository;
      }
      

      // CustomConnectionSignUp.java (自定义 ConnectionSignUp 实现类)

      /**
       * @author: hblolj
       * @Date: 2019/3/19 15:29
       * @Description:
       * @Version:
       **/
      @Component
      public class CustomConnectionSignUp implements ConnectionSignUp {
      
          @Override
          public String execute(Connection<?> connection) {
              // TODO: 2019/3/19 根据用户在第三方服务商的信息给用户在业务服务器上注册一个账户
              // 返回用户在业务系统上的唯一标识,这里不做实际操作,模拟返回用户在第三方服务器上 nickName
              return connection.getDisplayName();
          }
      }
      

自定义注册页面

  • 登录时发现未绑定,引导用户跳转到注册页面

    • 如果没有设置自动注册 ConnectionSignUp 的实现,且当前第三方账户在本系统中没有进行过绑定。SocialAuthenticationProvider 的 authenticate 方法中调用的 toUserId 方法返回的 userId 为 null,想先执行会抛出 BadCredentialsException。(详细分析可以参考上面 标记 6 后续的 SpringSecurity 认证流分析 的第三步)。
    • 这个异常最终会在 SocialAuthenticationFilter 的 doAuthentication 方法中被捕获,在 catch 中会判断 signupUrl 这个属性是否为空。不为空,就通过抛出指定类型异常的方式 redirect 到该页面。为空,就直接抛出异常。
    • 这里的 signupUrl 代表的就是注册页面,那么我们在哪里可以自定义配置这个 signupUrl 呢?我们可以跟踪发现 signupUrl 是 SocialAuthenticationFilter 的一个属性,通过 setSignupUrl 方法被初始化。而 setSignupUrl 方法的调用时机在 SpringSocialConfigurer 类的 configure 方法中,将 SpringSocialConfigurer 的 signupUrl 属性赋值给 SocialAuthenticationFilter 的 signupUrl。

    所以我们只需要自定义一个 SpringSocialConfigurer 的实现来替代默认的 SpringSocialConfigurer,且设置 SpringSocialConfigurer 的 signupUrl 即可。

    // SocialConfig.java (自定义 SocialConfigurerAdapter 实现类)

    /**
     * SpringSocial 认证配置
     * 将该 Config 添加到 WebSecurityConfig 中
     * @return
     */
    @Bean
    public SpringSocialConfigurer customSocialSecurityConfig(){
        // return new SpringSocialConfigurer();
        CustomSpringSocialConfigurer configurer = new CustomSpringSocialConfigurer(securityProperties.getSocial().getFilterProcessesUrl());
        // 配置注册页面,当找不到用户时,会跳转到该页面
        configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
        // 设置登录成功跳转地址
        configurer.postLoginUrl("/hello");
        return configurer;
    }
    
  • 添加接口,注册页面访问该接口进行注册、绑定

    注册页面只是注册的一半,我们还需要准备一个注册接口,在这个接口中完成以下两步:

    • 在本系统中注册一个用户
    • 将注册的用户与第三方账户进行绑定
    @Autowired
    private ProviderSignInUtils providerSignInUtils;
    
    @PostMapping("/regist")
    public void regist(@RequestParam String username, @RequestParam String password, HttpServletRequest request){
    
        log.info("regist");
    
        // TODO: 2019/3/19 注册用户
        // 不管是注册还是绑定,都会获取到用户在业务系统中的唯一标识
        // 注册完成后进行绑定
        // doPostSignUp 会从 Session 中取出抛出异常时存储的 ProviderSignInAttempt,
        // 使用其进行绑定,所以只有第三方登录状态才可以使用该注册,并且在本次 Session 中
        providerSignInUtils.doPostSignUp(username, new ServletWebRequest(request));
    }
    

与 SpringSecurity 的集成

SpringSocial 与 SpringSecurity 的集成,与我们前面集成自定义的短信登录、微信登录其实差不多,都是将自定义的过滤器加入到 SpringSecurity 的过滤器链中,达到加入整个认证流程的效果。

// WebSecurityConfig.java (WebSecurityConfigurerAdapter 的实现类,SpringSecurity 的配置类)

@Autowired
private SmsCodeAuthenticationConfig smsCodeAuthenticationConfig;

@Autowired
private WxAuthenticationConfig wxAuthenticationConfig;

/**
 * Social 认证配置
 */
@Autowired
private SpringSocialConfigurer customSocialConfig;

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.apply(smsCodeAuthenticationConfig) // 将短信认证加入认证流程
                .and()
            .apply(wxAuthenticationConfig) // 将微信认证加入认证流程
                .and()
            .apply(customSocialConfig) // 将 SpringSocial 认证加入认证流程
                .....

// CustomSpringSocialConfigurer.java (SpringSocialConfigurer 的实现类)

/**
 * @author: hblolj
 * @Date: 2019/3/18 14:07
 * @Description:
 * @Version:
 **/
public class CustomSpringSocialConfigurer extends SpringSocialConfigurer{

    // 自定义 SocialAuthenticationFilter 拦截的 Url
    private String filterProcessesUrl;

    public CustomSpringSocialConfigurer(String filterProcessesUrl) {
        this.filterProcessesUrl = filterProcessesUrl;
    }

    @Override
    protected <T> T postProcess(T object) {

        SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);

        filter.setFilterProcessesUrl(filterProcessesUrl);

        return (T) filter;
    }
}

绑定、解绑的使用

我们目前的绑定只能通过自动注册、自定义注册页面注册的动作来完成。但是在我们的实际需求中,可能在用户的个人信息页面,支持用户灵活的绑定或解绑定第三方账号。那么这些操作在 SpringSocial 中是怎么实现的呢?

首先通过前面的分析,我们应该知道绑定、解绑的核心是对我们系统中用户账号与对应的第三方系统中用户账号的关联关系的建立与消除。他有两个前置条件:

  • 用户在当前系统已经登录了
  • 用户通过第三方系统登录获取到在第三方系统上的唯一标识

在 SpringSocial 的 官网 上,我们可以看到它提供了一个 ConnectController 控制器,用来负责将用户引导服务提供商进行授权,并在授权后响应回调,协调应用程序与服务商之前的连接关系(绑定、解绑)。

我们看一下它的源码,里面具体方法就不一个一个去看了,主要结合注释与方法源码,把总结出来的结果说一下:

// ConnectController.java

/**
 * Generic UI controller for managing the account-to-service-provider connection flow.
 * <ul>
 * <li>GET /connect/{providerId}  - Get a web page showing connection status to {providerId}.</li>
 * <li>POST /connect/{providerId} - Initiate an connection with {providerId}.</li>
 * <li>GET /connect/{providerId}?oauth_verifier||code - Receive {providerId} authorization callback and establish the connection.</li>
 * <li>DELETE /connect/{providerId} - Disconnect from {providerId}.</li>
 * </ul>
 * @author Keith Donald
 * @author Craig Walls
 * @author Roy Clarkson
 */
@Controller
@RequestMapping("/connect")
public class ConnectController implements InitializingBean {
	.........
}

通过上面的注释与对应方法的源码,最后总结如下

  • ConnectController
    • 接收回调与现实绑定与解绑结果
      • Get /connect/{providerId}
        • 根据获取到的用户是否存在 connectio
          • 存在: 跳转到 connect/{providerId}Connected
          • 不存在: 跳转到 connect/{providerId}Connect
    • 绑定
      • Post /connect/{providerId}
        • 引导用户到 Service Provider 授权,回调回来访问 Get /connect/{providerId}
    • 解绑
      • Delete /connect/{providerId}
        • 解绑后,redirect 到 Get /connect/{providerId}
    • 查看当前用户绑定状态
      • Get /connect
        • 跳转到 connect/status

从上面的分析结果,我们需要自定义一些 View 来承载与实现上面的结果。

下面示例代码中的 callback 是 providerId

/**
 * 可以自定义一个名称为 qqConnectedView 的 Bean 来覆盖此默认实现
 * callbackConnect 是解绑成功
 * @return
 */
@Bean({"connect/callbackConnect"})
@ConditionalOnMissingBean(name = "qqConnectView")
public View qqConnectView(){
    return new CustomConnectionView();
}

/**
 * callbackConnected 是绑定成功
 * @return
 */
@Bean({"connect/callbackConnected"})
@ConditionalOnMissingBean(name = "qqConnectedView")
public View qqConnectedView(){
    return new CustomConnectionView();
}

// CustomConnectionView.java (自定义视图类,用来返回用户绑定、解绑结果)

/**
 * @author: hblolj
 * @Date: 2019/3/19 17:31
 * @Description:
 * @Version:
 **/
@Slf4j
public class CustomConnectionView extends AbstractView{

    @Override
    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {

        httpServletResponse.setContentType("text/html;charset=UTF-8");

        if (model.get("connections") == null){
            // 从数据库中查询该用户与对应的 providerId 已经没有关联关系了
            httpServletResponse.getWriter().write("<h3>解绑成功</h3>");
        }else {
            // 从数据库中查询出该用户与对应的 providerId 任然存在关联关系
            httpServletResponse.getWriter().write("<h3>绑定成功</h3>");
        }
    }
}

// CustomConnectionStatusView.java (自定义视图类,用来查询返回用户当前第三方服务商的绑定情况)

/**
 * @author: hblolj
 * @Date: 2019/3/19 17:02
 * @Description:
 * @Version:
 **/
@Component("connect/status")
public class CustomConnectionStatusView extends AbstractView{

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 查询出当前用户在系统中的第三方服务的绑定的绑定情况
     * @param model
     * @param httpServletRequest
     * @param httpServletResponse
     * @throws Exception
     */
    @Override
    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {

//        model.addAttribute("providerIds", this.connectionFactoryLocator.registeredProviderIds());
//        model.addAttribute("connectionMap", connections);

        // 获取到用户在当前系统中所有第三方登录关联记录
        Map<String, List<Connection<?>>> connections = (Map<String, List<Connection<?>>>) model.get("connectionMap");
        Map<String, Boolean> result = new HashMap<>();
        for (String key : connections.keySet()) {
            result.put(key, org.apache.commons.collections.CollectionUtils.isNotEmpty(connections.get(key)));
        }

        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
    }
}

设置 SocialAuthenticationFilter 的拦截 Url

  • 在自定义 CustomSpringSocialConfigurer 时注入,上面已经实现

登录成功的跳转页面设置

  • 在构造 customSocialSecurityConfig 时设置,上面已经实现
SpringSecurity OAuth2开发相关聚合文章 前言 说真的,SpringSecurity OAuth2这套框架会的人真的不多,尤其是比较深入的使用,这也是我聚合这篇文章的初衷!就相对我自己学习这个而言,也花了好多时间,从简单学习SpringSecurity,到分析整个运行流程,再到SpringSecurity OAuth2学习,再到深入源码,授权流程。说实话一路过来确实不容易。真的需要花时间经历,只有这样才能将这套框架变成自己的! 聚合 1. SpringSecurity OAuth开发前后端分离认证框架介绍 ... 阅读详情

相关推荐

量子密钥分发(QKD)原理、工程实现与实战部署全解析

密钥分发是信息安全的核心基础,它解决了通信双方如何安全共享秘密密钥的难题。传统方法依赖于数学问题的计算复杂性,而量子密钥分发(QKD)则基于量子力学原理,为密钥分发提供了信息论安全的物理保障。其核心价值在于,利用量子不可克隆定理和测量坍缩原理,使得任何窃听行为都会被合法通信方检测到,从而在理论上实现无条件安全的密钥协商。这项技术特别适用于对安全性要求极高的场景,如金融数据同步、政务专网和电力系统保护。本文聚焦于量子保密通信,深入剖析了以BB84协议为代表的QKD工作原理,并详细拆解了包含单光子探测、偏振编码

weixin_29324401的博客 350

Spring Social 认证流程源码详解

Spring Security 过滤链 认证流程源码详解 SocialAuthenticationFilter会将请求拦截下来然后将整个流程走完。进而去实现第三方登录。详细流程如下。 我们来看一下SocialAuthenticationFilter的源码 private Authentication attemptAuthService(SocialAuthenticationService...

暴走猿人的博客 564

proteus仿真51单片机操作指南:串口通信仿真配置详解

深入讲解如何在proteus仿真51单片机中实现串口通信,涵盖虚拟串口设置、电路连接与调试技巧,帮助开发者高效完成通信仿真验证。

weixin_36197669的博客 573

SpringSocial基本原理

SpringSocial:用于第三方应用登陆。 官网:https://projects.spring.io/spring-social/ 原理图: (来源于网上) SocialAuthenticationFilter:用于拦截第三方应用的请求。 public class...

喝醉的咕咕鸟 3702

【异常】解决JDK21升级中的SecurityException: JCE无法验证BC提供者问题

本文详细解析了JDK21升级中常见的SecurityException: JCE无法验证BC提供者问题的根源与解决方案。核心在于JDK21强化了JCE安全验证,导致签名不合规的老版本Bouncy Castle库被拒绝。文章提供了通过Maven依赖树精准定位、排除旧版本并引入兼容的bcprov-jdk18on新库的完整步骤,以及清理缓存等实战技巧,帮助开发者彻底解决此兼容性问题。

weixin_29197051的博客 492

基于RTL8370N的8口千兆无管理交换机硬件设计

无管理交换机是局域网边缘接入的核心基础设备,其本质是通过硬件状态机实现MAC地址学习、线速转发与自动协商,无需软件配置即可完成即插即用组网。原理上依赖高度集成的PHY+交换矩阵单芯片(如RTL8370N),结合确定性硬件逻辑替代MCU固件,保障低延迟、高可靠与零运维。技术价值体现在成本压缩、体积精简与部署极简化,广泛应用于家庭网络扩展、工业现场设备联网及嵌入式系统桥接等场景。本文详解一款基于RTL8370N的紧凑型8端口千兆无管理交换机,覆盖芯片选型依据、PHY接口设计、电源与散热优化等关键工程实践。

weixin_42327217的博客 402

SpringSecurity学习总结-4 第五章 使用Spring Social开发第三方登录

SpringSecurity学习总结-4 第五章 使用Spring Social开发第三方登录

Mr_XiMu的博客 1712

汽车工程师必看:从CAN到Ethernet,6种车载通信协议全解析(附应用场景对比)

本文全面解析了从CAN到Ethernet的6种车载通信协议,包括CAN、LIN、MOST、FlexRay等,并对比了它们的应用场景和技术特点。文章详细介绍了各协议的演进历程、技术优势及在现代汽车中的混合网络架构设计,为汽车工程师提供了实用的协议选型指南和未来通信技术发展趋势。

weixin_28683689的博客 493

Spring Security 源码分析六:Spring Social 社交登录源码解析

1. 引言 OAuth2是一种授权协议,简单理解就是它可以让用户在不将用户名密码交给第三方应用的情况下,第三方应用有权访问用户存在服务提供商上面的数据。 1.1 Spring Social 基本原理 访问第三方应用 将用户请求导向服务提供商 用户同意授权 携带授权码返回第三方莹莹 第三方应用携带授权码到服务提供商申请令牌 服务提供商返回令牌 获取用户基本信息 根据用户信息构建Authentication放入SecurityContext中如果在SecurityContext中放入一.

weixin_42073629的博客 330

实操微信支付,适合第一次接通微信支付的同学

我的实际操作命令如下:就会生成图3中多出来的file文件夹,/Users/camel/cert/xxxxxx_cert/file/wechatpay_5A4xxxxxxxxxx9.pem,wechatpay_xxxxx5A4849E9.pem就是微信支付平台证书,也是程序真正需要的。•注意:我的操作说明,我是把下载好的 CertificateDownloader.jar 文件,放在了解压证书的目录里(图3)。//下面的静态参数要仔细看下面的解释,否则只会调用别人写好的方法,压根学不会0-1接入微信支付。

wjianwei666的专栏 525

SE模块理解+SE-Resnet模块pytorch实现

文章目录SE模块理解SE实现注意力机制原因SE-resnet网络pytorch实现定义residual定义SE模块 SE模块理解 SENet是Squeeze-and-Excitation Networks的简称,拿到了ImageNet2017分类比赛冠军,其效果得到了认可,其提出的SE模块思想简单,易于实现,并且很容易可以加载到现有的网络模型框架中。SENet主要是学习了channel之间的相关性,筛选出了针对通道的注意力,稍微增加了一点计算量,但是效果比较好。 SE模块广泛应用于去噪,去雨方向,利用SE模

qq_42754919的博客 2万+

Spring Security从应用到源码,这份手册都讲全了

随着各种安全性问题的不断发生,可以说,安全性技术是构建个人技术体系不可缺少的一个环节,对于提升你的职业门槛也是一个重要的加分项。然而却有很多小伙伴在学习Spring Security 总是遇到重重困难

公众号:该用户快成仙了 218

Power domain 划分该如何下手

假如PD1比PD3更always on的话,我们只需要插入普通buffer即可,因为PD3 on,PD1也肯定是ON,PD1 off,PD3肯定时off,PD1能cover PD3的电源情况,因此使用普通buffer也可,但是处于省电角度,我们还是使用AO buffer;在我们做多power domain时,在切分power domain时,需要谨慎,稍有不慎,就会出现congestion以及timing问题,那问题来了,我们在切分power domain时有什么考究么,今天小编就和大家一起讨论讨论。

芯冰乐 207

Day247.SpringSocia源码分析、QQ互联注册及应用创建&实现QQ登录功能&登录功能细节处理&用户关系绑定 -springsecurity-jwt-oauth2

1.SpringSocia源码分析 一、Spring Social结构化角度解析源码 Spring Social是一个帮助我们连接社交媒体平台,方便在我们自己的应用上开发第三方登录认证等功能的Spring 类库。其中比较核心的类和接口,如下图所示,我们来一一解析。 首先我们简单回顾一下OAuth2,OAuth2主要包含两部分内容:认证和鉴权。 认证过程就是通过用户授权,获取授权码,最终换取AccessToken的过程。这个过程是标准的OAuth2认证流程,所有平台都遵循,可以认为是一致的。 鉴权过程就是携

阿昌爱Java 1113

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

全网首发阿里内部Spring Security项目实战搭建

1、看视频进行系统学习这几年的Crud经历,让我明白自己真的算是菜鸡中的战斗机,也正因为Crud,导致自己技术比较零散,也不够深入不够系统,所以重新进行学习是很有必要的。我差的是系统知识,差的结构框架和思路,所以通过视频来学习,效果更好,也更全面。关于视频学习,个人可以推荐去B站进行学习,B站上有很多学习视频,唯一的缺点就是免费的容易过时。2、读源码,看实战笔记,学习大神思路“编程语言是程序员的表达的方式,而架构是程序员对世界的认知”。所以,程序员要想快速认知并学习架构,读源码是必不可少的。

lingyued的博客 355

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

跨域不生效_年轻人你不讲武德,自己偷着学习!Spring Security五套「源码级」笔记哪里来的?我也要!...

Spring Security是一个强大且高度可定制的安全框架,致力于为Java应用提供身份认证和授权。本篇文章将会通过5个部分内容由浅入深地介绍了Spring Security的方方面面。第1部分主要讲解Spring Security的基本配置。第2部分剖析Web项目可能遇到的安全问题,并讲解如何使用SpringSecurity进行有效防护。第3部分详细介绍OAuth,并使用Spring Soc...

weixin_39988779的博客 80

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

你离弯道超车只差一份阿里大牛的Spring Security宝典

前言 据有关数据表明Spring Security在Java应用安全领域已经慢慢成为首先被推崇的安全解决方案。虽然它在Java应用安全领域所占比重越来越大,但大多数开发者面对Spring Security这样的“庞然大物”时无从入手,也因为对其不够了解而在实际项目中不敢轻易采用。如何学?怎么把它引入到项目里?已经是每一个开发人员需要考虑的问题。 为了能带大家进入Spring Security的新世界,也本着好东西就是要拿出来分享的原则!今天互联网雷锋(小编我)就把从阿里一位老哥手上得到的Spring S

文章中资料均可获取,有需要请添加yunduoa2019即可 136

Spring Security源码分析三:Spring Social实现QQ社交登录

Spring Security源码分析三:Spring Social实现QQ社交登录 &lt;!-- 作者区域 --&gt; &lt;div class="author"&gt; &lt;a class="avatar" href="/u/6476f5a7648e"&gt; &lt;img src="//upload.ji

小屁孩大帅-保密的博客 780

百度架构师手写万字Spring Security实战笔记,带你玩转Spring Security

Spring Security是一个强大且高度可定制的安全框架,致力于为Java应用提供身份认证和授权。虽然Spring Security有强大的功能,但它同时也有很高的学习成本。它囊括了身份认证的各种应用场景以及Web安全的大量知识,仅官方参考手册就有数十万字,并且还省略了诸多实现细节。许多开发人员在面对这样的“庞然大物”时无从入手,更因为对其不够了解而在实际项目中不敢轻易采用。

m0_73311735的博客 147

百度架构师手写万字Spring Security实战,一篇就搞懂

Spring Security是一个强大且高度可定制的安全框架,致力于为Java应用提供身份认证和授权。 虽然Spring Security有强大的功能,但它同时也有很高的学习成本。它囊括了身份认证的各种应用场景以及Web安全的大量知识,仅官方参考手册就有数十万字,并且还省略了诸多实现细节。许多开发人员在面对这样的“庞然大物”时无从入手,更因为对其不够了解而在实际项目中不敢轻易采用。

老男孩的架构路 132

Spring Security 源码分析十六:Spring Security 项目实战

1. 前言 本章是根据前面Spring Security系列实现一个基于角色的权限管理系统。 1.1 技术栈 Spring Boot Spring Security Spring Social(需配置host127.0.0.1 www.merryyou.cn) Spring Data JPA Freemarker Mysql Redis 前端miniui(非开源) 1.2 效果图 1.3 部分代码 $.ajax({ url: "${re.contextPat

weixin_42073629的博客 233

Spring Security源码分析六:Spring Social社交登录源码解析

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_33991727的博客 128

瞳孔盛宴,饿了么架构师力推的5份SpringSecurity笔记,源码经验双管旗下,太强了

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

m0_67645544的博客 523

springsecurity+springsocial资料收集

https://blog.youkuaiyun.com/tryandfight/article/details/80524573 https://niocoder.com/2018/01/09/Spring-Security%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B8%89-Spring-Social%E7%A4%BE%E4%BA%A4%E7%99%BB%E5%...

weixin_30701521的博客 87
上一篇: SpringSocial 开发 QQ 登录
Earth_Programer
博客等级 码龄11年 18粉丝 17原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值