Cześć,
czytam właśnie o podejściu Fail Fast i chcę je wdrożyć w swoim pet projekcie. Postanowiłem to zrobić na przykładzie - Integracja z Spotify API w celu uzyskania tokenu dostępu.
Po pomyślnej weryfikacji Spotify API zwróci response
, w którym interesuję mnie:
- access token,
- refresh token,
- scope.
Refresh token
nie ma expiration time, dlatego zdecydowałem się przechowywać go w Redis. Potrzebuję klucza identyfikującego użytkownika, wybór padł na ID
które będzie pobierane z JWT tokena
, który z kolei zostanie odczytany z cookie
.
Tu kontroler który to robi:
Jak token będzie null
, adnotacja @CookieValue
sama rzuci wyjątek. Postanowiłem jeszcze dodać walidację, która sprawdzi, czy token nie jest pusty, no bo po co kontynuować pracę z pustym tokenem.
@RestController
@RequestMapping("/api/v1/spotify/auth")
public class SpotifyCallbackController {
private final SpotifyAccessTokenHandler tokenHandler;
private final CookieUtils cookieUtils;
public SpotifyCallbackController(SpotifyAccessTokenHandler tokenHandler,
CookieUtils cookieUtils) {
this.tokenHandler = tokenHandler;
this.cookieUtils = cookieUtils;
}
@GetMapping("/callback")
public ResponseEntity<ApiResponse> handleSpotifyCallback(@RequestParam("code") String authCode,
@CookieValue(name = "jwtToken") String jwtToken) {
// dodana walidacja
if (jwtToken.isEmpty()) {
throw new IllegalArgumentException("Jwt is empty");
}
String accessToken = tokenHandler.retrieveSpotifyAccessToken(authCode, jwtToken);
ResponseCookie cookie = cookieUtils.generateCookie("spotifyAccessToken", accessToken, "/api/v1/spotify", 3600);
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(new ApiResponse("Spotify authorization successful."));
}
}
Kolejna klasa handler - zawiera komentarze z numerem walidacji, ich opis znajduję się pod kodem.
@Service
public class SpotifyAccessTokenHandler {
private final String redirectUri;
private final JwtTokenManager jwtTokenManager;
private final RedisTokenStore redisTokenStore;
private final SpotifyTokenApi spotifyTokenApi;
public SpotifyAccessTokenHandler(@Value("${REDIRECT_URI}") String redirectUri,
JwtTokenManager jwtTokenManager,
RedisTokenStore redisTokenStore,
SpotifyTokenApi spotifyTokenApi) {
this.redirectUri = redirectUri;
this.jwtTokenManager = jwtTokenManager;
this.redisTokenStore = redisTokenStore;
this.spotifyTokenApi = spotifyTokenApi;
}
public String retrieveSpotifyAccessToken(String authCode, String jwtToken) {
// 1. walidacja id
Integer userId = extractAndValidateUserId(jwtToken);
// 2. walidacja requestu i responsu wewnątrz dto
MultiValueMap<String, String> formData = new SpotifyAccessTokenRequest(
"authorization_code", authCode, redirectUri).toMultiValueMap();
SpotifyTokenResponse response = spotifyTokenApi.sendTokenRequest(formData);
redisTokenStore.saveRefreshToken(userId, response.refreshToken());
return response.accessToken();
}
private Integer extractAndValidateUserId(String jwtToken) {
Integer userId = jwtTokenManager.extractUserId(jwtToken);
if (userId == null || userId <= 0) {
throw new IllegalArgumentException("User id is null or invalid");
}
return userId;
}
}
- Zapisuję
refresh token
w Redis i potrzebuję klucza identyfikującego -id
, uznałem że na samym początku zwalidujęid
. No bo po co dalej kontynuować pracę jak coś z nim jest nie tak - nie będę mógł przypisać do użytkownikarefresh tokenu
. - Tworzę formularz przy użyciu
SpotifyAccessTokenRequest
i dodaję prostą walidację nanull
iempty
w konstruktorze.
public record SpotifyAccessTokenRequest(@JsonProperty("grant_type") String grantType,
@JsonProperty("code") String authCode,
@JsonProperty("redirect_uri") String redirectUri) {
public SpotifyAccessTokenRequest {
if (grantType == null || grantType.isEmpty()) {
throw new IllegalArgumentException("Grant type cannot be null or empty");
}
if (authCode == null || authCode.isEmpty()) {
throw new IllegalArgumentException("Authorization code annot be null or empty");
}
if (redirectUri == null || redirectUri.isEmpty()) {
throw new IllegalArgumentException("Redirect uri cannot be null or empty");
}
}
public MultiValueMap<String, String> toMultiValueMap() {
//
}
}
W SpotifyTokenResponse
w konstruktorze waliduję czy zwracane wartości nie są null
i empty
.
public record SpotifyTokenResponse(@JsonProperty("access_token") String accessToken,
@JsonProperty("refresh_token") String refreshToken,
@JsonProperty("scope") String scope) {
public SpotifyTokenResponse {
if (accessToken == null || accessToken.isEmpty()) {
throw new IllegalArgumentException("Access token cannot be null or empty");
}
if (refreshToken == null || refreshToken.isEmpty()) {
throw new IllegalArgumentException("Refresh token cannot be null or empty");
}
if (scope == null || scope.isEmpty()) {
throw new IllegalArgumentException("Scope cannot be null or empty");
}
}
}
Klasa odpowiadająca za wysyłanie requestu do API:
Sprawdzam czy bodyData
nie jest pusty.
@Service
@Slf4j
public class SpotifyTokenApi {
private final String spotifyClientId;
private final String spotifyClientSecret;
private final WebClient webClient;
public SpotifyTokenApi(@Value("${SPOTIFY_CLIENT_ID}") String spotifyClientId,
@Value("${SPOTIFY_CLIENT_SECRET}") String spotifyClientSecret,
@Qualifier("spotifyWebClient") WebClient webClient) {
this.spotifyClientId = spotifyClientId;
this.spotifyClientSecret = spotifyClientSecret;
this.webClient = webClient;
}
public SpotifyTokenResponse sendTokenRequest(MultiValueMap<String, String> bodyData) {
//walidacja
if (bodyData == null || bodyData.isEmpty()) {
throw new IllegalArgumentException("Request body data cannot be null or empty");
}
// kod
}
}
Skoro API przy requescie oczekuję konkretnych kluczy, może powinienem je zwalidować zamiast tak ogólnie bodyData
?
Dodałbym takie coś:
if (!bodyData.containsKey("grant_type") || bodyData.getFirst("grant_type") == null || bodyData.getFirst("grant_type").isEmpty()) {
throw new IllegalArgumentException("Missing or empty 'grant_type'");
}
if (!bodyData.containsKey("code") || bodyData.getFirst("code") == null || bodyData.getFirst("code").isEmpty()) {
throw new IllegalArgumentException("Missing or empty 'code'");
}
if (!bodyData.containsKey("redirect_uri") || bodyData.getFirst("redirect_uri") == null || bodyData.getFirst("redirect_uri").isEmpty()) {
throw new IllegalArgumentException("Missing or empty 'redirect_uri'");
}
Czy to co napisałem na sens? Co myślicie?
Z góry dziękuję za pomoc.