社交登入又稱作社會化登入(Social Login),是指網站的使用者可以使用騰訊QQ、人人網、開心網、新浪微博、搜狐微博、騰訊微博、淘寶、豆瓣、MSN、Google等社會化媒體賬號登入該網站。
前言
在之前的Spring Social
系列中,我們只是實現了使用服務提供商賬號登入到業務系統中,但沒有與業務系統中的賬號進行關聯。本章承接之前社交系列來實現社交賬號與業務系統賬號的繫結與解綁。
- Spring-Security原始碼分析三-Spring-Social社交登入過程
- Spring-Security原始碼分析四-Spring-Social社交登入過程
- Spring-Security原始碼分析六-Spring-Social社交登入原始碼解析
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表,下面我們來簡單分析一下
userId
業務系統的使用者唯一標識(我們使用的是username
)providerId
用於區分不同的服務提供商(qq
,weixin
,weibo
)providerUserId
服務提供商返回的唯一標識(openid
)
社交登入註冊實現
取消MyConnectionSignUp
在Spring-Security原始碼分析六-Spring-Social社交登入原始碼解析中,我們得知,當配置ConnectionSignUp
時,Spring Social
會根據我們配置的MyConnectionSignUp
返回userId
,接著執行userDetailsService.loadUserByUserId(userId)
,實現社交賬號登入。當取消掉MyConnectionSignUp
則會丟擲BadCredentialsException,BadCredentialsException
由SocialAuthenticationFilter處理,跳轉到預設的/signup
註冊請求,跳轉之前會將當前的社交賬號資訊儲存到session
中。
新增自定義註冊請求/socialRegister
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
filter.setSignupUrl("/socialRegister");
return (T) filter;
}
複製程式碼
新增到.permitAll();
.authorizeRequests().antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
......
"/socialRegister",//社交賬號註冊和繫結頁面
"/user/register",//處理社交註冊請求
......
.permitAll()//以上的請求都不需要認證
複製程式碼
配置ProviderSignInUtils
從Session中獲取社交賬號資訊
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator factoryLocator) {
return new ProviderSignInUtils(factoryLocator, getUsersConnectionRepository(factoryLocator));
}
複製程式碼
建立SocialUserInfo
展示當前社交賬號資訊
@Data
public class SocialUserInfo {
private String providerId;
private String providerUserId;
private String nickname;
private String headImg;
}
複製程式碼
實現socialRegister和user/register
/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);
}
複製程式碼
/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";
}
複製程式碼
修改MyUserDetailsService#loadUserByUserId
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
SysUser user = repository.findByUsername(userId);//根據使用者名稱查詢使用者
return user;
}
複製程式碼
繫結與解綁實現
要實現繫結與解綁,首先我們需要知道社交賬號的繫結狀態,繫結就是重新走一下OAuth2
流程,關聯當前登入使用者,解綁就是刪除UserConnection
表資料。Spring Social
預設在ConnectController
類上已經幫我們實現了以上的需求。
獲取狀態
/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
檢視即可獲得社交賬號的繫結狀態。
實現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)));
}
}
複製程式碼
繫結的實現
/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);
}
複製程式碼
實現 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);
}
}
複製程式碼
解綁的實現
/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/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();
}
複製程式碼
程式碼下載
從我的 github 中下載,github.com/longfeizhen…