前後端分離項目 — SpringSocial 綁定與解綁社交賬號如微信、QQ

1、準備工作
申請QQ、微信相關ClientId和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.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、準備工作做好之後,現在我們開始分析社交綁定,其實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 oauth1Callback(@PathVariable String providerId, NativeWebRequest request) {
        try {
            OAuth1ConnectionFactory<?> connectionFactory = (OAuth1ConnectionFactory)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 OAuth1 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=。

第一個方法是
...未完待續
5、解綁社交賬號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章