#reactjs #spring-boot #oauth #facebook-access-token
Вопрос:
В настоящее время я работаю над API Spring Boot REST. Я успешно добавил вход с использованием учетных данных клиента с помощью Spring Oauth и Spring Security (я могу успешно получить маркер доступа и обновить маркер с помощью /oauth/token
конечной точки). Но теперь я хочу предоставить социальный логин с помощью Facebook и Google. Как я понимаю, это и есть поток.
- Пользователь нажимает кнопку Входа в систему с помощью социальной кнопки в интерфейсе React.
- Затем ему будет предложено предоставить доступ. (Все еще в состоянии реакции)
- После этого он будет перенаправлен на интерфейс react с токеном доступа.
- Интерфейс отправляет этот токен доступа на серверную часть Spring Boot. (Я не знаю, до какой конечной точки)
- Затем серверная часть использует этот токен доступа для получения сведений из Facebook/Google и проверки наличия такого пользователя в нашей базе данных.
- Если такой пользователь существует, серверная часть вернет токены доступа и обновления интерфейсу.
- Теперь интерфейс может использовать все конечные точки.
Моя проблема в том, что я понятия не имею о шагах 4,5 и 6. Нужно ли создавать пользовательскую конечную точку для получения токенов доступа FB/Google? Как я могу выдавать пользовательские маркеры доступа и обновления в Spring Boot?
Я был бы очень признателен, если бы вы помогли мне с этим сценарием.
Комментарии:
1. Конечная точка 2, созданная клиентом Spring OAuth2
/oauth2/authorization/YOUR_CLIENT_ID
, используется для перенаправления пользователя на вход в социальную сеть и/login/oauth2/code/YOUR_CLIENT_ID/?code=CODE_GENERATED_BY_SPRINGamp;?state=STATE_GENERATED_BY_SPRING
используется после того, как пользователь вошел в свою социальную сеть, эта служба вызывается для завершения потока входа в систему. Необходимо реализовать SimpleUrlAuthenticationSuccessHandler, чтобы проверить, находится ли пользователь в БД (onAuthenticationSuccess
метод) или нет.2. Я не понял, есть ли у вас логин, основанный на имени пользователя и пароле.
3. Спасибо вам за комментарий. Да, у меня есть логин, основанный на имени пользователя и пароле. Но дело в том, что Spring boot используется для API REST. Мой интерфейс находится в другом домене (или поддомене). Поэтому, я полагаю, мне придется перенаправить с помощью React, получить маркер доступа и передать его в серверную часть, верно?
Ответ №1:
Поток это следующий:
- Интерфейсные вызовы поступают на
/oauth2/authorization/facebook
(или любой другой клиент, который вы хотите использовать) - Серверная часть отвечает перенаправлением на страницу входа в Facebook(в том числе в параметрах запроса, client_id, области действия, redirect_uri(должно присутствовать то же самое на консоли разработчика) и состоянии, которое используется для предотвращения XSRF-атак, в соответствии со стандартами OAuth2)
более подробную информацию вы можете посмотреть здесь https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
государство РЕКОМЕНДОВАЛО. Непрозрачное значение, используемое клиентом для поддержания состояния между запросом и обратным вызовом. Сервер авторизации включает это значение при перенаправлении агента пользователя обратно клиенту. Этот параметр СЛЕДУЕТ использовать для предотвращения подделки межсайтовых запросов, как описано в разделе 10.12. 3) Как только пользователь войдет в систему и примет любое всплывающее окно facebook или других сервисов, пользователь будет перенаправлен на страницу, представленную в «redirect_uri», эта страница должна быть компонентом вашего ReactJS. Обратный вызов будет сопровождаться некоторыми данными, указанными в параметрах запроса, обычно это два параметра: состояние(то же самое, что вы отправили на facebook) и код(который используется из BE для завершения процесса входа в систему).
- Как только facebook или какая-либо другая служба перезвонит вам, вы должны взять эти 2 параметра с URL-адреса(например, используя JS) и позвонить в
/login/oauth2/code/facebook/?code=CODE_GENERATED_BY_FACEBOOKamp;?state=STATE_GENERATED_BY_SPRING
- Весна вызовет службу facebook(с реализацией
OAuth2AccessTokenResponseClient
, используя ваш secret_token, идентификатор клиента, код и несколько других полей. Как только facebook ответит с помощью access_token и refresh_token, spring вызовет реализациюOAuth2UserService
, используемую для получения информации о пользователе из facebook с помощью access_token, созданного за мгновение до этого, в ответ на facebook будет создан сеанс, включающий принципала. (Вы можете перехватить успешный вход в систему, создав реализациюSimpleUrlAuthenticationSuccessHandler
и добавив ее в конфигурацию безопасности spring. (Для facebook, google и otka в теорииOAuth2AccessTokenResponseClient
иOAuth2UserService
реализациях уже должны существовать.
В этом обработчике вы можете указать логику добавления и поиска существующего пользователя.
возвращаемся к поведению по умолчанию
- Once spring created the new session and gave you the JSESSIONID cookie, it will redirect you to the root (I believe, I don’t remember exactly which is the default path after the login, but you can change it, creating your own implementation of the handler I told you before)
Note: access_token and refresh_token will be stored in a OAuth2AuthorizedClient
, stored in the ClientRegistrationRepository
.
This is the end. From now then you can call your back end with that cookie and the be will see you as a logged user. My suggestion is once you got the simple flow working, you should implement a JWT token to use and store in the localstorage of your browser instead of using the cookie.
Hopefully I gave you the infos you were looking for, if I missed something, misunderstood something or something it’s not clear let me know in the comment.
UPDATE (some java samples)
My OAuth2 SecurityConfig :
NOTE:
PROTECTED_URLS it’s just :
public static final RequestMatcher PROTECTED_URLS = new NegatedRequestMatcher(PUBLIC_URLS);
PUBLIC_URLS it’s just:
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher( new AntPathRequestMatcher("/api/v1/login"));
Также обратите внимание, что я использую двойную конфигурацию HttpSecurity. (Но в данном случае это тоже бесполезно публиковать)
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class OAuth2ClientSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final JWTService jwtService;
private final TempUserDataService tempUserDataService;
private final OAuth2AuthorizedClientRepo authorizedClientRepo;
private final OAuth2AuthorizedClientService clientService;
private final UserAuthenticationService authenticationService;
private final SimpleUrlAuthenticationSuccessHandler successHandler; //This is the default one, this bean has been created in another HttpSecurity Configuration file.
private final OAuth2TokenAuthenticationProvider authenticationProvider2;
private final CustomOAuth2AuthorizedClientServiceImpl customOAuth2AuthorizedClientService;
private final TwitchOAuth2UrlAuthSuccessHandler oauth2Filter; //This is the success handler customized.
//In this bean i set the default successHandler and the current AuthManager.
@Bean("oauth2TokenAuthenticaitonFilter")
TokenAuthenticationFilter oatuh2TokenAuthenticationFilter() throws Exception {
TokenAuthenticationFilter filter = new TokenAuthenticationFilter(PROTECTED_URLS);
filter.setAuthenticationSuccessHandler(successHandler);
filter.setAuthenticationManager(authenticationManager());
return filter;
}
@PostConstruct
public void setFilterSettings() {
oauth2Filter.setRedirectStrategy(new NoRedirectStrategy());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider2);
}
@Bean
public RestOperations restOperations() {
return new RestTemplate();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests().antMatchers("/twitch/**").authenticated()
.and().csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable().authenticationProvider(authenticationProvider2)
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
.addFilterBefore(oatuh2TokenAuthenticationFilter(), AnonymousAuthenticationFilter.class)
.oauth2Login().successHandler(oauth2Filter).tokenEndpoint()
.accessTokenResponseClient(new RestOAuth2AccessTokenResponseClient(restOperations()))
.and().authorizedClientService(customOAuth2AuthorizedClientService)
.userInfoEndpoint().userService(new RestOAuth2UserService(restOperations(), tempUserDataService, authorizedClientRepo));
}
@Bean
FilterRegistrationBean disableAutoRegistrationOAuth2Filter() throws Exception {
FilterRegistrationBean registration = new FilterRegistrationBean(oatuh2TokenAuthenticationFilter());
registration.setEnabled(false);
return registration;
}
}
По тому факту, что мой SessionCreationPolicy.STATELESS
файл cookie, созданный весной после окончания потока OAuth2, бесполезен. Поэтому, как только процесс завершен, я даю пользователю временный токен JWT, используемый для доступа к единственно возможной службе (службе регистрации).
Мой фильтр идентификации токенов:
public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String AUTHORIZATION = "Authorization";
private static final String BEARER = "Bearer";
public TokenAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
super(requiresAuthenticationRequestMatcher);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
String token = Optional.ofNullable(httpServletRequest.getHeader(AUTHORIZATION))
.map(v -> v.replace(BEARER, "").trim())
.orElseThrow(() -> new BadCredentialsException("Missing authentication token."));
Authentication auth = new UsernamePasswordAuthenticationToken(token, token);
return getAuthenticationManager().authenticate(auth);
}
@Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setStatus(401);
}
}
TwitchOAuth2UrlAuthSuccessHandler (Вот где происходит все волшебство):
Этот обработчик вызывается один раз службой пользователей, а служба пользователей вызывается при вызове пользователя api.myweb.com/login/oauth2/code/facebook/?code=XXXamp;state=XXX. (пожалуйста, не забывайте о штате)
@Component
@RequiredArgsConstructor
public class TwitchOAuth2UrlAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final OAuth2AuthorizedClientRepo oAuth2AuthorizedClientRepo;
private final UserAuthenticationService authenticationService;
private final JWTService jwtService;
private final Gson gson;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
super.onAuthenticationSuccess(request, response, authentication);
response.setStatus(200);
Cookie cookie = new Cookie("JSESSIONID", null);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
Optional<OAuth2AuthorizedClientEntity> oAuth2AuthorizedClient = oAuth2AuthorizedClientRepo.findById(new OAuth2AuthorizedClientId(((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(), authentication.getName()));
if (oAuth2AuthorizedClient.isPresent() amp;amp; oAuth2AuthorizedClient.get().getUserDetails() != null) {
response.getWriter().write(gson.toJson(authenticationService.loginWithCryptedPassword(oAuth2AuthorizedClient.get().getUserDetails().getUsername(), oAuth2AuthorizedClient.get().getUserDetails().getPassword())));
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().flush();
} else {
response.setHeader("Authorization", jwtService.createTempToken(((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(), authentication.getName()));
}
}
@Override
protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) {
return "";
}
}
RestOAuth2AccessTokenResponseClient (он отвечает за получение доступа и обновления из FB)
public class RestOAuth2AccessTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
private final RestOperations restOperations;
public RestOAuth2AccessTokenResponseClient(RestOperations restOperations) {
this.restOperations = restOperations;
}
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {
ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
String tokenUri = clientRegistration.getProviderDetails().getTokenUri();
MultiValueMap<String, String> tokenRequest = new LinkedMultiValueMap<>();
tokenRequest.add("client_id", clientRegistration.getClientId());
tokenRequest.add("client_secret", clientRegistration.getClientSecret());
tokenRequest.add("grant_type", clientRegistration.getAuthorizationGrantType().getValue());
tokenRequest.add("code", authorizationGrantRequest.getAuthorizationExchange().getAuthorizationResponse().getCode());
tokenRequest.add("redirect_uri", authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getRedirectUri());
tokenRequest.add("scope", String.join(" ", authorizationGrantRequest.getClientRegistration().getScopes()));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add(HttpHeaders.USER_AGENT, "Discord Bot 1.0");
ResponseEntity<AccessResponse> responseEntity = restOperations.exchange(tokenUri, HttpMethod.POST, new HttpEntity<>(tokenRequest, headers), AccessResponse.class);
if (!responseEntity.getStatusCode().equals(HttpStatus.OK) || responseEntity.getBody() == null) {
throw new SecurityException("The result of token call returned error or the body returned null.");
}
AccessResponse accessResponse = responseEntity.getBody();
Set<String> scopes = accessResponse.getScopes().isEmpty() ?
authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getScopes() : accessResponse.getScopes();
return OAuth2AccessTokenResponse.withToken(accessResponse.getAccessToken())
.tokenType(accessResponse.getTokenType())
.expiresIn(accessResponse.getExpiresIn())
.refreshToken(accessResponse.getRefreshToken())
.scopes(scopes)
.build();
}
}
Обслуживание пользователей
public class RestOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final RestOperations restOperations;
private final TempUserDataService tempUserDataService;
private final OAuth2AuthorizedClientRepo authorizedClientRepo;
public RestOAuth2UserService(RestOperations restOperations, TempUserDataService tempUserDataService, OAuth2AuthorizedClientRepo authorizedClientRepo) {
this.restOperations = restOperations;
this.tempUserDataService = tempUserDataService;
this.authorizedClientRepo = authorizedClientRepo;
}
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
String userInfoUrl = oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri();
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", oAuth2UserRequest.getAccessToken().getTokenValue()));
headers.add(HttpHeaders.USER_AGENT, "Discord Bot 1.0");
if (oAuth2UserRequest.getClientRegistration().getClientName().equals("OAuth2 Twitch")) {
headers.add("client-id", oAuth2UserRequest.getClientRegistration().getClientId());
}
ParameterizedTypeReference<Map<String, Object>> typeReference = new ParameterizedTypeReference<Map<String, Object>>() {
};
ResponseEntity<Map<String, Object>> responseEntity = restOperations.exchange(userInfoUrl, HttpMethod.GET, new HttpEntity<>(headers), typeReference);
if (!responseEntity.getStatusCode().equals(HttpStatus.OK) || responseEntity.getBody() == null) {
throw new SecurityException("The result of token call returned error or the body returned null.");
}
Map<String, Object> userAttributes = responseEntity.getBody();
userAttributes = LinkedHashMap.class.cast(((ArrayList) userAttributes.get("data")).get(0));
OAuth2AuthorizedClientId clientId = new OAuth2AuthorizedClientId(oAuth2UserRequest.getClientRegistration().getRegistrationId(), String.valueOf(userAttributes.get("id")));
Optional<OAuth2AuthorizedClientEntity> clientEntity = this.authorizedClientRepo.findById(clientId);
if (!clientEntity.isPresent() || clientEntity.get().getUserDetails() == null) {
TempUserData tempUserData = new TempUserData();
tempUserData.setClientId(clientId);
tempUserData.setUsername(String.valueOf(userAttributes.get("login")));
tempUserData.setEmail(String.valueOf(userAttributes.get("email")));
tempUserDataService.save(tempUserData);
}
Set<GrantedAuthority> authorities = Collections.singleton(new OAuth2UserAuthority(userAttributes));
return new DefaultOAuth2User(authorities, userAttributes, oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName());
}
Как и было сказано, это весь код, который вам нужен, просто чтобы дать вам еще одну подсказку. При вызове /login/oauth2/code/facebook/?code=XXXamp;?state=XXX
цепочки происходит следующее:
- restoauth2accesstokenответчик
- restoauth2пользовательский сервис
- TwitchOAuth2UrlAuthSuccessHandler
Я надеюсь, что это может вам помочь. Дайте мне знать, если вам понадобятся дополнительные объяснения.
Комментарии:
1. Еще раз большое вам спасибо. У вас есть какие-нибудь примеры кодов?
2. У меня нет чистого, но если вам это нужно, я могу опубликовать образец, когда закончу работу .
3. Прежде чем я опубликую какой-нибудь код , мне нужно знать, хотите ли вы связать учетную запись (пользователя и пароль) с логином OAuth2. Например, настройка двойного входа в систему. Это не совсем ясно
4. Не совсем. Мое намерение состоит в том, чтобы сравнить электронную почту пользователя в Facebook с моей базой данных и войти в него или иным образом создать для него новую учетную запись, используя данные jis из Facebook. Является ли это обычной практикой? Или мне следует связать учетную запись Facebook с помощью facebook_id?
5. Я был бы очень признателен, если бы вы могли опубликовать какой-нибудь код.