重构社交登录
app里面的第三方登录不向浏览器中一样,一般是通过调用sdk,引导到第三方app应用登录后返回;
浏览器模式
可能以下两种模式;
简化模式
上图来看,拿到openId之后,只要我们支持使用openid登录,即可;
可以大部分模仿短信验证码登录的代码,只有一点不同,提交的openid是属于social表中的数据,
所以相关的用户信息SocialUserDetailsService和用户连接信息UsersConnectionRepository
需要通过 socaial提供的表来获取校验逻辑
先看配置
package cn.mrcode.imooc.springsecurity.securityapp.social.openid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;
/**
* ${desc}
* @author zhuqiang
* @version 1.0.1 2018/8/8 15:59
* @date 2018/8/8 15:59
* @since 1.0
*/
@Component
public class OpenIdAuthenticationSecurityConfig
extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private SocialUserDetailsService userDetailsService;
@Autowired
private UsersConnectionRepository usersConnectionRepository;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Override
public void configure(HttpSecurity builder) throws Exception {
OpenIdAuthenticationProvider provider = new OpenIdAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setUsersConnectionRepository(usersConnectionRepository);
OpenIdAuthenticationFilter filter = new OpenIdAuthenticationFilter();
// 获取manager的是在源码中看到过
filter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
filter.setAuthenticationFailureHandler(authenticationFailureHandler);
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
// 需要一个服务提供商 和 一个过滤器
builder.
authenticationProvider(provider)
.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
}
}
服务商
package cn.mrcode.imooc.springsecurity.securityapp.social.openid;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.security.SocialUserDetailsService;
import java.util.HashSet;
import java.util.Set;
/**
* ${desc}
* @author zhuqiang
* @version 1.0.1 2018/8/8 16:14
* @date 2018/8/8 16:14
* @since 1.0
*/
public class OpenIdAuthenticationProvider implements AuthenticationProvider {
// 要使用social的
private SocialUserDetailsService userDetailsService;
private UsersConnectionRepository usersConnectionRepository;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 这里和之前短信验证码登录 唯一不同的是
// 这里是使用社交登录的userDetailsService 和 usersConnectionRepository
// 因为只有社交信息里面才会存在相关信息
OpenIdAuthenticationToken authenticationToken = (OpenIdAuthenticationToken) authentication;
Set<String> providerUserIds = new HashSet<>();
providerUserIds.add((String) authenticationToken.getPrincipal());
Set<String> userIds = usersConnectionRepository.findUserIdsConnectedTo(authenticationToken.getProviderId(), providerUserIds);
if (CollectionUtils.isEmpty(userIds) || userIds.size() != 1) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
String userId = userIds.iterator().next();
UserDetails user = userDetailsService.loadUserByUserId(userId);
if (user == null) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
OpenIdAuthenticationToken authenticationResult = new OpenIdAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
return OpenIdAuthenticationToken.class.isAssignableFrom(authentication);
}
public SocialUserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(SocialUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public UsersConnectionRepository getUsersConnectionRepository() {
return usersConnectionRepository;
}
public void setUsersConnectionRepository(UsersConnectionRepository usersConnectionRepository) {
this.usersConnectionRepository = usersConnectionRepository;
}
}
过滤器
package cn.mrcode.imooc.springsecurity.securityapp.social.openid;
import cn.mrcode.imooc.springsecurity.securitycore.properties.QQProperties;
import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityConstants;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* ${desc}
* @author zhuqiang
* @version 1.0.1 2018/8/8 16:03
* @date 2018/8/8 16:03
* @since 1.0
*/
public class OpenIdAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
private String openIdParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_OPEN_ID;
// 服务提供商id,qq还是微信
/** @see QQProperties#providerId */
private String providerIdParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_PROVIDERID;
private boolean postOnly = true;
// ~ Constructors
// ===================================================================================================
public OpenIdAuthenticationFilter() {
super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_OPEN_ID, "POST"));
}
// ~ Methods
// ========================================================================================================
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String openId = obtainOpenId(request);
String providerId = obtainProviderId(request);
if (openId == null) {
openId = "";
}
if (providerId == null) {
providerId = "";
}
openId = openId.trim();
OpenIdAuthenticationToken authRequest = new OpenIdAuthenticationToken(openId, providerId);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainOpenId(HttpServletRequest request) {
return request.getParameter(openIdParameter);
}
private String obtainProviderId(HttpServletRequest request) {
return request.getParameter(providerIdParameter);
}
protected void setDetails(HttpServletRequest request,
OpenIdAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setOpenIdParameter(String openIdParameter) {
Assert.hasText(openIdParameter, "Username parameter must not be empty or null");
this.openIdParameter = openIdParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getOpenIdParameter() {
return openIdParameter;
}
}
token 实体
package cn.mrcode.imooc.springsecurity.securityapp.social.openid;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/**
* ${desc}
* @author zhuqiang
* @version 1.0.1 2018/8/8 16:11
* @date 2018/8/8 16:11
* @since 1.0
*/
public class OpenIdAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private final Object principal;
private String providerId;
// ~ Constructors
// ===================================================================================================
public OpenIdAuthenticationToken(Object principal, String providerId) {
super(null);
this.principal = principal;
this.providerId = providerId;
super.setAuthenticated(true); // must use super, as we override
}
public OpenIdAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
// ========================================================================================================
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return null;
}
public String getProviderId() {
return providerId;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
资源服务器增加安全配置
cn.mrcode.imooc.springsecurity.securityapp.MyResourcesServerConfig#configure
.and().apply(openIdAuthenticationSecurityConfig)
记得放行openid过滤器的拦截地址
测试
在数据库中imooc_userconnection找一个之前在网页qq授权登录的openid
POST /authentication/openid HTTP/1.1
Host: localhost:80
Authorization: Basic bXlpZDpteWlk
Content-Type: application/x-www-form-urlencoded
openId=81F03E50B76D6D829F5A4875941567A6&providerId=qq
授权码模式
这个模式。只需要app端把拿到的授权码转发给服务器即可获得授权码;
这里需要注意的是: 这里拿到code。最终返回来的不是qq的accessToken,而是我们自己服务器的accessToken;
上面简化模式是客户端能直接拿到qq的accessToken和openid,
这里授权码模式是客户端只能拿到 code。还需要服务器去走social获取用户信息的步骤,
这里获取到qq的accessToken和openid后。我们拿着客户端传递的我们自己的client信息和这里获取到的openid;
然后走oath2的 token生成逻辑。最后返回
测试思路:
- demo引用浏览器环境
- 使用浏览器登录后,得到code
- 使用工具发送post请求到token地址,带上code和client信息
org.springframework.social.security.provider.OAuth2AuthenticationService#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);
// 在这里打断点,使用浏览器模块访问qq登录后,会重定向到这里
// 然后把服务器关闭掉
// 浏览器中的地址就是带有code的
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;
}
}
注意:在关闭服务时,需要点击 idea 左下角的 方块停止,需要快速点击,方块点完就会出现骷髅头,要连续快速点击,否则可能会导致断点继续往下走一会儿
我们拿到浏览器中带code的地址,把服务切回app模块,然后在工具中带上client信息访问;
GET /auth/qq?code=ACEB8728F5DE5B32F9C995BEFEB3C065&state=3cb4d5c7-60c6-4e88-b2a3-f34c5a8b176c HTTP/1.1
Host: mrcode.cn
Authorization: Basic bXlpZDpteWlk
我这里成功获取到了token(控制台打印的),但是postman中返回的错误信息
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
我跟了源码,发现成功后跳转到了"/";但是为什么不是重定向而是异常? 这个有待跟踪源码了解下
自定义获取第三方用户信息成功后的逻辑处理
之前讲到可以获得过滤器更改自定义注册地址;这里过滤器里面也可以设置一个授权成功的自定义处理器
cn.mrcode.imooc.springsecurity.securitycore.social.MySpringSocialConfigurer
public class MySpringSocialConfigurer extends SpringSocialConfigurer {
@Override
protected <T> T postProcess(T object) {
// org.springframework.security.config.annotation.SecurityConfigurerAdapter.postProcess()
// 在SocialAuthenticationFilter中配置死的过滤器拦截地址
// 这样的方法可以更改拦截的前缀
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
// filter.setFilterProcessesUrl("/oaths");
filter.setAuthenticationSuccessHandler(); // 可以把处理器添加到这里
return (T) filter;
}
}
现在来改造下
定义处理器接口,让使用处来实现,我们使用注解来获取初始化的bean
package cn.mrcode.imooc.springsecurity.securitycore.social;
import org.springframework.social.security.SocialAuthenticationFilter;
/**
* @author zhailiang
*
*/
public interface SocialAuthenticationFilterPostProcessor {
void process(SocialAuthenticationFilter socialAuthenticationFilter);
}
编写配置文件,获取过滤器设置处理器
package cn.mrcode.imooc.springsecurity.securitycore.social;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;
/**
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/6 12:12
*/
public class MySpringSocialConfigurer extends SpringSocialConfigurer {
private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;
@Override
protected <T> T postProcess(T object) {
// org.springframework.security.config.annotation.SecurityConfigurerAdapter.postProcess()
// 在SocialAuthenticationFilter中配置死的过滤器拦截地址
// 这样的方法可以更改拦截的前缀
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
// filter.setFilterProcessesUrl("/oaths");
// filter.setAuthenticationSuccessHandler();
// 让使用处自己获取token成功的逻辑
if (socialAuthenticationFilterPostProcessor != null) {
// 在配置初始化的时候,把过滤器传递给使用方,让使用方把处理器注入
socialAuthenticationFilterPostProcessor.process(filter);
}
return (T) filter;
}
public SocialAuthenticationFilterPostProcessor getSocialAuthenticationFilterPostProcessor() {
return socialAuthenticationFilterPostProcessor;
}
public void setSocialAuthenticationFilterPostProcessor(SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor) {
this.socialAuthenticationFilterPostProcessor = socialAuthenticationFilterPostProcessor;
}
}
配置文件引用
cn.mrcode.imooc.springsecurity.securitycore.social.SpringSocialConfig
@Autowired(required = false)
private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;
@Bean
public SpringSocialConfigurer imoocSocialSecurityConfig() {
// 默认配置类,进行组件的组装
// 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
MySpringSocialConfigurer springSocialConfigurer = new MySpringSocialConfigurer();
springSocialConfigurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
// 通过注解获取到使用方注入的bean,给我们刚才写的配置类
springSocialConfigurer.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
return springSocialConfigurer;
}
app实现该配置,配置处理成功的处理器
/**
*
*/
package cn.mrcode.imooc.springsecurity.securityapp.social.impl;
import cn.mrcode.imooc.springsecurity.securitycore.social.SocialAuthenticationFilterPostProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.stereotype.Component;
/**
* @author zhailiang
*/
@Component
public class AppSocialAuthenticationFilterPostProcessor implements SocialAuthenticationFilterPostProcessor {
@Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
/**
* @see cn.mrcode.imooc.springsecurity.securitycore.social.SocialAuthenticationFilterPostProcessor.process
*/
@Override
public void process(SocialAuthenticationFilter socialAuthenticationFilter) {
// 这里设置的其实就是之前 重构用户名密码登录里面实现的 MyAuthenticationSuccessHandler
socialAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
}
}
最后再次测试,得到了我们自己系统的 accessToken