【Spring Security OAuth2笔记系列】- App认证框架- 重构社交登录

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mr_zhuqiang/article/details/82020856

重构社交登录

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生成逻辑。最后返回

测试思路:

  1. demo引用浏览器环境
  2. 使用浏览器登录后,得到code
  3. 使用工具发送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;
  }
}

我们拿到浏览器中带code的地址,把服务切回app模块,然后在工具中带上client信息访问;

GET /auth/qq?code=ACEB8728F5DE5B32F9C995BEFEB3C065&amp;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

猜你喜欢

转载自blog.csdn.net/mr_zhuqiang/article/details/82020856