社交登陆又称做社会化登陆(Social Login),是指网站的用户可使用腾讯QQ、人人网、开心网、新浪微博、搜狐微博、腾讯微博、淘宝、豆瓣、MSN、Google等社会化媒体帐号登陆该网站。html
在以前的Spring Social
系列中,咱们只是实现了使用服务提供商帐号登陆到业务系统中,但没有与业务系统中的帐号进行关联。本章承接以前社交系列来实现社交帐号与业务系统帐号的绑定与解绑。java
create table UserConnection (
userId varchar(255) not null,
providerId varchar(255) not null,
providerUserId varchar(255),
......
primary key (userId, providerId, providerUserId));
create unique index UserConnectionRank on UserConnection(userId, providerId, rank);
复制代码
在使用社交登陆的时咱们建立的UserConnection表,下面咱们来简单分析一下git
userId
业务系统的用户惟一标识(咱们使用的是username
)providerId
用于区分不一样的服务提供商(qq
,weixin
,weibo
)providerUserId
服务提供商返回的惟一标识(openid
)在Spring-Security源码分析六-Spring-Social社交登陆源码解析中,咱们得知,当配置ConnectionSignUp
时,Spring Social
会根据咱们配置的MyConnectionSignUp
返回userId
,接着执行userDetailsService.loadUserByUserId(userId)
,实现社交帐号登陆。当取消掉MyConnectionSignUp
则会抛出BadCredentialsException,BadCredentialsException
由SocialAuthenticationFilter处理,跳转到默认的/signup
注册请求,跳转以前会将当前的社交帐号信息保存到session
中。github
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
filter.setSignupUrl("/socialRegister");
return (T) filter;
}
复制代码
.authorizeRequests().antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
......
"/socialRegister",//社交帐号注册和绑定页面
"/user/register",//处理社交注册请求
......
.permitAll()//以上的请求都不须要认证
复制代码
从Session中获取社交帐号信息sql
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator factoryLocator) {
return new ProviderSignInUtils(factoryLocator, getUsersConnectionRepository(factoryLocator));
}
复制代码
展现当前社交帐号信息json
@Data
public class SocialUserInfo {
private String providerId;
private String providerUserId;
private String nickname;
private String headImg;
}
复制代码
@GetMapping(value = "/socialRegister")
public ModelAndView socialRegister(HttpServletRequest request, Map<String, Object> map) {
SocialUserInfo userInfo = new SocialUserInfo();
Connection<?> connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
userInfo.setProviderId(connection.getKey().getProviderId());//哪个服务提供商
userInfo.setProviderUserId(connection.getKey().getProviderUserId());//openid
userInfo.setNickname(connection.getDisplayName());//名称
userInfo.setHeadImg(connection.getImageUrl());//显示头像
map.put("user", userInfo);
return new ModelAndView("socialRegister", map);
}
复制代码
@PostMapping("/user/register")
public String register(SysUser user, HttpServletRequest request, HttpServletResponse response) throws IOException {
String userId = user.getUsername();//获取用户名
SysUser result = sysUserService.findByUsername(userId);//根据用户名查询用户信息
if(result==null){
//若是为空则注册用户
sysUserService.save(user);
}
//将业务系统的用户与社交用户绑定
providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
//跳转到index
return "redirect:/index";
}
复制代码
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
SysUser user = repository.findByUsername(userId);//根据用户名查找用户
return user;
}
复制代码
要实现绑定与解绑,首先咱们须要知道社交帐号的绑定状态,绑定就是从新走一下OAuth2
流程,关联当前登陆用户,解绑就是删除UserConnection
表数据。Spring Social
默认在ConnectController
类上已经帮咱们实现了以上的需求。session
/connect
获取状态。app
@RequestMapping(method=RequestMethod.GET)
public String connectionStatus(NativeWebRequest request, Model model) {
setNoCache(request);
processFlash(request, model);
Map<String, List<Connection<?>>> connections = connectionRepository.findAllConnections();//根据userId查询UserConnection表
model.addAttribute("providerIds", connectionFactoryLocator.registeredProviderIds());//系统中已经注册的服务提供商
model.addAttribute("connectionMap", connections);
return connectView();//返回connectView()
}
protected String connectView() {
return getViewPath() + "status";//connect/status
}
复制代码
由以上可得,实现connect/status
视图便可得到社交帐号的绑定状态。ide
@Component("connect/status")
public class SocialConnectionStatusView extends AbstractView {
@Autowired
private ObjectMapper objectMapper;
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
Map<String, List<Connection<?>>> connections = (Map<String, List<Connection<?>>>) model.get("connectionMap");
Map<String, Boolean> result = new HashMap<>();
for (String key : connections.keySet()) {
result.put(key, CollectionUtils.isNotEmpty(connections.get(key)));
}
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(ResultUtil.success(result)));
}
}
复制代码
/connect/{providerId}
绑定社交帐号(POST
请求)
////跳转到受权的页面
@RequestMapping(value="/{providerId}", method=RequestMethod.POST)
public RedirectView connect(@PathVariable String providerId, NativeWebRequest request) {
ConnectionFactory<?> connectionFactory = connectionFactoryLocator.getConnectionFactory(providerId);
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<String, String>();
preConnect(connectionFactory, parameters, request);
try {
return new RedirectView(connectSupport.buildOAuthUrl(connectionFactory, request, parameters));
} catch (Exception e) {
sessionStrategy.setAttribute(request, PROVIDER_ERROR_ATTRIBUTE, e);
return connectionStatusRedirect(providerId, request);
}
}
复制代码
受权成功的回调地址
//将当前的登陆帐户与社交帐号绑定(写入到UserConnection表)
@RequestMapping(value="/{providerId}", method=RequestMethod.GET, params="code")
public RedirectView oauth2Callback(@PathVariable String providerId, NativeWebRequest request) {
try {
OAuth2ConnectionFactory<?> connectionFactory = (OAuth2ConnectionFactory<?>) connectionFactoryLocator.getConnectionFactory(providerId);
Connection<?> connection = connectSupport.completeConnection(connectionFactory, request);
addConnection(connection, connectionFactory, request);
} catch (Exception e) {
sessionStrategy.setAttribute(request, PROVIDER_ERROR_ATTRIBUTE, e);
logger.warn("Exception while handling OAuth2 callback (" + e.getMessage() + "). Redirecting to " + providerId +" connection status page.");
}
return connectionStatusRedirect(providerId, request);
}
//返回/connext/qqed视图
protected RedirectView connectionStatusRedirect(String providerId, NativeWebRequest request) {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
String path = "/connect/" + providerId + getPathExtension(servletRequest);
if (prependServletPath(servletRequest)) {
path = servletRequest.getServletPath() + path;
}
return new RedirectView(path, true);
}
复制代码
@Bean("connect/qqConnected")
public View qqConnectedView() {
return new SocialConnectView();
}
public class SocialConnectView extends AbstractView {
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
String msg = "";
response.setContentType("text/html;charset=UTF-8");
if (model.get("connections") == null) {
msg = "unBindingSuccess";
// response.getWriter().write("<h3>解绑成功</h3>");
} else {
msg = "bindingSuccess";
// response.getWriter().write("<h3>绑定成功</h3>");
}
response.sendRedirect("/message/" + msg);
}
}
复制代码
/connect/{providerId}
绑定社交帐号(DELETE
请求)
//删除UserConnection表数据,返回connect/qqConnect视图
@RequestMapping(value="/{providerId}", method=RequestMethod.DELETE)
public RedirectView removeConnections(@PathVariable String providerId, NativeWebRequest request) {
ConnectionFactory<?> connectionFactory = connectionFactoryLocator.getConnectionFactory(providerId);
preDisconnect(connectionFactory, request);
connectionRepository.removeConnections(providerId);
postDisconnect(connectionFactory, request);
return connectionStatusRedirect(providerId, request);
}
复制代码
/** * /connect/qq POST请求,绑定微信返回connect/qqConnected视图 * /connect/qq DELETE请求,解绑返回connect/qqConnect视图 * @return */
@Bean({"connect/qqConnect", "connect/qqConnected"})
@ConditionalOnMissingBean(name = "qqConnectedView")
public View qqConnectedView() {
return new SocialConnectView();
}
复制代码
从个人 github 中下载,github.com/longfeizhen…