← Назад к вопросам

Реализовывал ли стороннее API для авторизации

2.0 Middle🔥 211 комментариев
#REST API и микросервисы#Безопасность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Реализовывал ли стороннее 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. Лучшие практики

Что использовал:

  1. Никогда не храни credentials в коде

    // ❌ Плохо
    String clientSecret = "hardcoded_secret";
    
    // ✅ Хорошо
    @Value("${oauth.client.secret}")
    private String clientSecret;
    
  2. Используй HTTPS для всех запросов OAuth

    // Только в production
    // Локально можно разработчикам использовать http
    
  3. Обработай истечение токена

    @Transactional
    public void refreshUserToken(User user) {
        String newToken = tokenProvider.generateToken(user.getId());
        user.setLastTokenRefresh(LocalDateTime.now());
        userRepository.save(user);
    }
    
  4. Логируй попытки авторизации

    @Bean
    public AuditAware<String> auditorProvider() {
        return () -> Optional.of(
            SecurityContextHolder.getContext()
                .getAuthentication()
                .getName()
        );
    }
    
  5. Тестируй авторизацию

    @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
- Логируй и мониторь попытки авторизации
Реализовывал ли стороннее API для авторизации | PrepBro