Реализовывал ли стороннее API для авторизации
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Реализовывал ли стороннее API для авторизации
Да, это частая задача в современных приложениях. Рассказу о своём опыте интеграции с различными провайдерами авторизации.
1. OAuth 2.0 с Google
Проект: Web-приложение для управления проектами
Стек:
- Spring Security с OAuth 2.0
- Google API
- JWT токены
Реализация:
# application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid,profile,email
redirect-uri: http://localhost:8080/login/oauth2/code/google
provider:
google:
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://www.googleapis.com/oauth2/v4/token
user-info-uri: https://www.googleapis.com/oauth2/v1/userinfo
user-name-attribute: email
Spring Security конфигурация:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.defaultSuccessUrl("/dashboard")
.failureUrl("/login?error")
.successHandler(customSuccessHandler())
)
.logout(logout -> logout
.logoutSuccessUrl("/login")
.clearAuthentication(true)
);
return http.build();
}
@Bean
public OAuth2AuthenticationSuccessHandler customSuccessHandler() {
return new OAuth2AuthenticationSuccessHandler();
}
}
Обработчик успешной авторизации:
@Component
public class OAuth2AuthenticationSuccessHandler
extends SimpleUrlAuthenticationSuccessHandler {
@Autowired
private UserService userService;
@Autowired
private JwtTokenProvider tokenProvider;
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
// Получаем информацию пользователя из OAuth2
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
String email = oAuth2User.getAttribute("email");
String name = oAuth2User.getAttribute("name");
// Проверяем, существует ли пользователь в БД
User user = userService.findByEmail(email)
.orElseGet(() -> userService.createUser(
new CreateUserRequest(email, name, "OAUTH2_GOOGLE")
));
// Генерируем JWT токен
String jwtToken = tokenProvider.generateToken(user.getId());
// Перенаправляем с токеном
String targetUrl = "http://localhost:3000/dashboard?token=" + jwtToken;
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}
2. OAuth 2.0 с GitHub
Проект: Developer сообщество (похоже на Habr)
Особенность: GitHub позволяет легко получить информацию о профиле разработчика.
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private GithubService githubService;
@Autowired
private UserService userService;
@PostMapping("/github")
public ResponseEntity<AuthResponse> authenticateWithGithub(
@RequestBody GithubAuthRequest request) {
// request.code — код, полученный от GitHub на клиенте
// Обмениваем на access_token
String accessToken = githubService.exchangeCodeForToken(request.getCode());
// Получаем информацию о пользователе
GithubUser githubUser = githubService.getUserInfo(accessToken);
// Проверяем или создаём пользователя
User user = userService.findByGithubId(githubUser.getId())
.orElseGet(() -> userService.createUserFromGithub(githubUser));
// Генерируем наш JWT
String jwtToken = tokenProvider.generateToken(user.getId());
return ResponseEntity.ok(new AuthResponse(jwtToken, user));
}
}
@Component
public class GithubService {
@Value("${github.client-id}")
private String clientId;
@Value("${github.client-secret}")
private String clientSecret;
private final RestTemplate restTemplate = new RestTemplate();
public String exchangeCodeForToken(String code) {
// POST https://github.com/login/oauth/access_token
Map<String, String> body = Map.of(
"client_id", clientId,
"client_secret", clientSecret,
"code", code
);
HttpHeaders headers = new HttpHeaders();
headers.set("Accept", "application/json");
HttpEntity<Map<String, String>> request = new HttpEntity<>(body, headers);
GithubTokenResponse response = restTemplate.postForObject(
"https://github.com/login/oauth/access_token",
request,
GithubTokenResponse.class
);
return response.getAccessToken();
}
public GithubUser getUserInfo(String accessToken) {
// GET https://api.github.com/user
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + accessToken);
HttpEntity<Void> request = new HttpEntity<>(headers);
return restTemplate.exchange(
"https://api.github.com/user",
HttpMethod.GET,
request,
GithubUser.class
).getBody();
}
}
3. OpenID Connect
Проект: Корпоративное приложение с Single Sign-On (SSO)
Провайдер: Keycloak (собственный провайдер компании)
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: my-app
client-secret: ${KEYCLOAK_SECRET}
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/keycloak
scope: openid,profile,email
provider:
keycloak:
issuer-uri: http://keycloak:8080/auth/realms/company
authorization-uri: http://keycloak:8080/auth/realms/company/protocol/openid-connect/auth
token-uri: http://keycloak:8080/auth/realms/company/protocol/openid-connect/token
user-info-uri: http://keycloak:8080/auth/realms/company/protocol/openid-connect/userinfo
jwk-set-uri: http://keycloak:8080/auth/realms/company/protocol/openid-connect/certs
user-name-attribute: preferred_username
Использование JWT токена для авторизации:
@Component
public class JwtTokenValidator {
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuerUri;
private JwtDecoder jwtDecoder;
@PostConstruct
public void init() {
this.jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuerUri).build();
}
public boolean isTokenValid(String token) {
try {
Jwt jwt = jwtDecoder.decode(token);
return !isTokenExpired(jwt);
} catch (JwtException e) {
log.error("Token validation failed: {}", e.getMessage());
return false;
}
}
private boolean isTokenExpired(Jwt jwt) {
return jwt.getExpiresAt() != null &&
Instant.now().isAfter(jwt.getExpiresAt());
}
}
@RestController
@RequestMapping("/api/protected")
public class ProtectedController {
@GetMapping("/profile")
public ResponseEntity<UserProfile> getProfile(
@AuthenticationPrincipal Jwt jwt) {
String username = jwt.getClaimAsString("preferred_username");
String email = jwt.getClaimAsString("email");
UserProfile profile = new UserProfile(username, email);
return ResponseEntity.ok(profile);
}
}
4. API Key авторизация (для микросервисов)
Проект: Internal API между микросервисами
@Component
public class ApiKeyValidator {
@Value("${app.api-keys}")
private Map<String, String> apiKeys;
public boolean isValidApiKey(String apiKey) {
return apiKeys.values().contains(apiKey);
}
public String getServiceNameByApiKey(String apiKey) {
return apiKeys.entrySet().stream()
.filter(entry -> entry.getValue().equals(apiKey))
.map(Map.Entry::getKey)
.findFirst()
.orElse(null);
}
}
@Configuration
@EnableWebSecurity
public class ApiSecurityConfig {
@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/health", "/metrics").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.addFilterBefore(
new ApiKeyAuthenticationFilter(apiKeyValidator()),
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
}
public class ApiKeyAuthenticationFilter extends OncePerRequestFilter {
private final ApiKeyValidator validator;
public ApiKeyAuthenticationFilter(ApiKeyValidator validator) {
this.validator = validator;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && validator.isValidApiKey(apiKey)) {
String serviceName = validator.getServiceNameByApiKey(apiKey);
// Создаём Authentication
ApiKeyAuthentication auth = new ApiKeyAuthentication(
serviceName,
apiKey,
AuthorityUtils.createAuthorityList("ROLE_SERVICE")
);
auth.setAuthenticated(true);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
}
5. Обработка ошибок авторизации
@RestControllerAdvice
public class AuthExceptionHandler {
@ExceptionHandler(OAuth2AuthenticationException.class)
public ResponseEntity<ErrorResponse> handleOAuth2Error(
OAuth2AuthenticationException ex) {
log.error("OAuth2 authentication failed: {}", ex.getMessage(), ex);
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse(
"OAUTH2_AUTH_FAILED",
"OAuth authentication failed. Please try again."
));
}
@ExceptionHandler(JwtException.class)
public ResponseEntity<ErrorResponse> handleJwtError(
JwtException ex) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse(
"JWT_VALIDATION_FAILED",
"Invalid or expired token"
));
}
}
6. Лучшие практики
Что использовал:
-
Никогда не храни credentials в коде
// ❌ Плохо String clientSecret = "hardcoded_secret"; // ✅ Хорошо @Value("${oauth.client.secret}") private String clientSecret; -
Используй HTTPS для всех запросов OAuth
// Только в production // Локально можно разработчикам использовать http -
Обработай истечение токена
@Transactional public void refreshUserToken(User user) { String newToken = tokenProvider.generateToken(user.getId()); user.setLastTokenRefresh(LocalDateTime.now()); userRepository.save(user); } -
Логируй попытки авторизации
@Bean public AuditAware<String> auditorProvider() { return () -> Optional.of( SecurityContextHolder.getContext() .getAuthentication() .getName() ); } -
Тестируй авторизацию
@SpringBootTest
public class OAuth2SecurityTests { @Test @WithMockOAuth2User(authorities = "ROLE_USER") public void testAuthenticatedEndpoint() { // тест } }
### Практический опыт
**Сложности, которые встретил:**
1. **CORS проблемы** при OAuth редиректах (решено через proxy)
2. **Роли и permissions** — нужно маппировать роли провайдера на внутренние
3. **Обновление токена** — refresh token логика
4. **Многопровайдерная авторизация** — связать несколько OAuth провайдеров для одного пользователя
**Рекомендация:**
- Используй **Spring Security** для большинства случаев
- Для микросервисов рассмотри **Keycloak** или **Auth0**
- Всегда храни секреты в environment variables
- Логируй и мониторь попытки авторизации