本篇文章我们讲解基于SpringBoot2.0,SpringSocial开发QQ微信登录。 请大家阅读了OAuth协议, SpringSocial基本原理后再来看本篇文章
我们开发QQ微信的授权登录主要就是为了完成对以下组件的实现。获取用户信息封装成Connection,如何构建Connection?必须要ConnectionFactory,ConnectionFactory需要ServiceProvider和ApiAdapter,ServiceProvider又需要我们去实现API和OAuth2Operations。
1.Api开发
1.1声明获取用户信息接口
Api主要是为了获取用户信息,因此我们第一步声明一个获取用户信息的接口。
public interface QQ {
QQUserInfo getUserInfo();
}
1.2开发接口实现类
接口实现类继承了SpringSocial中的AbstractOAuth2ApiBinding并实现了QQ接口。
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";
private String appId;
private String openId;
private ObjectMapper objectMapper = new ObjectMapper();
public QQImpl(String accessToken, String appId) {
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
this.appId = appId;
String url = String.format(URL_GET_OPENID, accessToken);
String result = getRestTemplate().getForObject(url, String.class);
this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
}
/* (non-Javadoc)
* @see com.imooc.security.core.social.qq.api.QQ#getUserInfo()
*/
@Override
public QQUserInfo getUserInfo() {
String url = String.format(URL_GET_USERINFO, appId, openId);
String result = getRestTemplate().getForObject(url, String.class);
System.out.println(result);
QQUserInfo userInfo = null;
try {
userInfo = objectMapper.readValue(result, QQUserInfo.class);
userInfo.setOpenId(openId);
return userInfo;
} catch (Exception e) {
throw new RuntimeException("获取用户信息失败", e);
}
}
}
1.2.1 AbstractOAuth2ApiBinding
再讲解这段代码之前,我们先看一下SpringSocial中AbstractOAuth2ApiBinding中的部分源码:
public abstract class AbstractOAuth2ApiBinding implements ApiBinding, InitializingBean {
private final String accessToken;
private RestTemplate restTemplate;
/**
* Constructs the API template without user authorization. This is useful for accessing operations on a provider's API that do not require user authorization.
*/
protected AbstractOAuth2ApiBinding() {
accessToken = null;
restTemplate = createRestTemplateWithCulledMessageConverters();
configureRestTemplate(restTemplate);
}
//省略几万字~~~
这个抽象类里面声明了一个accessToken和一个restTemplate,授权登录中,获取每一个QQ用户的openId和登录信息都需要一个token,而且每个人的token不可能都一样,因此我们的QQImpl不能够是单例的。restTemplate主要用于发起rest请求。
1.2.2 QQImpl成员变量及构造函数
回到我们的代码,我们要获取用户信息要走两个步骤,第一步首先根据token获取用户的openId,第二步,根据openId获取到用户的具体信息。因此我们声明了URL_GET_OPENID,这个是QQ提供的获取用户OPENID的URL,URL_GET_USERINFO也是QQ提供的获取用户具体信息的URL。appId是我们接入QQ时分配的ID,openId存储了获取到的用户openId。构造函数中,我们调用了父类的构造方法,最主要的是第二个参数TokenStrategy.ACCESS_TOKEN_PARAMETER,表示我们去获取openID的时候,token是放在请求参数中的,父类默认是放在请求头里面的,所以我们这里必须这么写一下。然后还使用了父类的restTemplate去获取用户的openId。完成了第一步初始化。
1.2.3 QQImpl的gerUserInfo
这个方法使用openId和appId,发起rest请求从QQ那里获取到用户的具体信息,然后封装成QQUserInfo对象。
1.3QQUserInfo
这个就是把从QQ接收到的用户信息字段映射到的一个类中,具体的get和set方法就不写了。
public class QQUserInfo {
/**
* 返回码
*/
private String ret;
/**
* 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。
*/
private String msg;
/**
*
*/
private String openId;
/**
* 不知道什么东西,文档上没写,但是实际api返回里有。
*/
private String is_lost;
/**
* 省(直辖市)
*/
private String province;
/**
* 市(直辖市区)
*/
private String city;
/**
* 出生年月
*/
private String year;
/**
* 用户在QQ空间的昵称。
*/
private String nickname;
/**
* 大小为30×30像素的QQ空间头像URL。
*/
private String figureurl;
private String figureurl_type;
/**
* 大小为50×50像素的QQ空间头像URL。
*/
private String figureurl_1;
/**
* 大小为100×100像素的QQ空间头像URL。
*/
private String figureurl_2;
private String figureurl_qq;
/**
* 大小为40×40像素的QQ头像URL。
*/
private String figureurl_qq_1;
/**
* 大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100×100的头像,但40×40像素则是一定会有。
*/
private String figureurl_qq_2;
/**
* 性别。 如果获取不到则默认返回”男”
*/
private String gender;
/**
* 标识用户是否为黄钻用户(0:不是;1:是)。
*/
private String is_yellow_vip;
/**
* 标识用户是否为黄钻用户(0:不是;1:是)
*/
private String vip;
/**
* 黄钻等级
*/
private String yellow_vip_level;
/**
* 黄钻等级
*/
private String level;
/**
* 标识是否为年费黄钻用户(0:不是; 1:是)
*/
private String is_yellow_year_vip;
private String constellation;
}
OK,Api模块我们就开发完成了
2 OAuthOperations开发
2.1 QQOAuth2Template
为什么我们不直接使用OAuth2Template呢,而要自己去实现一个QQOAuth2Template?这是因为QQ返回回来的信息中是text/html格式的,OAuth2Template没有支持处理这种类型的请求,所以我们必须要自己手动定义一个
public class QQOAuth2Template extends OAuth2Template {
private Logger logger = LoggerFactory.getLogger(getClass());
public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
setUseParametersForClientAuthentication(true);
}
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
logger.info("获取accessToke的响应:"+responseStr);
String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
String accessToken = StringUtils.substringAfterLast(items[0], "=");
Long expiresIn = new Long(StringUtils.substringAfterLast(items[1], "="));
String refreshToken = StringUtils.substringAfterLast(items[2], "=");
return new AccessGrant(accessToken, null, refreshToken, expiresIn);
}
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
}
createRestTemplate方法添加了一个StringHttpMessageConverter,这样我们就可以成功的将服务提供商返回的信息转换成对应的对象了。
那为什么我们还要重写postForAccessGrant方法呢?我们会在后面讲解。
OAuthOperations开发就算开发完成了,那么我们相当于凑齐了ServiceProvider的两大组件了,我们可以做ServiceProvider的实现了。
3 ServiceProvider开发
ServiceProvider抛开API组件,主要是完成了OAuth中的步骤1-5,步骤1-5不清楚的同学可以看我之前写的文章。
3.1 QQServiceProvider
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {
private String appId;
private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
private static final String URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";
public QQServiceProvider(String appId, String appSecret) {
super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
this.appId = appId;
}
@Override
public QQ getApi(String accessToken) {
return new QQImpl(accessToken, appId);
}
}
QQServiceProvider继承了SpringSocial提供的AbstractOAuth2ServiceProvider。这是一个泛型的抽象类,泛型中需要我们提供获取用户信息的接口,那这个就是我们第一步中声明的QQ。
3.1.1 构造函数
调用了父类的构造方法,并且参数是我们自己声明的QQOAuth2Template,其中appId和appSecret是QQ分配给我们的,URL_AUTHORIZE表示我们引导用户跳转的授权页面,对应步骤1。URL_ACCESS_TOKEN对应步骤4,表示去获取token。getApi是ServiceProvider用来获取用户信息需要的方法。我们刚才说了每个用户的token是不一样的,而且token是有期限的。所以我们不能够直接把QQImpl声明为单例的,这里必须要new一个出来。
我们的ServiceProvider就开发完成了。
4 ApiAdapter
这个主要是用于适配作用的,试想,我们从QQ、微信、微博中获取到的UserInfo信息是五花八门的,但是我们的Connection想要的信息就那么多,如何适配呢?很简单直接继承SpringSocial提供的ApiAdapter
public class QQAdapter implements ApiAdapter<QQ> {
@Override
public boolean test(QQ api) {
return true;
}
@Override
public void setConnectionValues(QQ api, ConnectionValues values) {
QQUserInfo userInfo = api.getUserInfo();
values.setDisplayName(userInfo.getNickname());
values.setImageUrl(userInfo.getFigureurl_qq_1());
values.setProfileUrl(null);
values.setProviderUserId(userInfo.getOpenId());
}
@Override
public UserProfile fetchUserProfile(QQ api) {
// TODO Auto-generated method stub
return null;
}
@Override
public void updateStatus(QQ api, String message) {
//do noting
}
}
4.1 test方法
表示是否和QQ还能连接通畅,我们这里直接返回true
4.2 setConnectionValues方法
这里就是真正做适配的地方,我们把QQUserInfo,转换成了ConnectionValues。
当我们完成了适配和Serviceprovider后,我们就可以开始构造我们的ConnectionFactory了。
5 OAuth2ConnectionFactory
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
public QQConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
}
}
这里的providerId表示服务提供商的唯一标识。OAuth2ConnectionFactory的构造非常简单,然后SpringSocial就可以利用它创建Connection了,创建好后,就会使用UsersConnectionRepository来将Connection存储到DBUserConnection中去。UsersConnectionRepository已经由SpringSocail提供了一个JdbcUsersConnectionRepository了,所以我们只需要做一个配置即可。
6 配置SocialConfig
@Configuration
@EnableSocial
@ConditionalOnProperty(prefix = "imooc.security.social.qq", name = "app-id")
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private SecurityProperties securityProperties;
@Autowired(required = false)
private ConnectionSignUp connectionSignUp;
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
connectionFactoryLocator, Encryptors.noOpText());
repository.setTablePrefix("imooc_");
if(connectionSignUp != null) {
repository.setConnectionSignUp(connectionSignUp);
}
return repository;
}
@Bean
public SpringSocialConfigurer imoocSocialSecurityConfig() {
String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();
ImoocSpringSocialConfigurer configurer = new ImoocSpringSocialConfigurer(filterProcessesUrl);
configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
return configurer;
}
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
return new ProviderSignInUtils(connectionFactoryLocator,
getUsersConnectionRepository(connectionFactoryLocator)) {
};
}
@Override
public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer,
Environment environment) {
super.addConnectionFactories(connectionFactoryConfigurer, environment);
QQProperties qqConfig = securityProperties.getSocial().getQq();
WeixinProperties weixinConfig = securityProperties.getSocial().getWeixin();
connectionFactoryConfigurer.addConnectionFactory(
new QQConnectionFactory(qqConfig.getProviderId(), qqConfig.getAppId(), qqConfig.getAppSecret()));
connectionFactoryConfigurer.addConnectionFactory(new WeixinConnectionFactory(weixinConfig.getProviderId(),
weixinConfig.getAppId(), weixinConfig.getAppSecret()));
}
@Override
public UserIdSource getUserIdSource() {
return new AuthenticationNameUserIdSource();
}
}
6.1 成员变量解析
DataSource表示数据源,repository会使用到
connectionSignUp注册配置,详情请见下面的第9部分拓展部分
SecurityProperties,存放QQ和微信的配置,其中的成员变量包含了appId和appSecret
6.2 getUsersConnectionRepository
这里我们使用了SpringSocial默认提供的JdbcUsersConnectionRepository,第一个参数是dataSource,第二个参数connectionFactoryLocator可以帮我们选择对应的ConnectionFactory,我们这里只写了一个QQ登录,只有一个QQ的ConnectionFactory,但是下一篇文章中,我们写微信登录的时候,那么就会有多的ConnectionFactory了。Encryptors.noOpText()表示我们对数据不加密,这里我们只是为了演示使用,实际上我们的数据入库的时候,对于access_token这类敏感信息还是需要加密的。
6.3 addConnectionFactories
把我们的QQFactory和WeixinFactory加进去对于微信的实现我们下一篇文章讲,这里先卖个关子
6.4 getUserIdSource
这个也是SpringBoot2里面最恶心的地方了,在1.5的版本中是没有这个东西了,这个方法主要是被Filter调用的,我们就返回SpringSocial默认提供的实现就可以了。
6.5 建立表
create table UserConnection (userId varchar(255) not null,
providerId varchar(255) not null,
providerUserId varchar(255),
rank int not null,
displayName varchar(255),
profileUrl varchar(512),
imageUrl varchar(512),
accessToken varchar(512) not null,
secret varchar(512),
refreshToken varchar(512),
expireTime bigint,
primary key (userId, providerId, providerUserId));
create unique index UserConnectionRank on UserConnection(userId, providerId, rank);
这段SQL语句其实是Spring-social-core包提供的,直接使用就可以了。userId就是我们的业务系统userId,providerId表示服务提供商Id,providerUserId就是openId。在我们进行QQ登录的时候,我们会根据providerId和providerUserId拿到userId,然后通过userId拿到用户的具体业务信息。其实SpringSocial也提供了一个接口SocialUserDetailsService,帮助我们获取user的信息,所以我们也让MyUserDetailsService实现了SocialUserDetailsService接口:
@Component
public class MyUserDetailsService implements UserDetailsService, SocialUserDetailsService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private PasswordEncoder passwordEncoder;
/*
* (non-Javadoc)
*
* @see org.springframework.security.core.userdetails.UserDetailsService#
* loadUserByUsername(java.lang.String)
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("表单登录用户名:" + username);
return buildUser(username);
}
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
logger.info("设计登录用户Id:" + userId);
return buildUser(userId);
}
private SocialUserDetails buildUser(String userId) {
// 根据用户名查找用户信息
//根据查找到的用户信息判断用户是否被冻结
String password = passwordEncoder.encode("123456");
logger.info("数据库密码是:"+password);
return new SocialUser(userId, password,
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
6.6 SecurityProperties
@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
private ValidateCodeProperties code = new ValidateCodeProperties();
private SocialProperties social = new SocialProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
public ValidateCodeProperties getCode() {
return code;
}
public void setCode(ValidateCodeProperties code) {
this.code = code;
}
public SocialProperties getSocial() {
return social;
}
public void setSocial(SocialProperties social) {
this.social = social;
}
}
6.7 SocialProperties
public class SocialProperties {
private String filterProcessesUrl = "/auth";
private QQProperties qq = new QQProperties();
private WeixinProperties weixin = new WeixinProperties();
public QQProperties getQq() {
return qq;
}
public void setQq(QQProperties qq) {
this.qq = qq;
}
public String getFilterProcessesUrl() {
return filterProcessesUrl;
}
public void setFilterProcessesUrl(String filterProcessesUrl) {
this.filterProcessesUrl = filterProcessesUrl;
}
public WeixinProperties getWeixin() {
return weixin;
}
public void setWeixin(WeixinProperties weixin) {
this.weixin = weixin;
}
}
7 过滤器配置
我们把所有的组件都已经完成好了,接下来就是配置我们的过滤器了
7.1配置SpringSocialConfigurer
我们在配置6中有这么一段代码
@Bean
public SpringSocialConfigurer imoocSocialSecurityConfig() {
String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();
ImoocSpringSocialConfigurer configurer = new ImoocSpringSocialConfigurer(filterProcessesUrl);
configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
return configurer;
}
这里的ImoocSpringSocialConfigurer继承了SpringSocialConfigurer
public class ImoocSpringSocialConfigurer extends SpringSocialConfigurer {
private String filterProcessesUrl;
public ImoocSpringSocialConfigurer(String filterProcessesUrl) {
this.filterProcessesUrl = filterProcessesUrl;
}
@SuppressWarnings("unchecked")
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
return (T) filter;
}
}
继承的SpringSocialConfigurer其实生成了一个SocialAuthenticationFilter,而这个Filter默认只会拦截/auth开头的请求路径,为了满足我们的需求,所以我们这里使用了filter.setFilterProcessesUrl(filterProcessesUrl);来改变默认的配置。为什么是在setFilterProcessesUrl配置呢?因为SpringSocialConfigurer在把Filter放到过滤器链之前会调用一个postProcess方法,所以我们在调用父类的postProcess之后,我们就在这里设置processingUrl。除此之外,我们的请求跳转还涉及到providerId,这个providerId也是我们实现跳转和授权完成之后回调的关键。我们自定义Filter拦截/qqLogin/callback.do的请求会跳转到QQ登录,因此ConnectionProvider的providerId也必须配置成callback.do,filterProcessesUrl为qqLogin。
configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());是因为如果在DB中没有获取到userId则会跳转到默认的signUp页面,这里我们需要自定义一个跳转的注册路径。
7.2 加入配置
@Configuration
public class BrowserSecurityConfig extends AbstractChannelSecurityConfig {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Autowired
private ValidateCodeSecurityConfig validateCodeSecurityConfig;
@Autowired
private SpringSocialConfigurer imoocSocialSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
applyPasswordAuthenticationConfig(http);
http.apply(validateCodeSecurityConfig)
.and()
.apply(smsCodeAuthenticationSecurityConfig)
.and()
.apply(imoocSocialSecurityConfig)
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()
.antMatchers(
SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
securityProperties.getBrowser().getLoginPage(),
SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX+"/*",
securityProperties.getBrowser().getSignUpUrl(),
"/user/regist")
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
最主要的就是apply(imoocSocialSecurityConfig)配置
7.3 OAuth2AuthenticationService
其实这个类不是编写的代码,它是SocialAuthenticationFilter的关键service,在这里我们主要是简单讲讲它的原理。这个service中有一个getAuthToken方法:
public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
String code = request.getParameter("code");
if (!StringUtils.hasText(code)) {
OAuth2Parameters params = new OAuth2Parameters();
params.setRedirectUri(buildReturnToUrl(request));
setScope(request, params);
params.add("state", generateState(connectionFactory, request));
addCustomParameters(params);
throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
} else if (StringUtils.hasText(code)) {
try {
String returnToUrl = buildReturnToUrl(request);
AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
// TODO avoid API call if possible (auth using token would be fine)
Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
return new SocialAuthenticationToken(connection, null);
} catch (RestClientException e) {
logger.debug("failed to exchange for access", e);
return null;
}
} else {
return null;
}
}
因为第三步中QQ会返回授权码code,如果发现没有这个code,SpringSocial会认为是第一步,也就是要去授权认证,因此会作重定向。但是如果我们发现我们得到了Code,这个时候我们就会利用code去拿token信息。在这里拿token的时候,会使用到我们第二步中自己实现的template来转换对应的信息。
细心的读者会发现,我在讲解第二步的时候说postForAccessGrant被重写了,为什么会被重写呢?这是因为默认的OAuth2Template是这样解析QQ返回回来的参数的:
private AccessGrant extractAccessGrant(Map<String, Object> result) {
return createAccessGrant((String) result.get("access_token"), (String) result.get("scope"), (String) result.get("refresh_token"), getIntegerValue(result, "expires_in"), result);
}
但是QQ返回回来的并不是一个JSON串,而且字段也不是这样的,所以我们需要自己实现一个Template来专门解析这个返回的token信息。
为什么我们还要在QQOAuth2Template设置一个setUseParametersForClientAuthentication(true)呢?我们还是在默认的OAuth2Template中看答案:
public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri, MultiValueMap<String, String> additionalParameters) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
if (useParametersForClientAuthentication) {
params.set("client_id", clientId);
params.set("client_secret", clientSecret);
}
params.set("code", authorizationCode);
params.set("redirect_uri", redirectUri);
params.set("grant_type", "authorization_code");
if (additionalParameters != null) {
params.putAll(additionalParameters);
}
return postForAccessGrant(accessTokenUrl, params);
}
默认的useParametersForClientAuthentication为false,这样我们去获取token的时候,就不会带上appId和appSecret,所以我们这里必须要设置好!
在我们拿到Token之后,Filter会拿着这个Token选择一个Provider做验证,Provider会使用Repository到数据库中获取userId,如果获取不到,就会跳转到signUp页面,提示用户去注册。而且在跳转之前我们可以看到,session中已经存放了connection的信息,具体步骤如下:
基于此我们还需要设置一个注册页面,请见7.4章节
7.4 注册页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h2>Demo注册页</h2>
<form action="/user/regist" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2">
<button type="submit" name="type" value="regist">注册</button>
<button type="submit" name="type" value="binding">绑定</button>
</td>
</tr>
</table>
</form>
</body>
</html>
7.5 注册逻辑
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private ProviderSignInUtils providerSignInUtils;
@PostMapping("/regist")
public void regist(User user, HttpServletRequest request) {
//不管是注册用户还是绑定用户,都会拿到一个用户唯一标识。
String userId = user.getUsername();
providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
}
}
ProviderSignInUtils是SpringSocial提供的一个工具类,帮助我们把用户信息绑定到对应的账户中去。
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
return new ProviderSignInUtils(connectionFactoryLocator,
getUsersConnectionRepository(connectionFactoryLocator)) {
};
}
8 前端页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>社交登录</h3>
<a href="/qqLogin/callback.do">QQ登录</a>
<a href="/qqLogin/weixin">微信登录</a>
</body>
</html>
9 扩展
有时候我们发现在用第三方授权登录后,不需要用户在注册信息,那这个是怎么实现的呢?我们先看看SocialAuthenticationProvider源码
在toUserId中我们刚才找不到用户的时候直接跳转到了注册页面,原因是Repostiry在寻找userId的时候的逻辑是这样的:
如果找不到用户且connectionSignUp为空了,就会返回一个空的List,但是如果我们实现一个connectionSignUp,这里就可以根据我们的根据我们的逻辑把这个userId放到数据库中去,所以我们这里要自己实现一个connectionSignUp。
9.1 DemoConnectionSignUp
@Component
public class DemoConnectionSignUp implements ConnectionSignUp {
/* (non-Javadoc)
* @see org.springframework.social.connect.ConnectionSignUp#execute(org.springframework.social.connect.Connection)
*/
@Override
public String execute(Connection<?> connection) {
//根据社交用户信息默认创建用户并返回用户唯一标识
return connection.getDisplayName();
}
}
这样配置后,我们即使在数据库中没有该用户也能够拿到用户信息
10 实战
QQ授权登录我们就开发到这里,下一章节我们将大致讲解一下微信的登录配置流程。