1. 程式人生 > >Spring Security OAuth2 第三方登入(一)之流程說明

Spring Security OAuth2 第三方登入(一)之流程說明

SpringBoot第三方登入流程

在這裡插入圖片描述

OAuth2AuthenticationService

該類執行獲取code,token,建立connection,由SocailAuthenticationFilter呼叫

public class OAuth2AuthenticationService<S> extends AbstractSocialAuthenticationService<S> {

public SocialAuthenticationToken getAuthToken(HttpServletRequest request,
HttpServletResponse response) throws SocialAuthenticationRedirectException { String code = request.getParameter("code"); if (!StringUtils.hasText(code)) { OAuth2Parameters params = new OAuth2Parameters(); //拼接redirectUrl params.setRedirectUri(this.buildReturnToUrl
(request)); this.setScope(request, params); params.add("state", this.generateState(this.connectionFactory, request)); this.addCustomParameters(params); throw new SocialAuthenticationRedirectException(this.getConnectionFactory().getOAuthOperations().buildAuthenticateUrl
(params)); } else if (StringUtils.hasText(code)) { try { String returnToUrl = this.buildReturnToUrl(request); //獲取token,並返回AccessGrant AccessGrant accessGrant = this.getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, (MultiValueMap)null); //建立connection Connection<S> connection = this.getConnectionFactory().createConnection(accessGrant); return new SocialAuthenticationToken(connection, (Map)null); } catch (RestClientException var7) { this.logger.debug("failed to exchange for access", var7); return null; } } else { return null; } } }

SocialConfigurerAdapter

為第三方登入新增一些元件到容器,比如SpringSocialConfigurer(只是新增到容器中,需在WebSecurityConfigurerAdapter應用才可生效),JdbcUsersConnectionRepository等

  • 新增Filter
    SpringSecurity在新增驗證時都是通過在其FilterChain上新增Filter來實現,第三方登入需要配置的是AutenticationFilter
    程式碼演示
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

    @Autowired
    DataSource dataSource;
    @Autowired
    SecurityProperties securityProperties;
    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
    }
    
    //配置將 SpringSocialConfigurer新增到容器中, 
    //SpringSocialConfigurer的構造方法會在FilterChain上新增AutenticationFilter
    //配置自定義SpringSocialConfigurer,需要在
    @Bean
    public SpringSocialConfigurer imoocSocialSecurityConfig(){
        String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();

        SpringSocialConfigurer springSocialConfigurer = new ImoocSpringSocialConfigurer(filterProcessesUrl);
        return springSocialConfigurer;
    }
}
  • SpringSocialConfigurer

該類為SpringSecurity filterchain上新增SocialAuthenticationFilter,需要在WebSecurityConfigurerAdapter上應用

public void configure(HttpSecurity http) throws Exception {
        ApplicationContext applicationContext = (ApplicationContext)http.getSharedObject(ApplicationContext.class);
        UsersConnectionRepository usersConnectionRepository = (UsersConnectionRepository)this.getDependency(applicationContext, UsersConnectionRepository.class);
        SocialAuthenticationServiceLocator authServiceLocator = (SocialAuthenticationServiceLocator)this.getDependency(applicationContext, SocialAuthenticationServiceLocator.class);
        SocialUserDetailsService socialUsersDetailsService = (SocialUserDetailsService)this.getDependency(applicationContext, SocialUserDetailsService.class);
        //filter
        SocialAuthenticationFilter filter = new SocialAuthenticationFilter((AuthenticationManager)http.getSharedObject(AuthenticationManager.class), (UserIdSource)(this.userIdSource != null ? this.userIdSource : new AuthenticationNameUserIdSource()), usersConnectionRepository, authServiceLocator);
//.......
http.authenticationProvider(new SocialAuthenticationProvider(usersConnectionRepository, socialUsersDetailsService))
//新增filter,可重寫**postProcess**方法定製filter
.addFilterBefore((Filter)this.postProcess(filter), AbstractPreAuthenticatedProcessingFilter.class);
}
  • WebSecurityConfigurerAdapter

配置SpringSecurity,包括新增filter,定義攔截路徑等

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        validetCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
        validetCodeFilter.setSecurityProperties(securityProperties);
        validetCodeFilter.afterPropertiesSet();
        log.info("authentication");

        http.addFilterBefore(validetCodeFilter, UsernamePasswordAuthenticationFilter.class)
            .formLogin()
            .loginPage("/authentication/requrie")
            .loginProcessingUrl("/authentication/form")
            .successHandler(imoocAuthenticationSuccessHandler)
            .failureHandler(imoocAuthenticationFailureHandler)
                .and()
                //**應用SpringSocialConfigurer**
            .apply(imoocSocialSecurityConfig)
                .and()
            .authorizeRequests()
            .antMatchers("/authentication/requrie","/code/image","/login",securityProperties.getBrowser().getLoginPage()).permitAll()
            .anyRequest()
            .authenticated()
                .and()
            .csrf().disable();
    }

  • 配置SocialConfigurerAdapter
    (可以配置多個,若不重寫getUsersConnectionRepository()方法會直接呼叫父類方法)
    SocialConfigurerAdapter為配置類,直接新增到容器中即可,可以為容器新增ConnectionFactory和UsersConnectionRepository等
@Configuration
@ConfigurationProperties(prefix = "imooc.sercurity.social.qq", value = "appId")
public class QQAutoConfig extends SocialConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;
    //自動配置
    @Override
    public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
        connectionFactoryConfigurer.addConnectionFactory(creatConnectionFactroy());
    }

    @Override
    public UserIdSource getUserIdSource() {
        return super.getUserIdSource();
    }
    //專案中返回為null,UsersConnectionRepository是配置在另一個SocialConfigurerAdapter中的,因為第三方登入可以為多個,而UsersConnectionRepository只需要一個
    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        return new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
    }

    private ConnectionFactory<?> creatConnectionFactroy(){
        QQProperties qq =securityProperties.getSocial().getQq();
        return new QQConnectionFactroy(qq.getProviderId(),qq.getAppId(),qq.getAppSecret());
    }
}

ConnectionFactroy(OAuth2ConnectionFactory<?>)
其父類構造方法引數:
String providerId 第三方應用id,由自己定義,最好設計為可配置的,請求URI與之有關
QQServiceProvidor(String, String) 其兩個引數為申請的appId,和appSecret
QQAdapter api介面卡, 將第三方返回的資料轉為springSecurity規範的資料,然後通過JdbcUsersConnectionRepository存入資料庫

public class QQConnectionFactroy extends OAuth2ConnectionFactory<QQ> {

    public QQConnectionFactroy(String providerId, String appId, String appSecret) {
        super(providerId, new QQServiceProvidor(appId, appSecret), new QQAdapter());
    }
}
  • QQServiceProvidor

包含兩個類(換句話說可以獲得兩個關鍵類)
QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKNE))
QQImpl(accessToken, appId) api的實現

public class QQServiceProvidor extends AbstractOAuth2ServiceProvider<QQ> {
  //定義了獲取Code和Token的url
    private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
    private static final String URL_ACCESS_TOKNE ="https://graph.qq.com/oauth2.0/token";

    private String appId;

    public QQServiceProvidor(String appId, String appSecret) {
        super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKNE));
        this.appId = appId;
    }

    @Override
    public QQ getApi(String accessToken) {
        return new QQImpl(accessToken, appId);
    }


}
  • QQOAuth2Template()

用於完成OAuth2協議的獲取Code和token

public class OAuth2Template implements OAuth2Operations {
private final String clientId;
    private final String clientSecret;
    private final String accessTokenUrl;
    private final String authorizeUrl;
    private String authenticateUrl;
    //需要注意的是
    private RestTemplate restTemplate;
    private boolean useParametersForClientAuthentication;
public OAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
        this(clientId, clientSecret, authorizeUrl, (String)null, accessTokenUrl);
    }
//構造器中為clientId(appId),clientSecret,authorizeurl(獲取code的url),accessTokenUrl(獲取Token的url),均有ServiceProvider賦值
    public OAuth2Template(String clientId, String clientSecret, String authorizeUrl, String authenticateUrl, String accessTokenUrl) {
        Assert.notNull(clientId, "The clientId property cannot be null");
        Assert.notNull(clientSecret, "The clientSecret property cannot be null");
        Assert.notNull(authorizeUrl, "The authorizeUrl property cannot be null");
        Assert.notNull(accessTokenUrl, "The accessTokenUrl property cannot be null");
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        String clientInfo = "?client_id=" + this.formEncode(clientId);
        //拼接獲取code的url,為url新增encode後的appid引數,
        //若重寫方法沒有拼接需要重寫方法buildAuthenticateUrl()
        this.authorizeUrl = authorizeUrl + clientInfo;
        if (authenticateUrl != null) {
            this.authenticateUrl = authenticateUrl + clientInfo;
        } else {
            this.authenticateUrl = null;
        }

        this.accessTokenUrl = accessTokenUrl;
    }

//
public String buildAuthenticateUrl(OAuth2Parameters parameters) {
        return this.authenticateUrl != null ? this.buildAuthUrl(this.authenticateUrl, GrantType.AUTHORIZATION_CODE, parameters) : this.buildAuthorizeUrl(GrantType.AUTHORIZATION_CODE, parameters);
    }

protected RestTemplate createRestTemplate() {
        ClientHttpRequestFactory requestFactory = ClientHttpRequestFactorySelector.getRequestFactory();
        RestTemplate restTemplate = new RestTemplate(requestFactory);
        List<HttpMessageConverter<?>> converters = new ArrayList(2);
       //未新增StringHttpMessageConverter,不能處理text/html 
       //若需要處理則需要重寫該方法,為RestTemplate新增上
        converters.add(new FormHttpMessageConverter());
        converters.add(new FormMapHttpMessageConverter());
        converters.add(new MappingJackson2HttpMessageConverter());
        //注意restTemplate呼叫該方法會將其構造方法新增converter去除,然後新增
        restTemplate.setMessageConverters(converters);
        
        restTemplate.setErrorHandler(new LoggingErrorHandler());
        if (!this.useParametersForClientAuthentication) {
            List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
            if (interceptors == null) {
                interceptors = new ArrayList();
                restTemplate.setInterceptors((List)interceptors);
            }

            ((List)interceptors).add(new PreemptiveBasicAuthClientHttpRequestInterceptor(this.clientId, this.clientSecret));
        }

        return restTemplate;
    }
}
  • AbstractOAuth2ApiBinding
    該類定義了獲取第三方資訊的一些公用方法和屬性(如RestTemplate),需要被實現其構造方法,本例為QQImp
@Slf4j
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){
        //呼叫父類構造方法,設定預設屬性,即在RestTemplate傳送請求是在請求url中加上accessToken
        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\":","}");
        log.info(result);
    }

    @Override
    public QQUserInfo getQQUserInfo(){
        String url = String.format(URL_GET_USERINFO, appId, openId);
        String result = getRestTemplate().getForObject(url, String.class);

        try {
            QQUserInfo qqUserInfo = objectMapper.readValue(result, QQUserInfo.class);
            qqUserInfo.setOpenId(openId);
            return qqUserInfo;
        } catch (IOException e) {
            throw new RuntimeException("獲取使用者資訊失敗", e);
        }
    }
}

  • OAuth2ConnectionFactory
    建立connection
    獲取ServiceProvier,實現獲取code和Token
    獲取ApiAdapter,實現通過Api獲取第三方應用資訊與connection匹配
public class QQConnectionFactroy extends OAuth2ConnectionFactory<QQ> {

    public QQConnectionFactroy(String providerId, String appId, String appSecret) {
        super(providerId, new QQServiceProvidor(appId, appSecret), new QQAdapter());
    }
}