Интеграция Spring Security OAuth2 и Spring Social
Я работаю с приложением Spring Boot + Spring Security OAuth2, которое, как мне кажется, было вдохновлено примерами Дэйва Сайера. Приложение настроено как сервер авторизации OAuth2 с одним общедоступным клиентом, использующим поток учетных данных владельца ресурса. Успешный токен настроен как JWT.
Общедоступный клиент Angular отправляет запрос POST в /oauth/token с базовым заголовком аутентификации, содержащим идентификатор и секретный ключ клиента (это был самый простой способ заставить клиента пройти аутентификацию, даже если секрет не является частным). Тело запроса содержит имя пользователя, пароль и тип предоставления "пароль".
Помимо того, что он является сервером аутентификации, приложение является сервером ресурсов RESTful для пользователей, групп и организаций.
Я пытаюсь добавить дополнительный поток аутентификации SSO, используя Spring Social. Я настроил Spring Social для аутентификации через внешних провайдеров через /auth/[provider]; однако, следующие запросы больше не имеют корректно установленный SecurityContext. Возможно, сервер или клиент Spring Security OAuth переопределяет SecurityContext?
Если я смогу правильно установить SecurityContext после потока Spring Social, я получу новый TokenGranter, который позволяет новый тип предоставления "social", который будет проверять SecurityContextHolder для предварительно аутентифицированного пользователя.
Меня интересует как решение моей конкретной проблемы с SecurityContext (я полагаю, что это проблема с интеграцией Spring OAuth + Social), так и другой подход для аутентификации с внешними провайдерами и получения действительного JWT с нашего собственного сервера аутентификации.
Спасибо!
4 ответа
У меня была похожая проблема в веб-приложении, созданном JHipster. Наконец я решил пойти с SocialAuthenticationFilter
вариант от Spring Social (через SpringSocialConfigurer
). После успешного входа в социальную сеть сервер автоматически генерирует и возвращает "собственный" токен доступа через перенаправление клиентскому приложению.
Вот моя попытка:
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter implements EnvironmentAware {
//...
@Inject
private AuthorizationServerTokenServices authTokenServices;
@Override
public void configure(HttpSecurity http) throws Exception {
SpringSocialConfigurer socialCfg = new SpringSocialConfigurer();
socialCfg
.addObjectPostProcessor(new ObjectPostProcessor<SocialAuthenticationFilter>() {
@SuppressWarnings("unchecked")
public SocialAuthenticationFilter postProcess(SocialAuthenticationFilter filter){
filter.setAuthenticationSuccessHandler(
new SocialAuthenticationSuccessHandler(
authTokenServices,
YOUR_APP_CLIENT_ID
)
);
return filter;
}
});
http
//... lots of other configuration ...
.apply(socialCfg);
}
}
И SocialAuthenticationSuccessHandler
учебный класс:
public class SocialAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
public static final String REDIRECT_PATH_BASE = "/#/login";
public static final String FIELD_TOKEN = "access_token";
public static final String FIELD_EXPIRATION_SECS = "expires_in";
private final Logger log = LoggerFactory.getLogger(getClass());
private final AuthorizationServerTokenServices authTokenServices;
private final String localClientId;
public SocialAuthenticationSuccessHandler(AuthorizationServerTokenServices authTokenServices, String localClientId){
this.authTokenServices = authTokenServices;
this.localClientId = localClientId;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
log.debug("Social user authenticated: " + authentication.getPrincipal() + ", generating and sending local auth");
OAuth2AccessToken oauth2Token = authTokenServices.createAccessToken(convertAuthentication(authentication)); //Automatically checks validity
String redirectUrl = new StringBuilder(REDIRECT_PATH_BASE)
.append("?").append(FIELD_TOKEN).append("=")
.append(encode(oauth2Token.getValue()))
.append("&").append(FIELD_EXPIRATION_SECS).append("=")
.append(oauth2Token.getExpiresIn())
.toString();
log.debug("Sending redirection to " + redirectUrl);
response.sendRedirect(redirectUrl);
}
private OAuth2Authentication convertAuthentication(Authentication authentication) {
OAuth2Request request = new OAuth2Request(null, localClientId, null, true, null,
null, null, null, null);
return new OAuth2Authentication(request,
//Other option: new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), "N/A", authorities)
new PreAuthenticatedAuthenticationToken(authentication.getPrincipal(), "N/A")
);
}
private String encode(String in){
String res = in;
try {
res = UriUtils.encode(in, GeneralConstants.ENCODING_UTF8);
} catch(UnsupportedEncodingException e){
log.error("ERROR: unsupported encoding: " + GeneralConstants.ENCODING_UTF8, e);
}
return res;
}
}
Таким образом, ваше клиентское приложение получит маркер доступа вашего веб-приложения через перенаправление на /#/login?access_token=my_access_token&expires_in=seconds_to_expiration
До тех пор, пока вы установите соответствующий REDIRECT_PATH_BASE
в SocialAuthenticationSuccessHandler
,
Я надеюсь, что это помогает.
Во-первых, я настоятельно рекомендую вам отказаться от предоставления пароля для такого случая использования.
Общедоступные клиенты (JavaScript, установленные приложения) не могут сохранять секретность своего клиента, поэтому им НЕ ДОЛЖЕН быть назначен один: любой посетитель, проверяющий ваш код JavaScript, может обнаружить секрет и, таким образом, реализовать ту же страницу аутентификации, что и у вас, сохраняя свои пароли пользователей в процесс.
Неявный грант был создан именно для того, что вы делаете.
Преимущество использования потока на основе перенаправления заключается в том, что механизм аутентификации остается за сервером авторизации, а не у каждого из ваших приложений: это в основном определение единого входа (SSO).
С учетом вышесказанного ваш вопрос тесно связан с вопросом, на который я только что ответил: Собственный сервер Spring OAuth2 вместе с поставщиками OAuth от третьих сторон
Подводя итог ответу:
В конце концов, речь идет о том, как ваш сервер авторизации защищает точку авторизации: /oauth/authorize. Поскольку ваш сервер авторизации работает, у вас уже есть класс конфигурации, расширяющий WebSecurityConfigurerAdapter, который обрабатывает безопасность для / oauth / authorize с помощью formLogin. Вот где вам нужно интегрировать социальные вещи.
Вы просто не можете использовать предоставление пароля для того, чего пытаетесь достичь, ваш публичный клиент должен перенаправить на сервер авторизации. Сервер авторизации будет перенаправлять на социальную учетную запись в качестве механизма безопасности для /oauth/authorize
конечная точка.
Я начал с хорошего ответа выше ( /questions/14114851/integratsiya-spring-security-oauth2-i-spring-social/14114860#14114860), однако с моей версией Spring Security (4.2.8.RELEASE) это не удалось. Причина в том, что в org.springframework.security.access.intercept.AbstractSecurityInterceptor#authenticateIfRequired
PreAuthenticatedAuthenticationToken
ответа не подтверждено. Некоторые предоставленные полномочия должны быть переданы. Кроме того, разделение токена в параметре URL не очень хорошо, его всегда следует скрывать в полезной нагрузке HTTP или заголовке. Вместо этого загружается шаблон HTML, и значение токена вставляется в ${token}
заполнитель поля.
Вот пересмотренная версия:
ПРИМЕЧАНИЕ: используемый UserDetails
здесь реализует org.springframework.security.core.userdetails.UserDetails
@Component
public class SocialAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private OAuth2TokenStore tokenStore;
@Qualifier("tokenServices")
@Autowired
private AuthorizationServerTokenServices authTokenServices;
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
IClient user = ((SocialUserDetails) authentication.getPrincipal()).getUser();
// registration is not finished, forward the user, a marker interface
// IRegistration is used here, remove this if there no two step approach to
// create a user from a social network
if (user instanceof IRegistration) {
response.sendRedirect(subscriberRegistrationUrl + "/" + user.getId());
}
OAuth2AccessToken token = loginUser(user);
// load a HTML template from the class path and replace the token placeholder within, the HTML should contain a redirect to the actual page, but must store the token in a safe place, e.g. for preventing CSRF in the `sessionStorage` JavaScript storage.
String html = IOUtils.toString(getClass().getResourceAsStream("/html/socialLoginRedirect.html"));
html = html.replace("${token}", token.getValue());
response.getOutputStream().write(html.getBytes(StandardCharsets.UTF_8));
}
private OAuth2Authentication convertAuthentication(Authentication authentication) {
OAuth2Request request = new OAuth2Request(null, authentication.getName(),
authentication.getAuthorities(), true, null,
null, null, null, null);
// note here the passing of the authentication.getAuthorities()
return new OAuth2Authentication(request,
new PreAuthenticatedAuthenticationToken(authentication.getPrincipal(), "N/A", authentication.getAuthorities())
);
}
/**
* Logs in a user.
*/
public OAuth2AccessToken loginUser(IClient user) {
SecurityContext securityContext = SecurityContextHolder.getContext();
UserDetails userDetails = new UserDetails(user);
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, "N/A", userDetails.getAuthorities());
securityContext.setAuthentication(authentication);
OAuth2Authentication oAuth2Authentication = convertAuthentication(authentication);
// delete the token because the client id in the DB is calculated as hash of the username and client id (here also also identical to username), this would be identical to the
// to an existing user. This existing one can come from a user registration or a previous user with the same name.
// If a new entity with a different ID is used the stored token hash would differ and the the wrong token would be retrieved
tokenStore.deleteTokensForUserId(user.getUsername());
OAuth2AccessToken oAuth2AccessToken = authTokenServices.createAccessToken(oAuth2Authentication);
// the DB id of the created user is returned as additional data, can be
// removed if not needed
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(new HashMap<>());
oAuth2AccessToken.getAdditionalInformation().put("userId", user.getId());
return oAuth2AccessToken;
}
}
пример socialLoginRedirect.html
:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example App</title>
<meta http-equiv="Refresh" content="0; url=/index.html#/home"/>
</head>
<script>
window.sessionStorage.setItem('access_token', '${token}');
</script>
<body>
<p>Please follow <a href="/index.html#/home">this link</a>.</p>
</body>
</html>
Конфигурация проводки в WebSecurityConfigurerAdapter
:
@Configuration
@EnableWebSecurity
@EnableWebMvc
@Import(WebServiceConfig.class)
public class AuthenticationConfig extends WebSecurityConfigurerAdapter {
@Value("${registrationUrl}")
private String registrationUrl;
@Autowired
private SocialAuthenticationSuccessHandler socialAuthenticationSuccessHandler;
@Value("${loginUrl}")
private String loginUrl;
@Override
protected void configure(HttpSecurity http) throws Exception {
List<String> permitAllUrls = new ArrayList<>();
// permit social log in
permitAllUrls.add("/auth/**");
http.authorizeRequests().antMatchers(permitAllUrls.toArray(new String[0])).permitAll();
SpringSocialConfigurer springSocialConfigurer = new SpringSocialConfigurer();
springSocialConfigurer.signupUrl(registrationUrl);
springSocialConfigurer.postFailureUrl(loginUrl);
springSocialConfigurer
.addObjectPostProcessor(new ObjectPostProcessor<SocialAuthenticationFilter>() {
@SuppressWarnings("unchecked")
public SocialAuthenticationFilter postProcess(SocialAuthenticationFilter filter){
filter.setAuthenticationSuccessHandler(socialAuthenticationSuccessHandler);
return filter;
}
});
http.apply(springSocialConfigurer);
http.logout().disable().csrf().disable();
http.requiresChannel().anyRequest().requiresSecure();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
Я реализовал Spring oauth2, чтобы защитить свои службы отдыха и дополнительно добавить социальную регистрацию и неявную регистрацию для первого входа. для пользователя user вы можете сгенерировать токен, используя только имя пользователя и пароль, проблема с генерацией токена для социального пользователя. для этого вам нужно внедрить фильтр, который будет перехватывать ваш запрос / oauth / token перед обработкой. здесь, если вы хотите сгенерировать токен для социального пользователя, передайте имя пользователя и токен Facebook, здесь вы можете использовать токен facebook в качестве пароля и также сгенерировать токен для пользователя Facebook. если токен Facebook обновлен, то вам нужно написать триггер db, чтобы обновить токен в таблице пользователей.... может быть, это поможет вам