前后端分离项目 — SpringSocial 绑定与解绑社交账号如微信、QQ

Java基础

浏览数:475

2018-12-11

AD:资源代下载服务

1、准备工作

申请QQ、微信相关AppId和AppSecret,这些大家自己到QQ互联微信开发平台 去申请吧
还有java后台要引入相关的jar包,如下:

 <dependencies>
     <dependency>
         <groupId>org.springframework.security.oauth.boot</groupId>
         <artifactId>spring-security-oauth2-autoconfigure</artifactId>
     </dependency>
     <dependency>
         <groupId>org.springframework.security.oauth</groupId>
         <artifactId>spring-security-oauth2</artifactId>
         <version>2.3.3.RELEASE</version>
     </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-security</artifactId>
     </dependency>
     <!--<dependency>-->
         <!--<groupId>org.springframework.cloud</groupId>-->
         <!--<artifactId>spring-cloud-starter-security</artifactId>-->
     <!--</dependency>-->
     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-security</artifactId>
     </dependency>

     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-oauth2</artifactId>
     </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-redis</artifactId>
     </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-jdbc</artifactId>
     </dependency>
     <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>
     </dependency>
     <dependency>
         <groupId>org.springframework.social</groupId>
         <artifactId>spring-social-config</artifactId>
         <version>1.1.6.RELEASE</version>
     </dependency>
     <dependency>
         <groupId>org.springframework.social</groupId>
         <artifactId>spring-social-core</artifactId>
         <version>1.1.6.RELEASE</version>
     </dependency>
     <dependency>
         <groupId>org.springframework.social</groupId>
         <artifactId>spring-social-security</artifactId>
         <version>1.1.6.RELEASE</version>
     </dependency>
     <dependency>
         <groupId>org.springframework.social</groupId>
         <artifactId>spring-social-web</artifactId>
         <version>1.1.6.RELEASE</version>
     </dependency>
     <dependency>
         <groupId>io.jsonwebtoken</groupId>
         <artifactId>jjwt</artifactId>
         <version>0.9.1</version>
     </dependency>

     <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-lang3</artifactId>
         <version>3.7</version>
     </dependency>

     <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-collections4</artifactId>
         <version>4.2</version>
     </dependency>

     <dependency>
         <groupId>commons-beanutils</groupId>
         <artifactId>commons-beanutils</artifactId>
         <version>1.9.3</version>
     </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-configuration-processor</artifactId>
     </dependency>
     <dependency>
         <groupId>org.springframework.data</groupId>
         <artifactId>spring-data-mongodb</artifactId>
         <version>2.0.9.RELEASE</version>
     </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-mongodb</artifactId>
         <version>2.0.4.RELEASE</version>
     </dependency>
     <dependency>
         <groupId>com.fasterxml.jackson.core</groupId>
         <artifactId>jackson-core</artifactId>
         <version>2.9.6</version>
     </dependency>

然后在application.properties里面设置相关配置,如redis、mysql等设置,如下:

 spring.datasource.url=
 spring.datasource.username=
 spring.datasource.password=
 spring.datasource.driverClassName=com.mysql.jdbc.Driver
 
 spring.redis.host=127.0.0.1
 spring.redis.password=your_pwd
 spring.redis.port=6379
 spring.redis.timeout=30000
 
 ssb.security.social.register-url=/social/signUp
 ssb.security.social.filter-processes-url=/social-login
 ssb.security.social.bind-url=https://website/social-bind/qq
 ssb.security.social.callback-url=https://website/social-login
 ssb.security.social.connect-url=https://website/social-connect

 //QQ授权
 ssb.security.social.qq.app-id=
 ssb.security.social.qq.app-secret=
 ssb.security.social.qq.provider-id=qq
 
 //WeChat授权
 ssb.security.social.wechat.app-id=
 ssb.security.social.wechat.app-secret=
 ssb.security.social.wechat.provider-id=wechat

2、分析社交绑定ConnectController类

准备工作做好之后,现在我们开始分析社交绑定,其实spring-social框架里已经自带了spring-social-web,这个jar包里面有个ConnectController.java类,这个类已经帮我们实现了相关绑定与解绑实现方法,问题在于它是基于Session的,所以如果是前后端分离项目使用Session当然应有问题,所以我们要结合Redis来使用,把相关变量都存在Redis中,所以我们上面已经配置好了Redis,我们再来看看Redis配置代码:

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory){
        return new RestTemplate(factory);
    }

    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(50000);//单位为ms
        factory.setConnectTimeout(50000);//单位为ms
        return factory;
    }
}

3、获取系统当前用户所有社交账号绑定情况

设置好之后,我们来分析一下spring-social-web这个jar包获取社交账号绑定情况,它的请求地址是/connect,代码如下:

@Controller
@RequestMapping({"/connect"})
public class ConnectController implements InitializingBean {
    private static final Log logger = LogFactory.getLog(ConnectController.class);
    private final ConnectionFactoryLocator connectionFactoryLocator;
    private final ConnectionRepository connectionRepository;
    private final MultiValueMap<Class<?>, ConnectInterceptor<?>> connectInterceptors = new LinkedMultiValueMap();
    private final MultiValueMap<Class<?>, DisconnectInterceptor<?>> disconnectInterceptors = new LinkedMultiValueMap();
    private ConnectSupport connectSupport;
    private final UrlPathHelper urlPathHelper = new UrlPathHelper();
    private String viewPath = "connect/";
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    private String applicationUrl = null;
    protected static final String DUPLICATE_CONNECTION_ATTRIBUTE = "social_addConnection_duplicate";
    protected static final String PROVIDER_ERROR_ATTRIBUTE = "social_provider_error";
    protected static final String AUTHORIZATION_ERROR_ATTRIBUTE = "social_authorization_error";

    @Inject
    public ConnectController(ConnectionFactoryLocator connectionFactoryLocator, ConnectionRepository connectionRepository) {
        this.connectionFactoryLocator = connectionFactoryLocator;
        this.connectionRepository = connectionRepository;
    }

    /** @deprecated */
    @Deprecated
    public void setInterceptors(List<ConnectInterceptor<?>> interceptors) {
        this.setConnectInterceptors(interceptors);
    }

    public void setConnectInterceptors(List<ConnectInterceptor<?>> interceptors) {
        Iterator var2 = interceptors.iterator();

        while(var2.hasNext()) {
            ConnectInterceptor<?> interceptor = (ConnectInterceptor)var2.next();
            this.addInterceptor(interceptor);
        }

    }

    public void setDisconnectInterceptors(List<DisconnectInterceptor<?>> interceptors) {
        Iterator var2 = interceptors.iterator();

        while(var2.hasNext()) {
            DisconnectInterceptor<?> interceptor = (DisconnectInterceptor)var2.next();
            this.addDisconnectInterceptor(interceptor);
        }

    }

    public void setApplicationUrl(String applicationUrl) {
        this.applicationUrl = applicationUrl;
    }

    public void setViewPath(String viewPath) {
        this.viewPath = viewPath;
    }

    public void setSessionStrategy(SessionStrategy sessionStrategy) {
        this.sessionStrategy = sessionStrategy;
    }

    public void addInterceptor(ConnectInterceptor<?> interceptor) {
        Class<?> serviceApiType = GenericTypeResolver.resolveTypeArgument(interceptor.getClass(), ConnectInterceptor.class);
        this.connectInterceptors.add(serviceApiType, interceptor);
    }

    public void addDisconnectInterceptor(DisconnectInterceptor<?> interceptor) {
        Class<?> serviceApiType = GenericTypeResolver.resolveTypeArgument(interceptor.getClass(), DisconnectInterceptor.class);
        this.disconnectInterceptors.add(serviceApiType, interceptor);
    }

    @RequestMapping(
        method = {RequestMethod.GET}
    )
    public String connectionStatus(NativeWebRequest request, Model model) {
        this.setNoCache(request);
        this.processFlash(request, model);
        Map<String, List<Connection<?>>> connections = this.connectionRepository.findAllConnections();
        model.addAttribute("providerIds", this.connectionFactoryLocator.registeredProviderIds());
        model.addAttribute("connectionMap", connections);
        return this.connectView();
    }

    @RequestMapping(
        value = {"/{providerId}"},
        method = {RequestMethod.GET}
    )
    public String connectionStatus(@PathVariable String providerId, NativeWebRequest request, Model model) {
        this.setNoCache(request);
        this.processFlash(request, model);
        List<Connection<?>> connections = this.connectionRepository.findConnections(providerId);
        this.setNoCache(request);
        if(connections.isEmpty()) {
            return this.connectView(providerId);
        } else {
            model.addAttribute("connections", connections);
            return this.connectedView(providerId);
        }
    }

    @RequestMapping(
        value = {"/{providerId}"},
        method = {RequestMethod.POST}
    )
    public RedirectView connect(@PathVariable String providerId, NativeWebRequest request) {
        ConnectionFactory<?> connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId);
        MultiValueMap<String, String> parameters = new LinkedMultiValueMap();
        this.preConnect(connectionFactory, parameters, request);

        try {
            return new RedirectView(this.connectSupport.buildOAuthUrl(connectionFactory, request, parameters));
        } catch (Exception var6) {
            this.sessionStrategy.setAttribute(request, "social_provider_error", var6);
            return this.connectionStatusRedirect(providerId, request);
        }
    }

    @RequestMapping(
        value = {"/{providerId}"},
        method = {RequestMethod.GET},
        params = {"oauth_token"}
    )
    public RedirectView oauth2Callback(@PathVariable String providerId, NativeWebRequest request) {
        try {
            OAuth2ConnectionFactory<?> connectionFactory = (OAuth2ConnectionFactory)this.connectionFactoryLocator.getConnectionFactory(providerId);
            Connection<?> connection = this.connectSupport.completeConnection(connectionFactory, request);
            this.addConnection(connection, connectionFactory, request);
        } catch (Exception var5) {
            this.sessionStrategy.setAttribute(request, "social_provider_error", var5);
            logger.warn("Exception while handling OAuth2 callback (" + var5.getMessage() + "). Redirecting to " + providerId + " connection status page.");
        }

        return this.connectionStatusRedirect(providerId, request);
    }

    @RequestMapping(
        value = {"/{providerId}"},
        method = {RequestMethod.GET},
        params = {"code"}
    )
    public RedirectView oauth2Callback(@PathVariable String providerId, NativeWebRequest request) {
        try {
            OAuth2ConnectionFactory<?> connectionFactory = (OAuth2ConnectionFactory)this.connectionFactoryLocator.getConnectionFactory(providerId);
            Connection<?> connection = this.connectSupport.completeConnection(connectionFactory, request);
            this.addConnection(connection, connectionFactory, request);
        } catch (Exception var5) {
            this.sessionStrategy.setAttribute(request, "social_provider_error", var5);
            logger.warn("Exception while handling OAuth2 callback (" + var5.getMessage() + "). Redirecting to " + providerId + " connection status page.");
        }

        return this.connectionStatusRedirect(providerId, request);
    }

    @RequestMapping(
        value = {"/{providerId}"},
        method = {RequestMethod.GET},
        params = {"error"}
    )
    public RedirectView oauth2ErrorCallback(@PathVariable String providerId, @RequestParam("error") String error, @RequestParam(value = "error_description",required = false) String errorDescription, @RequestParam(value = "error_uri",required = false) String errorUri, NativeWebRequest request) {
        Map<String, String> errorMap = new HashMap();
        errorMap.put("error", error);
        if(errorDescription != null) {
            errorMap.put("errorDescription", errorDescription);
        }

        if(errorUri != null) {
            errorMap.put("errorUri", errorUri);
        }

        this.sessionStrategy.setAttribute(request, "social_authorization_error", errorMap);
        return this.connectionStatusRedirect(providerId, request);
    }

    @RequestMapping(
        value = {"/{providerId}"},
        method = {RequestMethod.DELETE}
    )
    public RedirectView removeConnections(@PathVariable String providerId, NativeWebRequest request) {
        ConnectionFactory<?> connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId);
        this.preDisconnect(connectionFactory, request);
        this.connectionRepository.removeConnections(providerId);
        this.postDisconnect(connectionFactory, request);
        return this.connectionStatusRedirect(providerId, request);
    }

    @RequestMapping(
        value = {"/{providerId}/{providerUserId}"},
        method = {RequestMethod.DELETE}
    )
    public RedirectView removeConnection(@PathVariable String providerId, @PathVariable String providerUserId, NativeWebRequest request) {
        ConnectionFactory<?> connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId);
        this.preDisconnect(connectionFactory, request);
        this.connectionRepository.removeConnection(new ConnectionKey(providerId, providerUserId));
        this.postDisconnect(connectionFactory, request);
        return this.connectionStatusRedirect(providerId, request);
    }

    protected String connectView() {
        return this.getViewPath() + "status";
    }

    protected String connectView(String providerId) {
        return this.getViewPath() + providerId + "Connect";
    }

    protected String connectedView(String providerId) {
        return this.getViewPath() + providerId + "Connected";
    }

    protected RedirectView connectionStatusRedirect(String providerId, NativeWebRequest request) {
        HttpServletRequest servletRequest = (HttpServletRequest)request.getNativeRequest(HttpServletRequest.class);
        String path = "/connect/" + providerId + this.getPathExtension(servletRequest);
        if(this.prependServletPath(servletRequest)) {
            path = servletRequest.getServletPath() + path;
        }

        return new RedirectView(path, true);
    }

    public void afterPropertiesSet() throws Exception {
        this.connectSupport = new ConnectSupport(this.sessionStrategy);
        if(this.applicationUrl != null) {
            this.connectSupport.setApplicationUrl(this.applicationUrl);
        }

    }

    private boolean prependServletPath(HttpServletRequest request) {
        return !this.urlPathHelper.getPathWithinServletMapping(request).equals("");
    }

    private String getPathExtension(HttpServletRequest request) {
        String fileName = this.extractFullFilenameFromUrlPath(request.getRequestURI());
        String extension = StringUtils.getFilenameExtension(fileName);
        return extension != null?"." + extension:"";
    }

    private String extractFullFilenameFromUrlPath(String urlPath) {
        int end = urlPath.indexOf(63);
        if(end == -1) {
            end = urlPath.indexOf(35);
            if(end == -1) {
                end = urlPath.length();
            }
        }

        int begin = urlPath.lastIndexOf(47, end) + 1;
        int paramIndex = urlPath.indexOf(59, begin);
        end = paramIndex != -1 && paramIndex < end?paramIndex:end;
        return urlPath.substring(begin, end);
    }

    private String getViewPath() {
        return this.viewPath;
    }

    private void addConnection(Connection<?> connection, ConnectionFactory<?> connectionFactory, WebRequest request) {
        try {
            this.connectionRepository.addConnection(connection);
            this.postConnect(connectionFactory, connection, request);
        } catch (DuplicateConnectionException var5) {
            this.sessionStrategy.setAttribute(request, "social_addConnection_duplicate", var5);
        }

    }

    private void preConnect(ConnectionFactory<?> connectionFactory, MultiValueMap<String, String> parameters, WebRequest request) {
        Iterator var4 = this.interceptingConnectionsTo(connectionFactory).iterator();

        while(var4.hasNext()) {
            ConnectInterceptor interceptor = (ConnectInterceptor)var4.next();
            interceptor.preConnect(connectionFactory, parameters, request);
        }

    }

    private void postConnect(ConnectionFactory<?> connectionFactory, Connection<?> connection, WebRequest request) {
        Iterator var4 = this.interceptingConnectionsTo(connectionFactory).iterator();

        while(var4.hasNext()) {
            ConnectInterceptor interceptor = (ConnectInterceptor)var4.next();
            interceptor.postConnect(connection, request);
        }

    }

    private void preDisconnect(ConnectionFactory<?> connectionFactory, WebRequest request) {
        Iterator var3 = this.interceptingDisconnectionsTo(connectionFactory).iterator();

        while(var3.hasNext()) {
            DisconnectInterceptor interceptor = (DisconnectInterceptor)var3.next();
            interceptor.preDisconnect(connectionFactory, request);
        }

    }

    private void postDisconnect(ConnectionFactory<?> connectionFactory, WebRequest request) {
        Iterator var3 = this.interceptingDisconnectionsTo(connectionFactory).iterator();

        while(var3.hasNext()) {
            DisconnectInterceptor interceptor = (DisconnectInterceptor)var3.next();
            interceptor.postDisconnect(connectionFactory, request);
        }

    }

    private List<ConnectInterceptor<?>> interceptingConnectionsTo(ConnectionFactory<?> connectionFactory) {
        Class<?> serviceType = GenericTypeResolver.resolveTypeArgument(connectionFactory.getClass(), ConnectionFactory.class);
        List<ConnectInterceptor<?>> typedInterceptors = (List)this.connectInterceptors.get(serviceType);
        if(typedInterceptors == null) {
            typedInterceptors = Collections.emptyList();
        }

        return typedInterceptors;
    }

    private List<DisconnectInterceptor<?>> interceptingDisconnectionsTo(ConnectionFactory<?> connectionFactory) {
        Class<?> serviceType = GenericTypeResolver.resolveTypeArgument(connectionFactory.getClass(), ConnectionFactory.class);
        List<DisconnectInterceptor<?>> typedInterceptors = (List)this.disconnectInterceptors.get(serviceType);
        if(typedInterceptors == null) {
            typedInterceptors = Collections.emptyList();
        }

        return typedInterceptors;
    }

    private void processFlash(WebRequest request, Model model) {
        this.convertSessionAttributeToModelAttribute("social_addConnection_duplicate", request, model);
        this.convertSessionAttributeToModelAttribute("social_provider_error", request, model);
        model.addAttribute("social_authorization_error", this.sessionStrategy.getAttribute(request, "social_authorization_error"));
        this.sessionStrategy.removeAttribute(request, "social_authorization_error");
    }

    private void convertSessionAttributeToModelAttribute(String attributeName, WebRequest request, Model model) {
        if(this.sessionStrategy.getAttribute(request, attributeName) != null) {
            model.addAttribute(attributeName, Boolean.TRUE);
            this.sessionStrategy.removeAttribute(request, attributeName);
        }

    }

    private void setNoCache(NativeWebRequest request) {
        HttpServletResponse response = (HttpServletResponse)request.getNativeResponse(HttpServletResponse.class);
        if(response != null) {
            response.setHeader("Pragma", "no-cache");
            response.setDateHeader("Expires", 1L);
            response.setHeader("Cache-Control", "no-cache");
            response.addHeader("Cache-Control", "no-store");
        }

    }
}

上面就是ConnectController的源码了,我们现在分析一下获取当前用户社交绑定情况的方法:

      @RequestMapping(
      method = {RequestMethod.GET}
  )
  public String connectionStatus(NativeWebRequest request, Model model) {
      this.setNoCache(request);
      this.processFlash(request, model);
      Map<String, List<Connection<?>>> connections = this.connectionRepository.findAllConnections();
      model.addAttribute("providerIds", this.connectionFactoryLocator.registeredProviderIds());
      model.addAttribute("connectionMap", connections);
      return this.connectView();
  }

  @RequestMapping(
      value = {"/{providerId}"},
      method = {RequestMethod.GET}
  )
  public String connectionStatus(@PathVariable String providerId, NativeWebRequest request, Model model) {
      this.setNoCache(request);
      this.processFlash(request, model);
      List<Connection<?>> connections = this.connectionRepository.findConnections(providerId);
      this.setNoCache(request);
      if(connections.isEmpty()) {
          return this.connectView(providerId);
      } else {
          model.addAttribute("connections", connections);
          return this.connectedView(providerId);
      }
  }

对了,就是这两个方法,前面第一个方法请求的地址是:/connect(需要用户登录) 这个地址是获取当前用户所有社交账号绑定情况,第二个方法请求的地址是:/connect/{providerId}(需要用户登录) 这个地址是获取某个社交账号绑定情况,如/connect/qq,所以我们要获取当前用户绑定的所有社交账号绑定情况,使用的是第一个方法,但是现在有个问题,获取完之后 它是直接跳转页面到/connect/status,当然这不是我们想要的,我们要修改这个类,比如地址换成/socialConnect,这个换成自己的就好,然后我们来改下这个方法,如下:

 @RequestMapping(
         method = {RequestMethod.GET}
 )
 public ResponseEntity<?> connectionStatus(NativeWebRequest request, Model model) throws JsonProcessingException {
     this.setNoCache(request);
     this.processFlash(request, model);
     Map<String, List<Connection<?>>> connections = this.connectionRepository.findAllConnections();
     model.addAttribute("providerIds", this.connectionFactoryLocator.registeredProviderIds());
     model.addAttribute("connectionMap", connections);
     Map<String,Boolean> result = new HashMap<String, Boolean>();
     for (String key : connections.keySet()){
         result.put(key, org.apache.commons.collections.CollectionUtils.isNotEmpty(connections.get(key)));
     }
     return ResponseEntity.ok(objectMapper.writeValueAsString(result));
 }

改好的代码直接返回Json数据给前端,而不是跳转页面,完美解决了前后端分离项目问题,好了,我们使用postman发送请求测试看看:

clipboard.png

如图所示,我们成功获取当前登录用户所有社交账号绑定情况了(为什么这里只有qq和微信?社交账号的类型是你application.proterties里面配置的)。

4、绑定社交账号

好了,我们来看看绑定社交账号的方法:

  @RequestMapping(
      value = {"/{providerId}"},
      method = {RequestMethod.POST}
  )
  public RedirectView connect(@PathVariable String providerId, NativeWebRequest request) {
      ConnectionFactory<?> connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId);
      MultiValueMap<String, String> parameters = new LinkedMultiValueMap();
      this.preConnect(connectionFactory, parameters, request);

      try {
          return new RedirectView(this.connectSupport.buildOAuthUrl(connectionFactory, request, parameters));
      } catch (Exception var6) {
          this.sessionStrategy.setAttribute(request, "social_provider_error", var6);
          return this.connectionStatusRedirect(providerId, request);
      }
  }
  
  @RequestMapping(
      value = {"/{providerId}"},
      method = {RequestMethod.GET},
      params = {"code"}
  )
  public RedirectView oauth2Callback(@PathVariable String providerId, NativeWebRequest request) {
      try {
          OAuth2ConnectionFactory<?> connectionFactory = (OAuth2ConnectionFactory)this.connectionFactoryLocator.getConnectionFactory(providerId);
          Connection<?> connection = this.connectSupport.completeConnection(connectionFactory, request);
          this.addConnection(connection, connectionFactory, request);
      } catch (Exception var5) {
          this.sessionStrategy.setAttribute(request, "social_provider_error", var5);
          logger.warn("Exception while handling OAuth2 callback (" + var5.getMessage() + "). Redirecting to " + providerId + " connection status page.");
      }

      return this.connectionStatusRedirect(providerId, request);
  }

现在来分析 下这两个 方法的作用,第一个方法请求的地址是:POST /connect/{providerId}(需要登录) ,第二个方法请求地址是:GET /connect/{providerId}?code=&state=(需要登录),第一个方法是获取社交授权连接地址(这个是你自己社交登录时候封装好的,这里我不打算详细讲解,后面课程再放出来吧)比如qq的授权地址:https://graph.qq.com/oauth2.0…,这样当你授权成功之后就回调到了第二个方法里面,顺便把code和state原样返回过来,这一套绑定机制都是基于session的,下面我们来分析看下他是如何实现的。

我们以微信为例,首先我们发送一个POST请求/connect/wechat,因为你已经登录了,所以后台可以获取当前user是谁,然后就获取到请求的链接:https://open.weixin.qq.com/co…,最后就是跳转到这个链接上面去。这是第一个方法的作用,接下来我们分析第二个方法。

请求上面的链接之后就是跳转到微信扫码的页面,如下所示:

clipboard.png

扫完之后立马就跳到上面链接redirect_uri地址上面去,也就是现在的第二个方法上面,而且是带着state和code两个参数,这时候后台开始验证你回传过来的state值是不是匹配的,不匹配就报错并且跳转到出错页面,匹配的话就往下走,并且通过code获取SpringSecurity OAuth相关社交用户信息并保存到数据库中,这就是code和state的作用,验证和获取完之后就可以,这样你就绑定成功了,最后跳转到/connected/wechat页面了,这样就结束了绑定功能了。

那么我们前后端分离项目要使用这套机制,我们必须改一下他的源码了。

首先第一个方法,我们要把userId保存到以state的redis键值对中,也就是:{state:userId},然后以JSON的格式返回社交授权的链接给前台,这是第一个方法要修改的思路。

然后第二个方法,是社交授权链接返回回来的,因为前后端分离项目session就无法使用了,所以要获取用户信息必须通过上面redis保存的{state:userId},来获取用户id。再一个我们通过code获取社交用户信息,两个数据都获取了,这个时候我们就可以安心的把社交用户信息保存到数据库中(这里的通过state从redis中获取userId,其实也是一种验证state的方式,你想想可是呢!),最后就跳转到你想要的页面就好了,下面就是修改后的代码了,可以看看:

@RequestMapping(
            value = {"/{providerId}"},
            method = {RequestMethod.POST}
    )
    public ResponseEntity<?> connect(@PathVariable String providerId,
                                     NativeWebRequest request) {
        HttpServletRequest nativeRequest = request.getNativeRequest(HttpServletRequest.class);
        Principal user = nativeRequest.getUserPrincipal();
        ConnectionFactory<?> connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId);
        MultiValueMap<String, String> parameters = new LinkedMultiValueMap();
        this.preConnect(connectionFactory, parameters, request);
        try {
            String social_connect_url = this.connectSupport.buildOAuthUrl(connectionFactory, request, parameters);
            String state = (String) this.sessionStrategy.getAttribute(request, "oauth2State");
            this.sessionStrategy.removeAttribute(request, "oauth2State");
            //把userId以state为key的形式保存到redis中
            socialRedisHelper.saveStateUserId(state, user.getName());
            //返回社交链接地址
            return ResponseEntity.ok(social_connect_url);
        } catch (Exception var6) {
            this.sessionStrategy.setAttribute(request, "social_provider_error", var6);
            logger.info(var6.getMessage());
            return null;
        }
    }
    
    //辅助方法1
    protected String callbackUrl(NativeWebRequest request) {
        if (this.callbackUrl != null) {
            return this.callbackUrl;
        } else {
            HttpServletRequest nativeRequest = request.getNativeRequest(HttpServletRequest.class);
            return this.applicationUrl != null ? this.applicationUrl + this.connectPath(nativeRequest) : nativeRequest.getRequestURL().toString();
        }
    }

    //辅助方法2
    private String connectPath(HttpServletRequest request) {
        String pathInfo = request.getPathInfo();
        return request.getServletPath() + (pathInfo != null ? pathInfo : "");
    }

    //回调方法
    @RequestMapping(
            value = {"/{providerId}"},
            method = {RequestMethod.GET},
            params = {"code"}
    )
    public void oauth2Callback(@PathVariable String providerId,
                               NativeWebRequest request,
                               HttpServletResponse response) {
        try {
            //ConnectController是先保存在session里面,然后回调从session里面取出来校验
            //我现在是通过redis保存state 的 userId,这样就相当于校验了state
            String state = request.getParameter("state");
            String code = request.getParameter("code");
            OAuth2ConnectionFactory<?> connectionFactory = (OAuth2ConnectionFactory) this.connectionFactoryLocator.getConnectionFactory(providerId);
            AccessGrant accessGrant = connectionFactory.getOAuthOperations().exchangeForAccess(code, this.callbackUrl(request), null);
            Connection<?> connection = connectionFactory.createConnection(accessGrant);
            //从redis中获取userid
            String userId = socialRedisHelper.getStateUserId(state);
            //保存到数据库中
            jdbcConnectionRepository.createConnectionRepository(userId).addConnection(connection);
            //跳转页面到前台任何你想设置的地址
            response.sendRedirect(connectUrl);
        } catch (Exception ex) {
            logger.info(ex.getMessage());
        }
    }

这样你就完成了后台绑定相关工作,那么我把前端相关代码也放出来大家看下吧:

gotoBind(type){
            let url = `${this.$url}/socialConnect/${type}`;
            this.$post(url)
            .then(res=>{
                if(res.code == 0){
                    this.openWindow(res.data.redirect_uri)
                }
            })
        },
openWindow(url){
            let sf_H = 550;
            let sf_W = 720;
            var iTop = (window.screen.height-30 -sf_H)/2; //获得窗口的垂直位置;  
            var iLeft = (window.screen.width-10 -sf_W)/2; //获得窗口的水平位置;
            let s = window.open(url,"social_bind_form",'height='+sf_H+
            ', width='+sf_W+',top='+iTop+',left='+iLeft+'toolbar=no, menubar=no, scrollbars=no, status=no, location=yes, resizable=yes');
        },

上面是获取社交绑定地址并跳转,下面是回调成功之后关闭对话框并刷新的页面代码。

<template>
    <section>
        <!--社交绑定成功处理页面-->
    </section>
</template>
<script>
import {mapActions,mapState} from 'vuex'

export default {
    data(){
        return{
        }
    },
    created(){
        window.close(); 
        opener.location.reload();
    },
    methods:{
        
    }
}
</script>

我们来演示一下:

clipboard.png

clipboard.png

clipboard.png

clipboard.png

5、解绑社交账号

绑定社交账号已经成功了,现在我们来看一下如何解绑社交账号吧,我们先看下源码是如何实现的,如下

@RequestMapping(
        value = {"/{providerId}"},
        method = {RequestMethod.DELETE}
    )
    public RedirectView removeConnections(@PathVariable String providerId, NativeWebRequest request) {
        ConnectionFactory<?> connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId);
        this.preDisconnect(connectionFactory, request);
        this.connectionRepository.removeConnections(providerId);
        this.postDisconnect(connectionFactory, request);
        return this.connectionStatusRedirect(providerId, request);
    }

    @RequestMapping(
        value = {"/{providerId}/{providerUserId}"},
        method = {RequestMethod.DELETE}
    )
    public RedirectView removeConnection(@PathVariable String providerId, @PathVariable String providerUserId, NativeWebRequest request) {
        ConnectionFactory<?> connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId);
        this.preDisconnect(connectionFactory, request);
        this.connectionRepository.removeConnection(new ConnectionKey(providerId, providerUserId));
        this.postDisconnect(connectionFactory, request);
        return this.connectionStatusRedirect(providerId, request);
    }

第一个方法请求地址是:Delete /connect/{providerId}(需登录),第二个方法请求地址是:Delete /connect/{providerId}/{providerUserId}(需登录),注意这里的providerUserId其实就是社交用户id,比如微信的openId,第一个方法是根据登录的userId和providerId来删除数据库中绑定的社交用户数据,第二个方法是根据登录的userId和providerId还有providerUserId来删除数据库中绑定的社交用户数据,这两个 方法都有相同的一点就是跳转到删除之后的页面,所以我们只要把跳转页面以JSON的形式返回给前端就好,下面就是修改后的代码:

 @RequestMapping(
            value = {"/{providerId}"},
            method = {RequestMethod.DELETE}
    )
    public ResponseEntity<?> removeConnections(@PathVariable String providerId, NativeWebRequest request) {
        ConnectionFactory<?> connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId);
        this.preDisconnect(connectionFactory, request);
        this.connectionRepository.removeConnections(providerId);
        this.postDisconnect(connectionFactory, request);
        return ResponseEntity.ok("success");
    }

    @RequestMapping(
            value = {"/{providerId}/{providerUserId}"},
            method = {RequestMethod.DELETE}
    )
    public ResponseEntity<?> removeConnection(@PathVariable String providerId,
                                              @PathVariable String providerUserId,
                                              NativeWebRequest request) throws IOException {
        try {
            ConnectionFactory<?> connectionFactory = this.connectionFactoryLocator.getConnectionFactory(providerId);
            this.preDisconnect(connectionFactory, request);
            this.connectionRepository.removeConnection(new ConnectionKey(providerId, providerUserId));
            this.postDisconnect(connectionFactory, request);
        } catch (Exception ex) {
            logger.info(ex.getMessage());
        }
        return ResponseEntity.ok("success");
    }

我们再把前端代码贴出来:

gotoUnBind(type){
            let url = `${this.$url}/socialConnect/${type}`;
            this.$delete(url)
            .then(res=>{
                if(res.code == 0){
                    this.$Message.success('解绑成功!')
                    location.reload();
                }
            })
        },

6、总结:

1、只要把思路理清楚了,其实修改成自己想要的代码就不难
2、注意ConnectController代码是基于Session的,所以你必须要登录的情况下才能使用
3、redis的使用在这里发挥到了一定作用,所以说前后端分离项目离不开redis

如果你觉得帮助到你了,可以打赏我更有动力来更新文章

clipboard.png

7、引用

qq互联文档
Spring Security Oauth2.0 实现短信验证码登录
深入了解 Spring Security
SpringBoot+Spring Security基本配置
spring-social-mongodb
微信的redirect_uri参数错误解决办法
网页微信第三方登录-redirect_uri参数错误
Java实现QQ、微信、新浪微博第三方登录
如何从零开始对接第三方登录(Java版):QQ登录和微博登录
QQ授权登录改
Spring Security QQ 登陆
第三方APP实现QQ登陆
2 Apache Shiro 身份认证(登录)
Spring Security 实战:QQ登录实现
基于Spring的QQ第三方登录实现
Spring Security源码分析三:Spring Social实现QQ社交登录
微信授权登录-前后端分离
从零开始的Spring Security Oauth2(一)
Spring Boot and OAuth2
Spring security OAuth2 深入解析
spring boot 入门之security oauth2 jwt完美整合例子-java编程
jojozhai/security
window.open打开的窗口关闭后刷新父页面的子页面
Spring Security 实战:QQ登录实现jwt

原文地址:https://segmentfault.com/a/1190000016542131