blog

半日かけてソースコードを拾い集め、ようやくOauth2カスタム処理結果の最適解を見つけた!

マイクロサービス許可究極解、Spring Cloud Gateway + Oauth2で実現する統一認証・認証方式」では、「マイクロサービス許可究極解、Spring Cloud Gateway + O...

Feb 23, 2020 · 9 min. read
シェア

まとめ

何が問題なのでしょうか?

Oauth2の処理結果のカスタマイズは、主にインターフェースから返される情報のフォーマットを標準化するために、以下の側面からアプローチされます。

  • Oauth2ログイン認証に成功した場合と失敗した場合に返される結果をカスタマイズします;
  • 期限切れまたは不正に署名された JWT トークンと、ゲートウェイ認証に失敗した場合の返り値;
  • 有効期限が切れているか、正しく署名されていない JWT トークンでホワイトリストのインターフェースにアクセスすると、ゲートウェイによる直接認証に失敗します。

ログイン認証結果のカスタマイズ

認証成功の返り値

  • 共通のリターン結果CommonResultは統一され、Oauth2の結果は明らかに一致しない、統一する必要があり、共通のリターン結果の形式は次のとおりです;
/**
 * 汎用リターンオブジェクト
 * Created by macro on 2019/4/19.
 */
public class CommonResult<T> {
 private long code;
 private String message;
 private T data;
}
  • org.springframework.security.oauth2.provider.endpoint.TokenEndpoint実際には、キーのクラスを見つける限り、Oauth2ログイン認証インターフェイスをカスタマイズすることができます、それは、非常に身近なログイン認証インターフェイスを定義している限り、ログイン認証インターフェイスを書き換えるには、直接ロジックのデフォルトの実装を呼び出し、次に対処するためにデフォルトの戻り値の結果は、次のことができますロジックのデフォルトの実装です;
@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {
	@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
	public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
	Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
		if (!(principal instanceof Authentication)) {
			throw new InsufficientAuthenticationException(
					"There is no client authentication. Try adding an appropriate authentication filter.");
		}
		String clientId = getClientId(principal);
		ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
		if (clientId != null && !clientId.equals("")) {
			// Only validate the client details if a client authenticated during this
			// request.
			if (!clientId.equals(tokenRequest.getClientId())) {
				// double check to make sure that the client ID in the token request is the same as that in the
				// authenticated client
				throw new InvalidClientException("Given client ID does not match authenticated client");
			}
		}
		if (authenticatedClient != null) {
			oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
		}
		if (!StringUtils.hasText(tokenRequest.getGrantType())) {
			throw new InvalidRequestException("Missing grant type");
		}
		if (tokenRequest.getGrantType().equals("implicit")) {
			throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
		}
		if (isAuthCodeRequest(parameters)) {
			// The scope was requested or determined during the authorization step
			if (!tokenRequest.getScope().isEmpty()) {
				logger.debug("Clearing scope of incoming token request");
				tokenRequest.setScope(Collections.<String> emptySet());
			}
		}
		if (isRefreshTokenRequest(parameters)) {
			// A refresh token has its own default scopes, so we should ignore any added by the factory here.
			tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
		}
		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
		if (token == null) {
			throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
		}
		return getResponse(token);
	}
}
  • 必要なJWT情報をオブジェクトにカプセル化し、ジェネリックな戻り結果のdataプロパティに入れます;
/**
 * Oauth2Get Tokenのリターン情報のカプセル化
 * Created by macro on 2020/7/17.
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Builder
public class Oauth2TokenDto {
 /**
 * アクセス・トークン
 */
 private String token;
 /**
 * トークンをリフレッシュする
 */
 private String refreshToken;
 /**
 * アクセストークンヘッダーのプレフィックス
 */
 private String tokenHead;
 /**
 * 有効時間
 */
 private int expiresIn;
}
  • Oauth2 デフォルトのログイン認証インターフェイスを実装した AuthController を作成します;
/**
 * Oauth2 Get Tokenインターフェースをカスタマイズする
 * Created by macro on 2020/7/17.
 */
@RestController
@RequestMapping("/oauth")
public class AuthController {
 @Autowired
 private TokenEndpoint tokenEndpoint;
 /**
 * Oauth2ログイン認証
 */
 @RequestMapping(value = "/token", method = RequestMethod.POST)
 public CommonResult<Oauth2TokenDto> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
 OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();
 Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder()
 .token(oAuth2AccessToken.getValue())
 .refreshToken(oAuth2AccessToken.getRefreshToken().getValue())
 .expiresIn(oAuth2AccessToken.getExpiresIn())
 .tokenHead("Bearer ").build();
 return CommonResult.success(oauth2TokenDto);
 }
}
  • ログイン認証インターフェースを再度呼び出すと、返される結果が一般的な返される結果の形式に合わせて変更されていることがわかります!

認証失敗の返送結果

  • 認証に成功した結果は統一され、認証に失敗した結果も統一されなければなりません;

  • ログイン認証のデフォルトの実装を注意深く見て見つけることができる、多くの失敗した認証操作は、直接OAuth2Exception例外をスローされ、コントローラでスローされる例外については、グローバルな処理のために@ControllerAdviceアノテーションを使用することができます;
/**
 * Oauth2がスローする例外のグローバル処理
 * Created by macro on 2020/7/17.
 */
@ControllerAdvice
public class Oauth2ExceptionHandler {
 @ResponseBody
 @ExceptionHandler(value = OAuth2Exception.class)
 public CommonResult handleOauth2(OAuth2Exception e) {
 return CommonResult.failed(e.getMessage());
 }
}
  • 間違ったパスワードが入力され、ログイン認証インターフェースが再度呼び出された場合、認証失敗の結果は一様であることがわかります。

カスタムゲートウェイの認証失敗結果

  • 期限切れまたは不正な署名のJWTトークンでパーミッションが必要なインターフェースにアクセスすると、ステータスコードが直接返されます;

  • このリターン結果は、一般的な結果フォーマットに準拠していないため、実際には、ステータスコードを返した後、以下のフォーマット情報を返します;
{
 "code": 401,
 "data": "Jwt expired at 2020-07-10T08:38:40Z",
 "message": "ログインしていないか、トークンの有効期限が切れている"
}
  • ResourceServerConfigServerAuthenticationEntryPointここで変更する非常に簡単な方法は、単にコードの行を追加し、ゲートウェイのセキュリティ設定を変更し、リソースサーバーを設定することができます;
/**
 * リソースサーバの構成
 * Created by macro on 2020/6/19.
 */
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
 private final AuthorizationManager authorizationManager;
 private final IgnoreUrlsConfig ignoreUrlsConfig;
 private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
 private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
 @Bean
 public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
 http.oauth2ResourceServer().jwt()
 .jwtAuthenticationConverter(jwtAuthenticationConverter());
 //JWTリクエストヘッダの期限切れや署名エラーの処理結果をカスタマイズする
 http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
 http.authorizeExchange()
 .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//ホワイトリストの設定
 .anyExchange().access(authorizationManager)//認証マネージャーの設定
 .and().exceptionHandling()
 .accessDeniedHandler(restfulAccessDeniedHandler)//認証されていない
 .authenticationEntryPoint(restAuthenticationEntryPoint)//認証されていない
 .and().csrf().disable();
 return http.build();
 }
}
  • 追加したら、パーミッションが必要なインターフェースに再度アクセスしてください。

互換性のあるホワイトリストのインターフェース

  • 実際には、ホワイトリストのインターフェイスのための問題は、有効期限が切れたまたは不正確に署名されたJWTトークンのアクセスを運ぶとき、それは直接トークンの有効期限切れの結果が返されますされている、あなたが試してログイン認証インターフェイスを訪問することができます;

  • 明らかにホワイトリストのインターフェイスであり、間違ったトークンを運ぶだけでアクセスを許可しませんが、明らかに少し不合理です。どのようにそれを解決するには、トークンなしでアクセスする方法を見てみましょう;

  • 実際には、Oauth2のデフォルトの認証フィルタの前に別のフィルタを追加するか、ホワイトリストに登録されたインターフェースの場合は、最初に定義された認証ヘッダを削除するだけです;
/**
 * ホワイトリストに登録されたパスにアクセスする際は、JWTリクエストヘッダを削除する。
 * Created by macro on 2020/7/24.
 */
@Component
public class IgnoreUrlsRemoveJwtFilter implements WebFilter {
 @Autowired
 private IgnoreUrlsConfig ignoreUrlsConfig;
 @Override
 public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
 ServerHttpRequest request = exchange.getRequest();
 URI uri = request.getURI();
 PathMatcher pathMatcher = new AntPathMatcher();
 //ホワイトリストのパス削除JWTリクエストヘッダ
 List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
 for (String ignoreUrl : ignoreUrls) {
 if (pathMatcher.match(ignoreUrl, uri.getPath())) {
 request = exchange.getRequest().mutate().header("Authorization", "").build();
 exchange = exchange.mutate().request(request).build();
 return chain.filter(exchange);
 }
 }
 return chain.filter(exchange);
 }
}
  • ResourceServerConfigで、このフィルタをデフォルトの認証フィルタに設定します;
/**
 * リソースサーバの構成
 * Created by macro on 2020/6/19.
 */
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
 private final AuthorizationManager authorizationManager;
 private final IgnoreUrlsConfig ignoreUrlsConfig;
 private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
 private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
 private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;
 @Bean
 public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
 http.oauth2ResourceServer().jwt()
 .jwtAuthenticationConverter(jwtAuthenticationConverter());
 //JWTリクエストヘッダの期限切れや署名エラーの処理結果をカスタマイズする
 http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
 //ホワイトリストに登録されたパスの場合、JWTリクエストヘッダを直接削除する
 http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);
 http.authorizeExchange()
 .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//ホワイトリストの設定
 .anyExchange().access(authorizationManager)//認証マネージャーの設定
 .and().exceptionHandling()
 .accessDeniedHandler(restfulAccessDeniedHandler)//認証されていない
 .authenticationEntryPoint(restAuthenticationEntryPoint)//認証されていない
 .and().csrf().disable();
 return http.build();
 }
}
  • 期限切れのリクエストヘッダで再度アクセスしてみると、すでに通常通りアクセスできることがわかりました。

まとめ

この時点で、マイクロサービスにおけるOauth2の使用による統一認証と認証スキームの実現がようやく完了しました!

Read next

アルゴリズムの複雑さO(1),O(n),O(logn),O(nlogn)の意味を説明した記事。

私はアルゴリズムの研究では、多くの仲間の開発者は、ソートはしばしばO、O、O、O、これらの複雑さに遭遇すると信じて、ここで疑問があるでしょう参照してください、最終的にこのOは何を表していますか?好奇心で今日の記事を開始します。 まずすべてのO、O、O、Oは、対応するアルゴリズムの時間の複雑さを表すために使用されます...

Feb 23, 2020 · 3 min read