Spring Security OAuth2 第三方登入(一)之流程說明
阿新 • • 發佈:2019-01-12
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());
}
}