Адрес фактического центра электронной коммерции SpringBoot (35k+star):GitHub.com/macro-positive/…
Резюме
существует«Идеальное решение для разрешений микросервисов, Spring Cloud Gateway + Oauth2 реализует унифицированную аутентификацию и аутентификацию! 》В этой статье мы рассказали об использовании Oauth2 в микросервисах, но не настроили результаты обработки Oauth2 по умолчанию. Иногда мы действительно надеемся, что аутентификация и авторизация в Oauth2 могут вернуть результат в указанном нами формате, например, результат аутентификации при входе в систему, результат сбоя аутентификации шлюза и так далее. В этой статье будет подробно представлена схема кастомной обработки результатов в Oauth2, надеюсь она будет всем полезна!
какая проблема
Настройка результатов обработки OAUTH2 в основном для унифицирования формата возвращенной информации от интерфейса, начиная с следующих аспектов.
- Настраиваемый результат проверки подлинности при входе в систему OAUTH2 и результат отказа;
- Срок действия токена JWT истек, или подпись неверна, и аутентификация шлюза не возвращает результат;
- При доступе к интерфейсу белого списка с просроченным или неправильно подписанным токеном JWT шлюзу не удается пройти прямую аутентификацию.
Настроить результаты аутентификации при входе
Аутентификация прошла успешно и результат возвращен
- Давайте сначала посмотрим на результат возврата по умолчанию и получим доступ к интерфейсу аутентификации входа Oauth2:http://localhost:9201/auth/oauth/token
- То, что мы использовали раньше, — это унифицированный общий возвращаемый результат.
CommonResult
, результат Oauth2 явно противоречив, и его необходимо унифицировать Общий формат возвращаемого результата следующий:
/**
* 通用返回对象
* Created by macro on 2019/4/19.
*/
public class CommonResult<T> {
private long code;
private String message;
private T data;
}
- Фактически, мы можем настроить интерфейс аутентификации при входе в систему Oauth2, если найдем ключевой класс, который
org.springframework.security.oauth2.provider.endpoint.TokenEndpoint
, который определяет хорошо знакомый нам интерфейс аутентификации при входе. Нам нужно только переписать интерфейс аутентификации при входе, напрямую вызвать логику реализации по умолчанию, а затем обработать результат, возвращаемый по умолчанию. Ниже приведена логика реализации по умолчанию;
@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
атрибут;
/**
* Oauth2获取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;
}
- Создавать
AuthController
, настроить интерфейс аутентификации входа Oauth2 по умолчанию;
/**
* 自定义Oauth2获取令牌接口
* 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
исключение, дляController
Выброшенное исключение, мы можем использовать@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 для доступа к интерфейсу, требующему разрешений, код состояния будет возвращен напрямую.
401
;
- Этот возвращаемый результат не соответствует нашему общему формату результатов. Фактически, мы хотим вернуть код состояния как
200
, а затем вернуть следующую информацию о формате;
{
"code": 401,
"data": "Jwt expired at 2020-07-10T08:38:40Z",
"message": "暂未登录或token已经过期"
}
- Вот очень простая модификация, просто добавьте строку кода, чтобы изменить конфигурацию безопасности шлюза.
ResourceServerConfig
, установите сервер ресурсовServerAuthenticationEntryPoint
Ты сможешь;
/**
* 资源服务器配置
* 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 в микросервисах для реализации унифицированной аутентификации и решений для аутентификации, наконец, завершено!