Адрес личного блога:blog.sqdyy.cn
Архитектура Spring Security OAuth2
Spring Security OAuth2 — этоOAuth2
Библиотека классов, которая инкапсулируетAuthorization Server
,Resource Server
иClient
триSpring
Возможности, требуемые ролью приложения.Spring Security OAuth
нужно сSpring Framework(Spring MVC)
иSpring Security
Предоставленные функции работают вместе при использованииSpring Security OAuth
ПостроитьAuthorization Server
,Resource Server
иClient
в случае,Spring Security OAuth2
Общая схема архитектуры выглядит следующим образом:
- владелец ресурса через
UserAgent
доступclient
, где авторизация разрешает доступ к конечной точке авторизации,OAuth2RestTemplate
будет создаватьOAuth2
проверенныйREST
просить, инструктироватьUserAgent
перенаправить наAuthorization Server
Конечная точка авторизации дляAuthorizationEndpoint
. -
UserAgent
доступAuthorization Server
конечной точки авторизацииauthorize
метод, когда авторизация не зарегистрирована, конечной точке авторизации потребуется интерфейс авторизации/oauth/confirm_access
Отображается владельцу ресурса, владелец ресурса передаст авторизацию после авторизации.AuthorizationServerTokenServices
Создайте код авторизации или токен доступа, сгенерированный токен в конечном итоге пройдетuserAgent
Перенаправление передается клиенту. - Клиент
OAuth2RestTemplate
После получения кода авторизации создайте запрос на доступ к серверу авторизацииTokenEndpoint
конечная точка токена, конечная точка токена по вызовуAuthorizationServerTokenServices
Для проверки кода авторизации, предоставленного клиентом для авторизации, и выдачи клиенту ответа на токен доступа. - клиента
OAuth2RestTemplate
Добавьте токен доступа, полученный от сервера авторизации, в заголовок запроса для доступа к серверу ресурсов, и сервер ресурсов передастOAuth2AuthenticationManager
перечислитьResourceServerTokenServices
Проверьте маркер доступа и проверочную информацию, связанную с маркером доступа. После успешной проверки токена доступа он возвращается клиенту для запроса соответствующего ресурса.
объяснено выше
Spring Security OAuth2
Процесс выполнения трех ролей приложения, мы проанализируем архитектуру и исходный код этих трех ролей один за другим, чтобы углубить наше понимание.
Архитектура сервера авторизации
Сервер авторизации в основном предоставляет услугу аутентификации владельца ресурса.Клиент получает авторизацию от владельца ресурса через сервер авторизации, а затем получает токен, выданный сервером авторизации. В этом процессе аутентификации задействованы две важные конечные точки, одна из которых является конечной точкой авторизации.AuthorizationEndpoint
, другой — конечная точка токенаTokenEndpoint
. Далее будет проанализирован внутренний рабочий процесс этих двух конечных точек с помощью исходного кода.
Конечная точка авторизации
Сначала давайте посмотрим на конечную точку авторизации доступа.AuthorizationEndpoint
Процесс выполнения:
-
UserAgent
доступ к серверу авторизацииAuthorizationEndpoint
(Конечная точка авторизации) URI:/oauth/authorize
, который вызываетauthorize
метод, в основном используемый для определения того, был ли пользователь авторизован, если авторизован для выдачи нового кода авторизации, в противном случае перейти на страницу авторизации пользователя. -
authorize
он позвонит первымClientDetailsService
Получите информацию о клиенте и проверьте параметры запроса. - впоследствии
authorize
Затем метод передает параметры запроса вUserApprovalHandler
Используется для проверки, зарегистрирован ли клиент.scope
уполномоченный. - Когда авторизация не зарегистрирована, т.е.
approved
заfalse
, который покажет владельцу ресурса интерфейс для запроса авторизации/oauth/confirm_access
. - То же, что 4.
- URI конечной точки авторизации, через которую владелец ресурса снова получит доступ к серверу авторизации после подтверждения авторизации:
/oauth/authorize
, этот параметр запроса увеличится на единицуuser_oauth_approval
, поэтому вызывается другой метод картыapproveOrDeny
. -
approveOrDeny
позвонюuserApprovalHandler.updateAfterApproval
Определите, следует ли обновлять в зависимости от того, авторизован ли пользовательauthorizationRequest
в объектеapproved
Атрибуты. -
userApprovalHandler
Класс реализации по умолчанию:ApprovalStoreUserApprovalHandler
, который находится внутриApprovalStore
изaddApprovals
регистрировать авторизационную информацию.
Когда параметры запроса не переносятсяuser_oauth_approval
, посетитauthorize
метод, процесс выполнения соответствует вышеперечисленным шагам 1-5, если пользователь прошел авторизацию, будет выдан новыйauthorization_code
, иначе перейти на страницу авторизации пользователя:
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
// 根据请求参数封装 认证请求对象 ----> AuthorizationRequest
// Pull out the authorization request first, using the OAuth2RequestFactory.
// All further logic should query off of the authorization request instead of referring back to the parameters map.
// The contents of the parameters map will be stored without change in the AuthorizationRequest object once it is created.
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
// 获取请求参数中的response_type类型,并进行条件检验:response_type只支持token和code,即令牌和授权码
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}
// 请求参数必须携带客户端ID
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}
try {
// 在使用Spring Security OAuth2授权完成之前,必须先完成Spring Security对用户进行的身份验证
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
// 获取客户端详情
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
// 获得重定向URL,它可以来自请求参数,也可以来自客户端详情,总之你需要将它存储在授权请求中
// The resolved redirect URI is either the redirect_uri from the parameters or the one from clientDetails.
// Either way we need to store it on the AuthorizationRequest.
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException(
"A redirectUri must be either supplied or preconfigured in the ClientDetails");
}
authorizationRequest.setRedirectUri(resolvedRedirect);
// 根据客户端详情来校验请求参数中的scope
// We intentionally only validate the parameters requested by the client (ignoring any data that may have been added to the request by the manager).
oauth2RequestValidator.validateScope(authorizationRequest, client);
// 此处检测请求的用户是否已经被授权,或者有配置默认授权的权限;若已经有accessToke存在或者被配置默认授权的权限则返回含有授权的对象
// 用到userApprovalHandler ----> ApprovalStoreUserApprovalHandler
// Some systems may allow for approval decisions to be remembered or approved by default.
// Check for such logic here, and set the approved flag on the authorization request accordingly.
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
// TODO: is this call necessary?
// 如果authorizationRequest.approved为true,则将跳过Approval页面。
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// 已授权 直接返回对应的视图,返回的视图中包含新生成的authorization_code(固定长度的随机字符串)值
// Validation is all done, so we can check for auto approval...
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
// Place auth request into the model so that it is stored in the sessionfor approveOrDeny to use.
// That way we make sure that auth request comes from the session,
// so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
model.put("authorizationRequest", authorizationRequest);
// 未授权 跳转到授权界面,让用户选择是否授权
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}
}
Пользователь подтверждает, авторизоваться ли через страницу авторизации, и передает параметры запросаuser_oauth_approval
Получите доступ к конечной точке авторизации, которая будет выполнятьapproveOrDeny
метод, процесс выполнения соответствует шагам 6-7 выше:
@RequestMapping(value = "/oauth/authorize", method = RequestMethod.POST, params = OAuth2Utils.USER_OAUTH_APPROVAL)
public View approveOrDeny(@RequestParam Map<String, String> approvalParameters, Map<String, ?> model,
SessionStatus sessionStatus, Principal principal) {
// 在使用Spring Security OAuth2授权完成之前,必须先完成Spring Security对用户进行的身份验证
if (!(principal instanceof Authentication)) {
sessionStatus.setComplete();
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorizing an access token.");
}
// 获取请求参数
AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest");
if (authorizationRequest == null) {
sessionStatus.setComplete();
throw new InvalidRequestException("Cannot approve uninitialized authorization request.");
}
try {
// 获取请求参数中的response_type类型
Set<String> responseTypes = authorizationRequest.getResponseTypes();
// 设置Approval的参数
authorizationRequest.setApprovalParameters(approvalParameters);
// 根据用户是否授权,来决定是否更新authorizationRequest对象中的approved属性。
authorizationRequest = userApprovalHandler.updateAfterApproval(authorizationRequest,
(Authentication) principal);
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// 需要携带重定向URI
if (authorizationRequest.getRedirectUri() == null) {
sessionStatus.setComplete();
throw new InvalidRequestException("Cannot approve request when no redirect URI is provided.");
}
// 用户拒绝授权,响应错误信息到客户端的重定向URL上
if (!authorizationRequest.isApproved()) {
return new RedirectView(getUnsuccessfulRedirect(authorizationRequest,
new UserDeniedAuthorizationException("User denied access"), responseTypes.contains("token")),
false, true, false);
}
// 简化模式,直接颁发访问令牌
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest).getView();
}
// 授权码模式,生成授权码存储并返回给客户端
return getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal);
}
finally {
sessionStatus.setComplete();
}
}
TokenEndpoint
Далее мы смотрим на конечную точку токенаTokenEndpoint
Процесс выполнения:
-
userAgent
Получив доступ к URI конечной точки токена сервера авторизации TokenEndpoint:/oauth/token
, который вызываетpostAccessToken
метод, в основном используемый для созданияToken
. -
postAccessToken
позвонит первымClientDetailsService
Получите информацию о клиенте и проверьте параметры запроса. - Вызовите соответствующий режим авторизации для достижения генерации класса
Token
. - Реализованы соответствующие режимы авторизации
AbstractTokenGranter
абстрактный класс, его членыAuthorizationServerTokenServices
Может использоваться для создания, обновления, полученияtoken
. -
AuthorizationServerTokenServices
Класс реализации по умолчанию толькоDefaultTokenServices
,пропусти это的createAccessToken
способ можно посмотретьtoken
как он был создан. - реальная операция
token
КлассTokenStore
, процедура основана наTokenStore
Различные реализации интерфейса для производства и храненияtoken
.
Перечислено нижеTokenEndpoint
URI:/oauth/token
Анализ исходного кода:
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
// 在使用Spring Security OAuth2授权完成之前,必须先完成Spring Security对用户进行的身份验证
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
// 通过客户端Id获取客户端详情
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
// 根据请求参数封装 认证请求对象 ----> AuthorizationRequest
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) {
// 根据客户端详情来校验请求参数中的scope,防止客户端越权获取更多权限
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");
}
// 如果grant_type=authoraztion_code,则清空scope
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());
}
}
// 如果grant_type=refresh_token,设置刷新令牌的scope
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)));
}
// 为客户端生成token
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
Самое важное в конечной точке токена — это то, как создатьtoken
, различные режимы авторизации будут основаны наAbstractTokenGranter
Интерфейс реализован иначе,AbstractTokenGranter
доверитAuthorizationServerTokenServices
создавать, обновлять, получатьtoken
.AuthorizationServerTokenServices
Реализация по умолчанию толькоDefaultTokenServices
, просто извлеките егоcreateAccessToken
Исходный код метода можно увидеть:
// 生成accessToken和RefreshToken
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
// 首先尝试获取当前存在的Token
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
// 如果现有的访问令牌accessToken不为空且没有失效,则保存现有访问令牌, 如果失效则重新存储新的访问令牌
if (existingAccessToken != null) {
if (existingAccessToken.isExpired()) {
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);
}
else {
// Re-store the access token in case the authentication has changed
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}
// 如果没有刷新令牌则创建刷新令牌,如果刷新令牌过期,重新创建刷新令牌。
// Only create a new refresh token if there wasn't an existing one associated with an expired access token.
// Clients might be holding existing refresh tokens, so we re-use it in the case that the old access token expired.
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}
// But the refresh token itself might need to be re-issued if it has expired.
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
// 生成新的访问令牌并储存,保存刷新令牌
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
Архитектура сервера ресурсов
Сервер ресурсов в основном используется для обработки запроса клиента на доступ к защищенному ресурсу и возврата соответствующего ответа. Сервер ресурсов проверяет допустимость маркера доступа клиента и получает информацию об аутентификации, связанную с маркером доступа. После получения информации об аутентификации убедитесь, что токен доступа разрешен.scope
Внутри поведение обработки после завершения проверки может быть реализовано аналогично обычному приложению. Ниже приведен запущенный процесс сервера ресурсов:
- (1) Когда клиент начинает обращаться к серверу ресурсов, он сначала проходит через
OAuth2AuthenticationProcessingFilter
, роль этого перехватчика состоит в том, чтобы извлечь токен доступа из запроса, а затем извлечь аутентификационную информацию из токена.Authentication
и сохранить его в контексте. - (2)
OAuth2AuthenticationProcessingFilter
Перехватчик будет звонитьAuthenticationManager的authenticate
Способ извлечения информации аутентификации. - (2·)
OAuth2AuthenticationProcessingFilter
Если произойдет ошибка аутентификации, перехватчик托AuthenticationEntryPoint
Сделайте ответ об ошибке, класс реализации по умолчаниюOAuth2AuthenticationEntryPoint
. - (3)
OAuth2AuthenticationProcessingFilter
Перейти к следующему фильтру безопасности после завершения выполненияExceptionTranslationFilter
. - (3·)
ExceptionTranslationFilter
Фильтр используется для обработки исключений, возникающих в процессе аутентификации и авторизации системы.Если в перехватчике возникает исключение, он делегируетAccessDeniedHandler
Сделайте ответ об ошибке, класс реализации по умолчаниюOAuth2AccessDeniedHandler
. - (4) Когда проверка аутентификации/авторизации запроса прошла успешно, вернуть соответствующий ресурс, который должен запросить клиент.
Сервер ресурсов, о котором нам нужно позаботиться, — это то, как он проверяет, что токен доступа клиента действителен, поэтому мы начнем сOAuth2AuthenticationProcessingFilter
Начиная с исходного кода, роль этого перехватчика состоит в том, чтобы извлечь токен доступа из запроса, а затем извлечь аутентификационную информацию из токена.Authentication
и поместите это в контекст:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
boolean debug = logger.isDebugEnabled();
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
try {
// 从请求中提取token,然后再提取token中的认证信息Authorization
Authentication authentication = this.tokenExtractor.extract(request);
if (authentication == null) {
if (this.stateless && this.isAuthenticated()) {
if (debug) {
logger.debug("Clearing security context.");
}
SecurityContextHolder.clearContext();
}
if (debug) {
logger.debug("No token in request, will continue chain.");
}
} else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
//获取token携带的认证信息Authentication并进行验证,然后存到spring security的上下文,以供后续使用
Authentication authResult = this.authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
this.eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
}
} catch (OAuth2Exception var9) {
SecurityContextHolder.clearContext();
if (debug) {
logger.debug("Authentication request failed: " + var9);
}
this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9));
return;
}
chain.doFilter(request, response);
}
В приведенном выше коде упоминаетсяOauth2AuthenticationManager
получитеtoken
Переносимая информация аутентификации аутентифицируется.Из исходного кода мы можем знать, что он в основном выполняет 3 шага:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
String token = (String) authentication.getPrincipal();
// 1.通过token获取OAuuth2Authentication对象
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
// 2.验证资源服务的ID是否正确
Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
}
// 3.验证客户端的访问范围(scope)
checkClientDetails(auth);
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
// Guard against a cached copy of the same details
if (!details.equals(auth.getDetails())) {
// Preserve the authentication details from the one loaded by token services
details.setDecodedDetails(auth.getDetails());
}
}
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}
После проверки, послеExceptionTranslationFilter
фильтр для доступа к ресурсу.
Клиентская архитектура
Spring security OAuth2
клиентские элементы управленияOAuth 2.0
Права доступа к защищенным ресурсам других серверов. Конфигурация включает в себя установление соединения между связанным защищенным ресурсом и пользователем, имеющим разрешение на доступ к ресурсу. В клиенте также необходимо реализовать функцию хранения кода авторизации пользователя и токена доступа.
Структура клиентского кода не отличается особой сложностью, вот описание схемы архитектуры, если интересно, можете изучить исходный код по описанному ниже процессу:
- во-первых
UserAgent
звонящий клиентController
, который пройдет раньшеOAuth2ClientContextFilter
фильтр, который в основном используется для захвата того, что может произойти на шаге 5UserRedirectRequiredException
, чтобы перенаправить на сервер авторизации для повторной авторизации. - Необходимо внедрить код, относящийся к уровню обслуживания клиента.
RestOperations->OAuth2RestOperations
Класс реализации интерфейсаOAuth2RestTemplate
. В основном он обеспечивает доступ к серверу авторизации или серверу ресурсов.RestAPI
. -
OAuth2RestTemplate
членOAuth2ClientContext
Класс реализации интерфейсаDefaultOAuth2ClientContext
. Он проверит, действителен ли токен доступа, и, если он действителен, выполнит шаг 6, чтобы получить доступ к серверу ресурсов. - Если токен доступа не существует или срок его действия истек, вызовите
AccessTokenProvider
для получения токена доступа. -
AccessTokenProvider
Получите токен доступа в соответствии с определенными сведениями о ресурсе и типом авторизации, если нет, бросьтеUserRedirectRequiredException
. - Укажите токен доступа, полученный в 3 или 5, для доступа к серверу ресурсов. Если во время доступа возникает исключение срока действия маркера, инициализируйте сохраненный маркер доступа, а затем перейдите к шагу 4.
Схема архитектуры и часть содержания в тексте взяты изРуководство по разработке TERASOLUNA Server Framework (5.x),Если воспроизводится, укажите источник.