Spring Security源碼分析十四:Spring Social社交登陸綁定與解綁

社交登陸又稱做社會化登陸(Social Login),是指網站的用戶可使用騰訊QQ、人人網、開心網、新浪微博、搜狐微博、騰訊微博、淘寶、豆瓣、MSN、Google等社會化媒體帳號登陸該網站。html

前言

在以前的Spring Social系列中,咱們只是實現了使用服務提供商帳號登陸到業務系統中,但沒有與業務系統中的帳號進行關聯。本章承接以前社交系列來實現社交帳號與業務系統帳號的綁定與解綁。java

  1. Spring-Security源碼分析三-Spring-Social社交登陸過程
  2. Spring-Security源碼分析四-Spring-Social社交登陸過程
  3. 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表,下面咱們來簡單分析一下git

  1. userId業務系統的用戶惟一標識(咱們使用的是username
  2. providerId用於區分不一樣的服務提供商(qq,weixin,weibo
  3. providerUserId 服務提供商返回的惟一標識(openid

社交登陸註冊實現

取消MyConnectionSignUp

Spring-Security源碼分析六-Spring-Social社交登陸源碼解析中,咱們得知,當配置ConnectionSignUp時,Spring Social會根據咱們配置的MyConnectionSignUp返回userId,接着執行userDetailsService.loadUserByUserId(userId),實現社交帳號登陸。當取消掉MyConnectionSignUp則會拋出BadCredentialsExceptionBadCredentialsExceptionSocialAuthenticationFilter處理,跳轉到默認的/signup註冊請求,跳轉以前會將當前的社交帳號信息保存到session中。github

添加自定義註冊請求/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中獲取社交帳號信息sql

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

建立SocialUserInfo

展現當前社交帳號信息json

@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;
    }
複製代碼

效果以下: 註冊效果以下: 微信

https://user-gold-cdn.xitu.io/2018/2/2/161572d98b582698?w=1190&h=660&f=gif&s=5001375
https://user-gold-cdn.xitu.io/2018/2/2/161572d98b582698?w=1190&h=660&f=gif&s=5001375

綁定與解綁實現

要實現綁定與解綁,首先咱們須要知道社交帳號的綁定狀態,綁定就是從新走一下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

實現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://user-gold-cdn.xitu.io/2018/2/2/1615728d4c38d43f?w=645&h=375&f=png&s=15827
https://user-gold-cdn.xitu.io/2018/2/2/1615728d4c38d43f?w=645&h=375&f=png&s=15827

綁定的實現

/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);
    }
}
複製代碼

效果以下:

https://user-gold-cdn.xitu.io/2018/2/2/161572a94fd0de16?w=1190&h=660&f=gif&s=3504379
https://user-gold-cdn.xitu.io/2018/2/2/161572a94fd0de16?w=1190&h=660&f=gif&s=3504379

解綁的實現

/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();
    }
複製代碼

效果以下:

https://user-gold-cdn.xitu.io/2018/2/2/161572c42c9c00e8?w=1190&h=660&f=gif&s=2612585
https://user-gold-cdn.xitu.io/2018/2/2/161572c42c9c00e8?w=1190&h=660&f=gif&s=2612585

代碼下載

從個人 github 中下載,github.com/longfeizhen…

相關文章
相關標籤/搜索