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

29次阅读

共计 20237 个字符,预计需要花费 51 分钟才能阅读完成。

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.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、准备工作做好之后,现在我们开始分析社交绑定,其实 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 发送请求测试看看:

如图所示,我们成功获取当前登录用户所有社交账号绑定情况了 (为什么这里只有 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、解绑社交账号

正文完
 0