Проектирование и реализация аутентификации, аутентификации и управления разрешениями API в микросервисной архитектуре (2)

задняя часть Микросервисы

Введение: эта статья является второй в серии «Проектирование и реализация аутентификации, аутентификации и управления разрешениями API в микросервисной архитектуре», посвященной конкретной реализации аутентификации удостоверения пользователя и выдачи токена. Эта статья длинная и анализирует большую часть задействованного кода. Ее можно собрать и прочитать в свободное время. Добро пожаловать, чтобы подписаться на эту серию статей.

1. Обзор системы

в предыдущем постеПроектирование и реализация аутентификации, аутентификации и управления разрешениями API в микросервисной архитектуре (1)Вводятся предыстория проекта, технические исследования и окончательный выбор, а также отображается окончательный результат реализации конечной точки. Хотя архитектура системы упоминается, подробная блок-схема не приводится. В авторском сценарии приложения система Auth совмещена со шлюзом. Настройте соответствующую информацию о конечной точке на шлюзе, например, вход в систему для подачи заявки на авторизацию токена, проверку check_token и другие конечные точки.

На следующем рисунке показана блок-схема комбинации шлюза и системы аутентификации Конкретные детали реализации системы шлюза будут представлены в отдельной статье позже. (На рисунке блок-схемы здесь автор использует минималистский язык для описания, все ученики слегка распыляют 😆!)

login
login

На приведенном выше рисунке показан простой процесс входа в систему, и детали опущены.Проверка достоверности информации о пользователе фактически вызывает систему пользователя. Общий процесс выглядит следующим образом: после того, как клиентский запрос достигает шлюза, он входит в конечную точку в соответствии с запросом, идентифицированным шлюзом, и перенаправляет его в систему аутентификации для проверки информации пользователя.

Еще один аспект — проверка общих запросов. Некоторые общедоступные интерфейсы, не требующие разрешений, настраиваются на шлюзе. После того, как запрос достигнет шлюза, соответствующий путь будет освобожден напрямую. Если запрос необходимо проверить, будет перехвачена соответствующая проверочная информация запроса, а также контекстная информация, необходимая для проверки разрешения API (проект автора выполняет предавторизационную проверку для некоторых операций, которые будут рассмотрены в следующем главу) и вызов системы аутентификации, переадресация маршрута после успешной проверки.

gw
gw

Эта статья посвящена нашемупервый разАутентификация удостоверения пользователя и выдача токена, упомянутые в статье. Это также в основном включает два аспекта:

  • Аутентификация легитимности пользователя
  • Получить авторизованный токен

2. Конфигурация и диаграмма классов

2.1 Основная конфигурация AuthorizationServer

оAuthorizationServerа такжеResourceServerКонфигурация была указана в предыдущей статье.AuthorizationServerв основном унаследованоAuthorizationServerConfigurerAdapter, переопределяя три метода интерфейса его реализации:

    //对应于配置AuthorizationServer安全认证的相关信息,创建ClientCredentialsTokenEndpointFilter核心过滤器
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { 
    }

    //配置OAuth2的客户端相关信息
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    }

    //配置身份认证器,配置认证方式,TokenStore,TokenGranter,OAuth2RequestFactory
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    }

2.2 ОсновноеAuthenticationдиаграмма классов

auth
auth

Основной метод проверкиauthenticate(Authentication authentication)в интерфейсеAuthenticationManager, его класс реализации имеетProviderManager, это видно из рисунка вышеProviderManagerзависит отAuthenticationProviderинтерфейс, который определяетList<AuthenticationProvider>глобальные переменные. Автор реализует класс реализации этого интерфейса здесьCustomAuthenticationProvider. пользовательскийprovider, И вGlobalAuthenticationConfigurerAdapterНастройте и измените пользовательскую проверку вprovider, приоритетconfigure()метод.

@Configuration
public class AuthenticationManagerConfig extends GlobalAuthenticationConfigurerAdapter {

    @Autowired
    CustomAuthenticationProvider customAuthenticationProvider;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(customAuthenticationProvider);//使用自定义的AuthenticationProvider
    }

}

AuthenticationManagerBuilderиспользуется для созданияAuthenticationManager, что позволяет настраивать несколько способовAuthenticationProvider, такие как LDAP, на основе JDBC и т. д.

3. Токен аутентификации и авторизации

Ниже описаны основные классы и интерфейсы токенов аутентификации и авторизации.

3.1 Встроенные конечные точкиTokenEndpoint

Основные конечные точки, связанные с токенами, встроены в пакет jar, предоставляемый Spring-Security-Oauth2. В этой статье токен аутентификации и авторизации и/oauth/tokenСвязанный, класс интерфейса, который он обрабатывает,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 {
    //首先对client信息进行校验
        if (!(principal instanceof Authentication)) {
            throw new InsufficientAuthenticationException(
                    "There is no client authentication. Try adding an appropriate authentication filter.");
        }

        String clientId = getClientId(principal);
        //根据请求中的clientId,加载client的具体信息
        ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

        TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

        ... 

        //验证scope域范围
        if (authenticatedClient != null) {
            oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
        }
        //授权方式不能为空
        if (!StringUtils.hasText(tokenRequest.getGrantType())) {
            throw new InvalidRequestException("Missing grant type");
        }
        //token endpoint不支持Implicit模式
        if (tokenRequest.getGrantType().equals("implicit")) {
            throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
        }
        ...

        //进入CompositeTokenGranter,匹配授权模式,然后进行password模式的身份验证和token的发放
        OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
        if (token == null) {
            throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
        }

        return getResponse(token);

    }
    ...
}

client
client

Код аннотирован выше, читатели могут посмотреть, если им интересно. Основной процесс обработки интерфейса заключается в проверке того, является ли информация аутентификации законной. Если она является незаконной, сразу создается исключение, а затем обрабатывается запрошенный GrantType.В соответствии с GrantType выполняется аутентификация в режиме пароля и выдача токена.
участие здесьgetTokenGranter(), код также указан ниже:

public class CompositeTokenGranter implements TokenGranter {

    //GrantType的集合,有五种,之前有讲
    private final List<TokenGranter> tokenGranters;

    public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
        this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
    }

    //遍历list,匹配到相应的grantType就进行处理
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        for (TokenGranter granter : tokenGranters) {
            OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
            if (grant!=null) {
                return grant;
            }
        }
        return null;
    }
    ...
}

Этот запрос использует режим пароля, а затем входит в конкретный поток обработки своего GrantType.grant()метод.

    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

        if (!this.grantType.equals(grantType)) {
            return null;
        }

        String clientId = tokenRequest.getClientId();
        //加载clientId对应的ClientDetails,为了下一步的验证
        ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        //再次验证clientId是否拥有该grantType模式,安全
        validateGrantType(grantType, client);
        //获取token
        return getAccessToken(client, tokenRequest);

    }

    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
    //进入创建token之前,进行身份验证
        return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    }

    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
    //身份验证
        OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, null);
    }

Приведенный выше фрагмент кодаgrant()Детали реализации конкретного метода. GrantType соответствует соответствующемуgrant()После этого выполните базовую проверку для обеспечения безопасности, а затем войдите в основной процесс, который заключается в проверке личности и выпуске токенов в следующих разделах.

3.2 Пользовательские классы проверкиCustomAuthenticationProvider

CustomAuthenticationProviderКонкретная реализация метода проверки определена в . Его конкретная реализация заключается в следующем.

    //主要的自定义验证方法
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();
        Map data = (Map) authentication.getDetails();
        String clientId = (String) data.get("client");
        Assert.hasText(clientId,"clientId must have value" );
        String type = (String) data.get("type");
        //通过调用user服务,校验用户信息
        Map map = userClient.checkUsernameAndPassword(getUserServicePostObject(username, password, type));
            //校验返回的信息,不正确则抛出异常,授权失败
        String userId = (String) map.get("userId");
        if (StringUtils.isBlank(userId)) {
            String errorCode = (String) map.get("code");
            throw new BadCredentialsException(errorCode);
        }
        CustomUserDetails customUserDetails = buildCustomUserDetails(username, password, userId, clientId);
        return new CustomAuthenticationToken(customUserDetails);
    }
    //构造一个CustomUserDetails,简单,略去
    private CustomUserDetails buildCustomUserDetails(String username, String password, String userId, String clientId) {
    }
    //构造一个请求userService的map,内容略
    private Map<String, String> getUserServicePostObject(String username, String password, String type) {
    }

authenticate()Наконец, верните построенный пользовательскийCustomAuthenticationToken,существуетCustomAuthenticationTokenв, будетboolean authenticatedЕсли установлено значение true, проверка информации о пользователе прошла успешно. Здесь переданы параметрыCustomUserDetailsЭто связано с генерацией токенов, как информация в полезной нагрузке, о которой будет сказано ниже.

//继承抽象类AbstractAuthenticationToken
public class CustomAuthenticationToken extends AbstractAuthenticationToken {
    private CustomUserDetails userDetails;
    public CustomAuthenticationToken(CustomUserDetails userDetails) {
        super(null);
        this.userDetails = userDetails;
        super.setAuthenticated(true);
    }
    ...
}

а такжеAbstractAuthenticationTokenреализует интерфейсAuthentication和CredentialsContainer, конкретную информацию внутри читатель может посмотреть в исходном коде.

3.3 О JWT

После завершения проверки информации о пользователе следующим шагом является авторизация пользователя. Прежде чем говорить о конкретной авторизации, давайте добавим немного информации о токене JWT.

Веб-токен Json (JWT) — это открытый стандарт на основе JSON (RFC 7519), реализованный для передачи утверждений между средами веб-приложений. Маркер разработан, чтобы быть компактным и безопасным, особенно подходящим для сценариев единого входа (SSO) для распределенных сайтов. Заявки JWT обычно используются для передачи аутентифицированной информации об удостоверении пользователя между поставщиками удостоверений и поставщиками услуг для получения ресурсов с серверов ресурсов, а также могут добавлять некоторую дополнительную информацию о заявке, необходимую для другой бизнес-логики.Можно использовать непосредственно для проверки подлинности или зашифровать.

Из приведенного выше описания мы можем увидеть определение JWT, Здесь читатели могут сравнить разницу между аутентификацией с помощью токена и традиционной аутентификацией сеанса. Рекомендовать статьюЧто такое JWT -- ВЕБ-ТОКЕН JSON, автор не будет здесь подробно останавливаться, а лишь кратко представит его состав.

JWT состоит из трех частей: заголовок заголовка, полезная информация и подпись подписи. Ниже приведен пример сгенерированного access_token в предыдущей статье.

  • header
    Заголовок jwt содержит две части информации: одна — это тип объявления, здесь jwt, а другая — алгоритм шифрования объявления, который обычно напрямую использует HMAC SHA256. Первая часть обычно фиксируется как:
    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
  • playload
    Сохраненная действительная информация, эта действительная информация состоит из трех частей: декларации, зарегистрированной в стандарте, общедоступной декларации и частной декларации. Дополнительная информация, добавленная автором здесь,X-KEETS-UserIdа такжеX-KEETS-ClientId. Читатели могут настроить в соответствии с фактическими потребностями проекта. Окончательный результат кодирования base64 playload:

      eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cCI6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2VldHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOiJmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQ
  • signature
    Третья часть jwt — это информация о визе, которая состоит из трех частей: заголовка (после base64), полезной нагрузки (после base64) и секрета.
    Что касается секрета, то внимательные читатели могут обнаружить, что в предыдущей конфигурации есть определенные настройки. Строка, образованная конкатенацией первых двух частей, шифруется методом шифрования, объявленным в заголовке, с помощью секретной комбинации с солью, а затем составляет третью часть jwt. Результаты третьей части:

    5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdo

Что касается конкретного метода нанесения, вы можете обратиться к конструкции в первой статье./logoutконечная точка.

3.3 ПользовательскийAuthorizationTokenServices

Теперь пришло время создать токен для пользователя, который в основном связан с пользовательским интерфейсом.AuthorizationServerTokenServicesСвязанный.AuthorizationServerTokenServicesСуществует три основных метода:

    //创建token
    OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
    //刷新token
    OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
            throws AuthenticationException;
    //获取token
    OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

Из-за нехватки места автор здесь толькоcreateAccessToken()Анализируется метод реализации, реализуются другие методы.Читатели могут следить за проектом автора на GitHub.

public class CustomAuthorizationTokenServices implements AuthorizationServerTokenServices, ConsumerTokenServices {
    ...

    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        //通过TokenStore,获取现存的AccessToken
        OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken;
        //移除已有的AccessToken和refreshToken
        if (existingAccessToken != null) {
            if (existingAccessToken.getRefreshToken() != null) {
                refreshToken = existingAccessToken.getRefreshToken();
                // The token store could remove the refresh token when the
                    // access token is removed, but we want to be sure
                tokenStore.removeRefreshToken(refreshToken);
            }
            tokenStore.removeAccessToken(existingAccessToken);
        }
        //recreate a refreshToken
        refreshToken = createRefreshToken(authentication);

        OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
        if (accessToken != null) {
            tokenStore.storeAccessToken(accessToken, authentication);
        }
        refreshToken = accessToken.getRefreshToken();
        if (refreshToken != null) {
            tokenStore.storeRefreshToken(refreshToken, authentication);
        }
        return accessToken;
    }
    ...
}

Конкретная реализация здесь аннотирована выше, и в основном нет переписывания Читатели могут обратиться к исходному коду здесь.createAccessToken()Также вызываются два частных метода, создающих accessToken и refreshToken соответственно. Чтобы создать accessToken, он должен быть основан на refreshToken.
Здесь вы можете настроить срок действия токена.Создание accessToken реализовано следующим образом:

     private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.

    private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.

    private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
    //对应tokenId,存储的标识
        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
        int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
        if (validitySeconds > 0) {
            token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
        }
        token.setRefreshToken(refreshToken);
        //scope对应作用范围
        token.setScope(authentication.getOAuth2Request().getScope());
        //上一节介绍的自定义TokenEnhancer,这边使用
        return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
    }

Поскольку упоминаетсяTokenEnhancer, вставьте код сюда.

public class CustomTokenEnhancer extends JwtAccessTokenConverter {

    private static final String TOKEN_SEG_USER_ID = "X-KEETS-UserId";
    private static final String TOKEN_SEG_CLIENT = "X-KEETS-ClientId";

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
                                     OAuth2Authentication authentication) {
        CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();

        Map<String, Object> info = new HashMap<>();
        //从自定义的userDetails中取出UserId
        info.put(TOKEN_SEG_USER_ID, userDetails.getUserId());

        DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
        customAccessToken.setAdditionalInformation(info);

        OAuth2AccessToken enhancedToken = super.enhance(customAccessToken, authentication);
        //设置ClientId
        enhancedToken.getAdditionalInformation().put(TOKEN_SEG_CLIENT, userDetails.getClientId());

        return enhancedToken;
    }

}

С тех пор проверка личности пользователя и выдача токенов авторизации прекратились. Окончательный результат успешно возвращен:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsImV4cCI6MTUwODQ0Nzc1NiwidXNlcl9uYW1lIjoia2VldHMiLCJqdGkiOiJiYWQ3MmIxOS1kOWYzLTQ5MDItYWZmYS0wNDMwZTdkYjc5ZWQiLCJjbGllbnRfaWQiOiJmcm9udGVuZCIsInNjb3BlIjpbImFsbCJdfQ.5ZNVN8TLavgpWy8KZQKArcbj7ItJLLaY1zBRaAgMjdo",   
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJYLUtFRVRTLVVzZXJJZCI6ImQ2NDQ4YzI0LTNjNGMtNGI4MC04MzcyLWMyZDYxODY4ZjhjNiIsInVzZXJfbmFtZSI6ImtlZXRzIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImJhZDcyYjE5LWQ5ZjMtNDkwMi1hZmZhLTA0MzBlN2RiNzllZCIsImV4cCI6MTUxMDk5NjU1NiwianRpIjoiYWE0MWY1MjctODE3YS00N2UyLWFhOTgtZjNlMDZmNmY0NTZlIiwiY2xpZW50X2lkIjoiZnJvbnRlbmQifQ.mICT1-lxOAqOU9M-Ud7wZBb4tTux6OQWouQJ2nn1DeE",
    "expires_in": 43195,
    "scope": "all",
    "X-KEETS-UserId": "d6448c24-3c4c-4b80-8372-c2d61868f8c6",
    "jti": "bad72b19-d9f3-4902-affa-0430e7db79ed",
    "X-KEETS-ClientId": "frontend"
}

4. Резюме

В начале этой статьи дан обзор системы аутентификации и нарисована краткая блок-схема входа в систему и проверки, чтобы читатели могли иметь общее представление о реализации системы. Затем в основном объясняется конкретная реализация аутентификации удостоверения пользователя и выдачи токена. Проанализированы и объяснены основные классы и интерфейсы. В следующей статье в основном объясняется проверка подлинности токена и проверка разрешений контекста на уровне API.

Адрес источника этой статьи:
Гитхаб:GitHub.com/Доступный ETS2012/A…
Облако кода:git ee.com/can-ets/au-th-…

Подписывайтесь на свежие статьи, приглашаю обратить внимание на мой публичный номер

微信公众号
Публичный аккаунт WeChat


Ссылаться на

  1. Что такое JWT -- ВЕБ-ТОКЕН JSON
  2. Re: Spring Security OAuth2 с нуля (2)

Связанное Чтение

Проектирование и реализация аутентификации, аутентификации и управления разрешениями API в микросервисной архитектуре (1)