文章目录
项目源码地址 https://github.com/nieandsun/security
1 /signup错误
当解决了/signin错误后,我们就已经从QQ获取到了用户信息,即Connection对象,而SpringSocial/SpringSecurity又将其进一步封装成了一个SocialAuthenticationToken对象进行后续的校验工作—》可以参看上篇文章的2.3.1和2.2中的attemptAuthService方法。
attemptAuthService方法实际上定义了真正认证校验的整体框架,本篇文章将从该方法继续来解读SpringSocial/SpringSecurity的源码,并一探/signup错误的神秘面纱。
- attemptAuthService — 定义了真正认证校验的整体框架
private Authentication attemptAuthService(final SocialAuthenticationService<?> authService, final HttpServletRequest request, HttpServletResponse response)
throws SocialAuthenticationRedirectException, AuthenticationException {
//这里的token其实是用从QQ获取的用户信息(被封装到一个Connection对象里)进一步封装后得到的未被校验的对象
//类比一下,在用户名+密码登陆时也会有一个未校验的token
final SocialAuthenticationToken token = authService.getAuthToken(request, response);
if (token == null) return null;
Assert.notNull(token.getConnection());
//即Authentication auth=SecurityContextHolder.getContext().getAuthentication();
//即先去SecurityContextHolder里看看有没有Authentication对象
Authentication auth = getAuthentication();
if (auth == null || !auth.isAuthenticated()) {
//进行密码是否过期,用户是否可用等校验,如校验成功,返回一个标识为校验成功的Authentication对象
return doAuthentication(authService, request, token);
} else {
//暂时不进行追究
addConnection(authService, request, token, auth);
return null;
}
}
在获取到了封装了QQ用户信息的SocialAuthenticationToken对象后会走doAuthentication(authService, request, token);
方法进行进一步的校验工作。该方法的具体实现如下:
private Authentication doAuthentication(SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token) {
try {
if (!authService.getConnectionCardinality().isAuthenticatePossible()) return null;
token.setDetails(authenticationDetailsSource.buildDetails(request));
//下面这句话在<<spring-security入门6---表单登陆认证原理源码解析>>那篇文章里详细地介绍过,有兴趣的可以看一下
//它的作用是循环遍历各个Provider,并根据token的类型找到相应的Provider进行下一步的校验工作
//在springsocial的认证过程中用到的是SocialAuthenticationProvider
Authentication success = getAuthenticationManager().authenticate(token);
Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), "unexpected principle type");
updateConnections(authService, token, success);
return success;
}
// 从下面源代码中的注释,其实可以很清楚地看到/signup错误发生的时机
//即当上面的代码出现BadCredentialsException异常时,SpringSocial/SpringSecurity会catch住该异常,
//并抛出一个重定向异常---》系统默认的重定向url就是XXX/signup(即注册),而我们未对/signup进行授权,
//所以最后就会出现一个/signup异常。
catch (BadCredentialsException e) {
// connection unknown, register new user?
if (signupUrl != null) {
// store ConnectionData in session and redirect to register page
sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
}
throw e;
}
}
再来看一下SocialAuthenticationProvider中的authenticate方法,看一下为什么会报出BadCredentialsException异常
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(SocialAuthenticationToken.class, authentication, "unsupported authentication type");
Assert.isTrue(!authentication.isAuthenticated(), "already authenticated");
SocialAuthenticationToken authToken = (SocialAuthenticationToken) authentication;
String providerId = authToken.getProviderId();
Connection<?> connection = authToken.getConnection();
//拿着Connection对象去userconnection表里去查相应的userId,由于我们的系统里暂时还没有一个用户,
//所以查到的userId肯定为null----》为null就会报出BadCredentialsException---》然后就会回到上面的类
//让你跳转到一个XXX/signup的url----》其实说白了就是引导用户进入注册用户的页面(这一块需要我们自己实现)
String userId = toUserId(connection);
if (userId == null) {
throw new BadCredentialsException("Unknown access token");
}
//当能从userconnection表里查到数据时会走下面的语句
//即拿着userId去我们的业务表里拿到用户信息
UserDetails userDetails = userDetailsService.loadUserByUserId(userId);
if (userDetails == null) {
throw new UsernameNotFoundException("Unknown connected account id");
}
//校验用户是否过期等,并将校验成功后的用户对象封装到一个标记为已经校验成功的Authentication对象中
return new SocialAuthenticationToken(connection, userDetails, authToken.getProviderAccountData(), getAuthorities(providerId, userDetails));
}
代码解读到这里,其实已经可以很清楚地知道为什么项目会报出XXX/signup的错误了。那解决该问题的思路自然也就有了,具体思路如下:
- 我们需要自己写一个注册页面
- 让signupUrl对应的url可以指向我们写的注册页
- 对signupUrl进行授权
- 开发用户注册相关的逻辑
2 /signup错误解决方式
2.1 先写一个默认的注册页面,提醒开发者设置自己的注册页
系统注册页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>注册</title>
</head>
<body>
<h2>标准注册页面</h2>
<h3>这是系统注册页面,请配置nrsc.security.browser.signUpUrl属性来设置自己的注册页</h3>
</body>
</html>
2.2指定默认的注册页面为系统注册页
(1)在BrowserProperties里指定默认注册页所在的路径
package com.nrsc.security.core.properties;
import lombok.Data;
/**
* @author : Sun Chuan
* @date : 2019/6/20 22:13
*/
@Data
public class BrowserProperties {
/**指定默认的注册页面*/
private String signUpUrl = "/defaultPage/default-signUp.html";
/**指定默认的登陆页面*/
private String loginPage = SecurityConstants.DEFAULT_LOGIN_PAGE_URL;
/**指定默认的处理成功与处理失败的方法*/
private LoginType loginType = LoginType.JSON;
/**记住我的时间3600秒即1小时*/
private int rememberMeSeconds = 3600;
}
当然我们可以通过在yml里进行配置来修改具体的注册页面地址,代码截图如下:
(2)指定SpringSocial/SpringSecurity跳向注册页面时的url为我们配置的url
/**
* 通过apply()该实例,可以将SocialAuthenticationFilter加入到spring-security的过滤器链
*/
@Bean
public SpringSocialConfigurer nrscSocialSecurityConfig() {
// 默认配置类,进行组件的组装
// 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
String filterProcessesUrl = nrscSecurityProperties.getSocial().getFilterProcessesUrl();
NrscSpringSocialConfigurer configurer = new NrscSpringSocialConfigurer(filterProcessesUrl);
//指定SpringSocial/SpringSecurity跳向注册页面时的url为我们配置的url
configurer.signupUrl(nrscSecurityProperties.getBrowser().getSignUpUrl());
return configurer;
}
2.3 对signupUrl进行授权
这里仅贴出部分代码,具体代码可去github下载并查看commit记录
.authorizeRequests()
.antMatchers(
SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
nrscSecurityProperties.getBrowser().getLoginPage(),
SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*",
nrscSecurityProperties.getBrowser().getSignUpUrl() //对注册url进行授权
)
3 开发注册相关的逻辑
@PostMapping("/register")
public void register(NrscUser user) {
//注册用户相关逻辑
}
4 测试
4.1 未配置nrsc.security.browser.signUpUrl属性时
扫码进行授权后会跳转到如下页面,说明开发的代码已经生效。
4.2 配置了signUpUrl时
配置效果2.2(1)中的插图所示,nrsc-signUp.html的源码如下:
<!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="register">注册</button>
<button type="submit" name="type" value="binding">绑定</button>
</td>
</tr>
</table>
</form>
</body>
</html>
此时扫码进行授权后会跳转到如下页面,说明配置已经生效。
5 未完待续
当然这样并没有完,当SpringSocial/SpringSecurity将我们指向了注册页面,我们接下来肯定要开发关于注册的逻辑,但是,这里的注册逻辑肯定不应该仅简简单单的为我们平时见到的那种单纯的注册新用户的逻辑。为什么???
其实很好理解,因为这是从第三方登陆过程中重定向而来的注册,当用户注册完,肯定要与该第三方(即QQ、微信等)建立起关系,也就是说下次用户直接用QQ或微信等进行登陆,那肯定直接就可以登陆了。—》 落实到代码上,就是用户注册完,需要同时往userconnection表里插入一条数据
。
鉴于加班过于劳累,具体功能实现,下篇继续!!!