⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://niocoder.com/2018/02/02/Spring-Security源码分析十四-Spring-Social绑定与解绑/ 「龙飞」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

社交登录又称作社会化登录(Social Login),是指网站的用户可以使用腾讯QQ、人人网、开心网、新浪微博、搜狐微博、腾讯微博、淘宝、豆瓣、MSN、Google等社会化媒体账号登录该网站。

1. 前言

在之前的Spring Social系列中,我们只是实现了使用服务提供商账号登录到业务系统中,但没有与业务系统中的账号进行关联。本章承接之前社交系列来实现社交账号与业务系统账号的绑定与解绑。

  1. Spring-Security源码分析三-Spring-Social社交登录过程
  2. Spring-Security源码分析四-Spring-Social社交登录过程
  3. Spring-Security源码分析六-Spring-Social社交登录源码解析

1.1 UserConnection

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表,下面我们来简单分析一下

  1. userId业务系统的用户唯一标识(我们使用的是username
  2. providerId用于区分不同的服务提供商(qq,weixin,weibo
  3. providerUserId 服务提供商返回的唯一标识(openid

1.2 社交登录注册实现

1.2.1 取消MyConnectionSignUp

Spring-Security源码分析六-Spring-Social社交登录源码解析中,我们得知,当配置ConnectionSignUp时,Spring Social会根据我们配置的MyConnectionSignUp返回userId,接着执行userDetailsService.loadUserByUserId(userId),实现社交账号登录。当取消掉MyConnectionSignUp则会抛出BadCredentialsExceptionBadCredentialsExceptionSocialAuthenticationFilter处理,跳转到默认的/signup注册请求,跳转之前会将当前的社交账号信息保存到session中。

1.2.1.1 添加自定义注册请求/socialRegister

@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
filter.setSignupUrl("/socialRegister");
return (T) filter;
}

1.2.1.2 添加到.permitAll();

.authorizeRequests().antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
......
"/socialRegister",//社交账号注册和绑定页面
"/user/register",//处理社交注册请求
......
.permitAll()//以上的请求都不需要认证

1.2.3 配置ProviderSignInUtils

从Session中获取社交账号信息

@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator factoryLocator) {
return new ProviderSignInUtils(factoryLocator, getUsersConnectionRepository(factoryLocator));
}

1.2.4 创建SocialUserInfo

展示当前社交账号信息

@Data
public class SocialUserInfo {

private String providerId;

private String providerUserId;

private String nickname;

private String headImg;

}

1.2.5 实现socialRegister和user/register

1.2.5.1 /socialRegister

@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);
}

1.2.5.2 /user/register

@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";
}

1.2.6 修改MyUserDetailsService#loadUserByUserId

@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
SysUser user = repository.findByUsername(userId);//根据用户名查找用户
return user;
}

效果如下: 注册效果如下: https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/security/spring-social-register.gif

1.3 绑定与解绑实现

要实现绑定与解绑,首先我们需要知道社交账号的绑定状态,绑定就是重新走一下OAuth2流程,关联当前登录用户,解绑就是删除UserConnection表数据。Spring Social默认在ConnectController类上已经帮我们实现了以上的需求。

1.3.1 获取状态

/connect获取状态。

@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视图即可获得社交账号的绑定状态。

1.3.1.1 实现connect/status

@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)));
}
}

返回结果如下: https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/security/spring-social-status.png

1.3.2 绑定的实现

/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);
}

1.3.2.1 实现 connect/qqConnected视图

    @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);
}
}

效果如下: https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/security/spring-social-banding01.gif

1.3.3 解绑的实现

/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);
}

1.3.3.1 实现connect/qqConnect视图

/**
* /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();
}

效果如下: https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/security/spring-social-banding02.gif

2. 代码下载

从我的 github 中下载,https://github.com/longfeizheng/logback

文章目录
  1. 1. 1. 前言
    1. 1.1. 1.1 UserConnection
    2. 1.2. 1.2 社交登录注册实现
      1. 1.2.1. 1.2.1 取消MyConnectionSignUp
        1. 1.2.1.1. 1.2.1.1 添加自定义注册请求/socialRegister
        2. 1.2.1.2. 1.2.1.2 添加到.permitAll();
      2. 1.2.2. 1.2.3 配置ProviderSignInUtils
      3. 1.2.3. 1.2.4 创建SocialUserInfo
      4. 1.2.4. 1.2.5 实现socialRegister和user/register
        1. 1.2.4.1. 1.2.5.1 /socialRegister
        2. 1.2.4.2. 1.2.5.2 /user/register
      5. 1.2.5. 1.2.6 修改MyUserDetailsService#loadUserByUserId
    3. 1.3. 1.3 绑定与解绑实现
      1. 1.3.1. 1.3.1 获取状态
        1. 1.3.1.1. 1.3.1.1 实现connect/status
      2. 1.3.2. 1.3.2 绑定的实现
        1. 1.3.2.1. 1.3.2.1 实现 connect/qqConnected视图
      3. 1.3.3. 1.3.3 解绑的实现
        1. 1.3.3.1. 1.3.3.1 实现connect/qqConnect视图
  2. 2. 2. 代码下载