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.Driverspring.redis.host=127.0.0.1spring.redis.password=your_pwdspring.redis.port=6379spring.redis.timeout=30000ssb.security.social.register-url=/social/signUpssb.security.social.filter-processes-url=/social-loginssb.security.social.bind-url=https://website/social-bind/qqssb.security.social.callback-url=https://website/social-loginssb.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配置代码:@Configurationpublic 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、解绑社交账号