注册成为QQ开发者并且本地测试的博客地址(传送门)
如果没有域名的话可以使用NAT来内网穿透
需要自己将需要的属性添加到自己的properties类中去
/**
* @author lwj
*/
@Data
public class QQProperties {
/**
* Application id.
*/
private String appId;
/**
* Application secret.
*/
private String appSecret;
private String providerId = "qq";
}
== 由于在spring-boot-autoconfigure-2.0.4.RELEASE.jar没有对 social的自动配置了 ==
@Configuration
@ConditionalOnProperty(prefix = "moss.security.social.qq", name = "app-id")
public class QQAutoConfig extends SocialConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Override
public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
connectionFactoryConfigurer.addConnectionFactory(createConnectionFactory());
}
protected ConnectionFactory<?> createConnectionFactory() {
QQProperties qqProperties = securityProperties.getSocial().getQq();
return new QQConnectionFactory(qqProperties.getProviderId(), qqProperties.getAppId(), qqProperties.getAppSecret());
}
// 后补:做到处理注册逻辑的时候发现的一个bug:登录完成后,数据库没有数据,但是再次登录却不用注册了
// 就怀疑是否是在内存中存储了。结果果然发现这里父类的内存ConnectionRepository覆盖了SocialConfig中配置的jdbcConnectionRepository
// 这里需要返回null,否则会返回内存的 ConnectionRepository
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
return null;
}
}
redirect url 即是在QQ互联平台注册的网站回调域
请求链的图解
在配置好后,点击QQ登录之后,在此页面上点击账号授权后,发现页面被重定向一个signin的路由,说明并未登录成功。
登录失败
页面回到需要登陆的路由下
此时发现是在拿到授权码之后交换令牌的时候出现了错误
从报错信息上可以看到这是应为无法处理[text/html]这种格式的响应,可以看到OAuth2Template中并不存在处理[text/html]这种类型的响应的converter
这里我们需要去写一个自定义的template来匹配这个返回结果的解析
/**
* 自定义的template类
*
* @author lwj
*/
public class QQOAuth2Template extends OAuth2Template {
public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
}
/**
* 添加处理【text/html】类型的响应的Converter,{@link StringHttpMessageConverter}
* @return
*/
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
}
修改配置项中的template实例化对象
在获取令牌的方法中可以看到该方法将返回数据解析成一个map
然后在通过map来获取对应的值
但是,查看文档后发现,返回值为字符串,所以我们需要自己去解析这个字符串,然后再调用
在QQOAuth2Template类中添加下列方法
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
String responseStr = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
log.info("获取accessToken的响应:" + responseStr);
String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseStr, "&");
String accessToken = StringUtils.substringAfterLast(items[0], "=");
Long expiresIn = Long.valueOf(StringUtils.substringAfterLast(items[1], "="));
String refreshToken = StringUtils.substringAfterLast(items[2], "=");
return new AccessGrant(accessToken, null, refreshToken, expiresIn);
}
截止到以上的代码已经能够出现QQ登陆的界面,但是在登陆后发现还是跳转到了需要登陆的界面。
在查看控制台打印的日志看到跳转的路由是signUp,
该路由是在SocialAuthenticationFilter.class中定义,因为当SocialAuthenticationProvider中出现异常后,都会被该类收集并处理。
在查看SocialAuthenticationProvider中的代码可以发现,此处是由于去查询数据库中的moss_UserConnection表,但是目前并没有数据。userId为null时会抛出异常,被重定向到登陆的url中,如果项目中未添加注册页面则会报404错误。
在SocialConfig类中添加如下配置
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
return new ProviderSignInUtils(connectionFactoryLocator, getUsersConnectionRepository(connectionFactoryLocator)) {
};
}
在BrowserProperties类中添加默认注册页面的路径
/** 默认注册页 */
private String signUpUrl = "/signUp.html";
在browser项目中添加signUp.html
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册title>
head>
<body>
<h2>标准注册界面h2>
<h3>这是系统注册页面,请配置h3>
body>
html>
在demo项目中添加demo项目的注册页面demo-signUp
<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="banding">绑定button>
td>
tr>
table>
form>
body>
html>
在demo项目中的UserController中添加注册方法
@PostMapping("/regist")
public void regist(User user, HttpServletRequest request) {
// 不管是注册用户还是绑定用户,都会拿到用户地一个唯一标识(此处用用户的名称作为唯一标识)
String userId = user.getUsername();
providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
}
在上面的截图的空色框中的方法,我们可以发现当该方法中的 ConnectionSignUp类被实例化了之后,this.connectionSignUp.execute(connection)代码执行后会返回一个newUserId,并且下面的代码会将该id保存到数据库中,完成注册功能,那么我们只需要在项目中去实例化这个ConnectionSignUp类即可。
回到demo项目的security文件夹下新建DemoConnectionSignUp 类
package com.moss.securitydemo.security;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.stereotype.Component;
/**
* 自动注册接口实现
* 注释掉该@Component注解后需要通过注册页面来注册
* 该类中判断了根据是否配置了ConnectionSignUp类来决定是否自动注册
* {@link org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository}
*
* @author lwj
*/
@Component
public class DemoConnectionSignUp implements ConnectionSignUp {
/**
* 复写该方法用来指定唯一标识
* @param connection
* @return
*/
@Override
public String execute(Connection<?> connection) {
// 根据connection中的用户信息创建用户并返回唯一标识
return connection.getDisplayName();
}
}
微信登陆部分需要使用微信开放平台的开发者账号才可以做测试,暂不处理
该接口需要将ConnectController加入到Spring的bean中,需要去SocialConfig类中配置
@Bean
public ConnectController connectController(ConnectionFactoryLocator connectionFactoryLocator, ConnectionRepository connectionRepository) {
return new ConnectController(connectionFactoryLocator, connectionRepository);
}
查看ConnectController类的源码
执行后返回方法会返回一个视图,connect/status,那么就需要去配置一个视图来解析数据
@Component("connect/status")
public class MossConnectionStatusView extends AbstractView {
@Autowired
private ObjectMapper objectMapper;
@Override
protected void renderMergedOutputModel(Map<String, Object> map, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
Map<String, List<Connection<?>>> connections = (Map<String, List<Connection<?>>>) map.get("connectionMap");
Map<String, Boolean> result = new HashMap<>();
for (String key : connections.keySet()) {
result.put(key, CollectionUtils.isNotEmpty(connections.get(key)));
}
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
}
}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>绑定title>
head>
<body>
<h2>标准绑定界面h2>
<form action="/connect/qq" method="post">
<button type="submit">绑定QQbutton>
form>
body>
html>
在绑定成功后会返回一个视图,所以需要去配置一个绑定成功的视图,同样的在解绑的时候,同样也会返回一个视图
绑定成功:connect/qqConnected;解绑成功:connect/qqConnect
创建视图处理类
public class MossConnectView extends AbstractView {
@Override
protected void renderMergedOutputModel(Map<String, Object> map, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
httpServletResponse.setContentType("text/html;charset=UTF-8");
if (map.get("connection") == null) {
httpServletResponse.getWriter().write("绑定成功!
");
} else {
httpServletResponse.getWriter().write("解绑成功!
");
}
}
}
将视图处理类添加到QQAutoConfig中
/**
* 配置默认QQ绑定/解绑成功的返回页
* @return
*/
@Bean({"connect/qqConnect", "connect/qqConnected"})
@ConditionalOnMissingBean(name = "qqConnectedView")
public View qqConnectedView() {
return new MossConnectView();
}
当我们去添加测试绑定的时候会绑定的默认页面
发现,我们点击绑定了之后,页面出现如图错误,查看redirect uri发现是域名+/connect/qq,说明回调路由和当前的路由不匹配
我们需要去QQ互联平台上添加绑定的回调路由
至此,点击绑定后成功跳转到了QQ授权的页面,在点击了登陆授权后,页面跳转到
其实就是发送一个Method为DELETE的请求http://域名/connect/qq,同样也需要配置一个视图去解析,这个视图就是在上面的地方添加的
TomcatEmbeddedServletContainerFactory在Springboot 2.x中找不到,找到了另一个类TomcatServletWebServerFactory
设置了10秒的session超时时间
但是发现过了10秒并没有过期,调查该类中的设置session的方法可以看到
在BrowserSecurityConfig类中的配置安全规则部分添加session部分的配置,并将session过期的路由添加到默认可以访问的路由中
在BrowserSecurityController中添加处理session过期的url
/**
* 处理session过期
* @return
*/
@GetMapping("/session/invalid")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public SimpleResponse sessionInvalid() {
String message = "session失效";
return new SimpleResponse(message);
}
在上面的BrowserSecurityConfig中添加
.maximumSessions(1)
.expiredSessionStrategy(new MossExpiredSessionStrtegy())
在browser项目中创建session文件夹,并在文件夹下创建MossExpiredSessionStrtegy类
/**
* session并发处理
*
* @author lwj
*/
public class MossExpiredSessionStrtegy implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {
sessionInformationExpiredEvent.getResponse().setContentType("application/json;charset=utf-8");
sessionInformationExpiredEvent.getResponse().getWriter().write("session并发!");
}
}
在两个浏览器之间都登陆的情况下,第二个浏览器访问路由会报下面的情况说明配置完成
阻止第二个用户登陆
上面的配置在第二个用户登陆后,第一个用户继续访问会引发session并发,但是当如果我们需要阻止第二个用户登陆,来防止第一个用户的登陆被刷掉的话,那么我们需要在配置中增加一行配置
.maxSessionsPreventsLogin(true)
重构部分的代码暂不在此处延伸。
在application.yml中添加
spring:
session:
store-type: redis
特别注意:spring-session:1.3.3.RELEASE在高版本的spring boot autoconfig中已经不支持了;
需要分开引用下面的包
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-coreartifactId>
<version>2.1.2.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
<version>2.1.2.RELEASEversion>
dependency>
修改验证码部分的代码
public class ImageCode extends ValidateCode implements Serializable {
private static final long serialVersionUID = 8774439331692262265L;
修改AbstractValidateCodeProcessor类中将验证码保存到redis中的部分的代码
/**
* 保存校验码
*
* @param servletWebRequest
* @param validateCode
*/
private void save(ServletWebRequest servletWebRequest, C validateCode) {
// 不保存图片对象到redis session中,无法序列化
// 因为在验证的时候不需要图片对象
ValidateCode code = new ValidateCode(validateCode.getCode(), validateCode.getExpireTime());
sessionStrategy.setAttribute(servletWebRequest, getSessionKey(servletWebRequest), code);
}
在demo的Index.html中添加退出按钮
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Indextitle>
head>
<body>
主页
<a href="/signOut">退出登录a>
body>
html>
启动项目后,测试发现点击后,跳转到了需要再次登录的链接上,这是因为Spring默认退出后跳转的是登录url
图中的部分是配置了
logoutUrl:配置退出登录的url
logoutSuccessHandler:配置退出登录成功控制器
deleteCookies:配置退出登录成功后删除cookies
@Data
public class MossLogoutSuccessHandler implements LogoutSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
private String signOutUrl;
public MossLogoutSuccessHandler(String signOutUrl) {
this.signOutUrl = signOutUrl;
}
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
logger.info("退出成功");
if (StringUtils.isBlank(signOutUrl)) {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse("退出成功")));
} else {
httpServletResponse.sendRedirect(signOutUrl);
}
}
}
在BrowserSecurityBeanConfig中配置MossLogoutSuccessHandler类
/**
* 默认退出成功控制器
* @return
*/
@Bean
@ConditionalOnMissingBean(LogoutSuccessHandler.class)
public LogoutSuccessHandler logoutSuccessHandler() {
return new MossLogoutSuccessHandler(securityProperties.getBrowser().getSignOutUrl());
}
将退出登录成功后跳转的页面配置到core项目中的BrowserProperties中
在demo项目中的application.yml文件中增加退出登录后跳转的页面的配置
signOutUrl: /demo-logout.html
在demo项目下创建demo-logout.html
<html lang="en">
<head>
<meta charset="UTF-8">
<title>退出成功title>
head>
<body>
<h2>Demo退出成功h2>
body>
html>
在BrowserSecurityConfig类中添加退出成功跳转页的url
securityProperties.getBrowser().getSignOutUrl()