@startuml token-structure !include ./class-settings.puml interface AuthenticationToken { + Object getPrincipal() + Object getCredentials() } interface HostAuthenticationToken { + String getHost() } interface RememberMeAuthenticationToken { + boolean isRememberMe() } class UsernamePasswordToken { - String username - char[] password - boolean rememberMe - String host + void clear() } class BearerToken { - String token - String host } HostAuthenticationToken --|> AuthenticationToken RememberMeAuthenticationToken --|> AuthenticationToken UsernamePasswordToken ..|> HostAuthenticationToken UsernamePasswordToken ..|> RememberMeAuthenticationToken BearerToken ..|> HostAuthenticationToken @enduml @startuml subject-structure !include ./class-settings.puml interface Subject { + Object getPrincipal() + PrincipalCollection getPrincipals() + boolean isPermitted(String) + boolean isPermitted(Permission) + boolean[] isPermitted(String...) + boolean{} isPermitted(List) + boolean isPermittedAll(String...) + boolean isPermittedAll(Collection) + void checkPermission(String) + void checkPermission(Permission) + void checkPermissions(String...) + void checkPermissions(Collection) + boolean hasRole(String roleIdentifier) + boolean hasRoles(List) + boolean hasAllRoles(Collection) + void checkRole(String) + void checkRoles(Collection) + void checkRoles(String...) + void login(AuthenticationToken) + boolean isAuthenticated() + boolean isRemembered() + Session getSession() + Session getSession(boolean) + void logout() + V execute(Callable) + void execute(Runnable) + Callable associateWith(Callable) + Runnable associateWith(Runnable) + void runAs(PrincipalCollection) + boolean isRunAs() + PrincipalCollection getPreviousPrincipals() + PrincipalCollection releaseRunAs() } interface PrincipalCollection { + Object getPrimaryPrincipal() + T oneByType(Class) + Collection byType(Class) + List asList() + Set asSet() + Collection fromRealm(String) + Set getRealmNames() + boolean isEmpty() } interface MutablePrincipalCollection { + void add(Object, String) + void addAll(Collection, String) + void addAll(PrincipalCollection) + void clear() } class SimplePrincipalCollection interface PrincipalMap { + Map getRealmPrincipals(String) + Map setRealmPrincipals(String, Map) + Object setRealmPrincipal(String, String, Object) + Object getRealmPrincipal(String, String) + Object removeRealmPrincipal(String, String) } class SimplePrincipalMap interface SubjectContext { + SecurityManager getSecurityManager() + void setSecurityManager(SecurityManager) + SecurityManager resolveSecurityManager() + Serializable getSessionId() + void setSessionId(Serializable) + Subject getSubject() + void setSubject(Subject subject) + PrincipalCollection getPrincipals() + PrincipalCollection resolvePrincipals() + void setPrincipals(PrincipalCollection) + Session getSession() + void setSession(Session) + Session resolveSession() + boolean isAuthenticated() + void setAuthenticated(boolean) + boolean isSessionCreationEnabled() + void setSessionCreationEnabled(boolean) + boolean resolveAuthenticated() + AuthenticationInfo getAuthenticationInfo() + void setAuthenticationInfo() + AuthenticationToken getAuthenticationToken() + void setAuthenticationToken(AuthenticationToken) + String getHost() + void setHost(String) + String resolveHost() } PrincipalCollection -* Subject MutablePrincipalCollection --|> PrincipalCollection MutablePrincipalCollection <|. SimplePrincipalCollection PrincipalCollection <|-- PrincipalMap PrincipalMap <|. SimplePrincipalMap Subject -* SubjectContext PrincipalCollection -* SubjectContext Session --* SubjectContext SubjectContext *-- AuthenticaitonInfo SubjectContext *-- AuthenticaitonToken @enduml @startuml securitymanager-structure !include ./class-settings.puml interface SecurityManager { + Subject login(Subject, AuthenticationToken) + void logout(Subject) + Subject createSubject(SubjectContext) } interface Authorizer { + boolean isPermitted(PrincipalCollection, String) + boolean isPermitted(PrincipalCollection, Permission) + boolean[] isPermitted(PrincipalCollection, String...) + boolean[] isPermitted(PrincipalCollection, List) + boolean isPermittedAll(PrincipalCollection, String...) + boolean isPermittedAll(PrincipalCollection, Collection...) + void checkPermission(PrincipalCollection, String) + void checkPermission(PrincipalCollection, Permission) + void checkPermissions(PrincipalCollection, String...) + void checkPermissions(PrincipalCollection, Collection) + boolean hasRole(PrincipalCollection, String) + boolean[] hasRoles(PrincipalCollection, List) + boolean hasAllRoles(PrincipalCollection, Collection) + void checkRole(PrincipalCollection, String) + void checkRoles(PrincipalCollection, Collection) + void checkRoles(PrincipalCollection, String...) } interface Authenticator { + AuthenticaionInfo authenticate(AuthenticationToken) } interface SessionManager { + Session start(SessionContext) + Session getSession(SessionKey) } interface SessionKey { + Serializable getSessionId() } interface SessionContext { + void setHost(String) + String getHost() + Serializable getSessionId() + void setSessionId(Serializable) } abstract class CachingSecurityManager { - CacheManager cacheManager - EventBus eventBus # void afterCacheManagerSet() # void applyEventBusToCacheManager() # void afterEventBusSet() + void destroy() } interface CacheManagerAware { + void setCacheManager(CacheManager) } interface EventBusAware { + void setEventBus(EventBus) } interface CacheManager { + Cache getCache(String) } interface Cache { + V get(K) + V put(K, V) + V remove(K) + void clear() + int size() + Set keys() + Collection values() } interface EventBus { + void publish(Object) + void register(Object) + void unregister(Object) } abstract class RealmSecurityManager { - Collection realms; + void setRealms(Collection) # void afterRealmsSet() # void afterCacheManagerSet() # void afterEventBusSet() # void applyCacheManagerToRealms() # void applyEventBusToRealms() } abstract class AuthenticatingSecurityManager { - Authenticator authenticator + AuthenticationInfo authenticate(AuthenticationToken) } abstract class AuthorizingSecurityManager { - Authorizer authorizer --代理方法-- 代理所有Authorizer中的方法 } abstract class SessionSecurityManager { - SessionManager sessionManager # void applyCacheManagerToSessionManager() # void applyEventBusToSessionManager() + Session start(SessionContext) } class DefaultSecurityManager { # RememberMeManager rememberMeManager # SubjectDAO subjectDAO # SubjectFactory subjectFactory # Subject createSubject(AuthenticationToken, AuthenticationInfo, Subject) # void bind(Subject) # void rememberMeSuccessfulLogin(AuthenticationToken, AuthenticationInfo, Subject) # void rememberMeFailedLogin(AuthenticationToken, AuthenticationException, Subject) # void rememberMeLogout(Subject) + Subject login(Subject, AuthenticationToken) # void onSuccessfulLogin(AuthenticationToken, AuthenticationInfo, Subject) # void onFailedLogin(AuthenticationToken, AuthenticationException, Subject) # void beforeLogout(Subject) # SubjectContext copy(SubjectContext) + Subject createSubject(SubjectContext) # Subject doCreateSubject(SubjectContext) # void save(Subject) # void delete(Subject) # SubjectContext ensureSecurityManager(SubjectContext) # SubjectContext resolveSession(SubjectContext) # Session resolveContextSession(SubjectContext) # SessionKey getSessionKey(SubjectContext) -{static} boolean isEmpty(PrincipalCollection) # SubjectContext resolvePrincipals(SubjectContext) # SessionContext createSessionContext(SubjectContext) + void logout(Subject) # void stopSession(Subject) # void unbind(Subject) # PrincipalCollection getRememberedIdentity(SubjectContext) } interface RememberMeManager { + PrincipalCollection getRememberedPrincipals(SubjectContext) + void forgetIdentity(SubjectContext) + void onSuccessfulLogin(Subject, AuthenitcationToken, AuthenticationInfo) + void onFailedLogin(Subject, AuthenticationToken, AuthenticationException) + void onLogout(Subject) } abstract class AbstractRememberMeManager { - Serializer serializer - CipherService cipherService - byte[] encryptionCipherKey - byte[] decryptionCipherKey #{abstract} void forgetIdentity(Subject) # boolean isRememberMe(AuthenticationToken) + void rememberIdentity(Subject, AuthenticationToken, AuthenticationInfo) # PrincipalCollection getIdentityToRemember(Subject, AuthenticationInfo) # void rememberIdentity(Subject, PrincipalCollection) # byte[] convertPrincipalsToBytes(PrincipalCollection) #{abstract} void rememberSerializedIdentity(Subject, byte[]) + PrincipalCollection getRememberedPrincipals(SubjectContext) #{abstract} byte[] getRememberedSerializedIdentity(SubjectContext) # PrincipalCollection convertBytesToPrincipals(byte[], SubjectContext) # PrincipalCollection onRememberedPrincipalFailure(RuntimeException, SubjectContext) # byte[] encrypt(byte[]) # byte[] decrypt(byte[]) # byte[] serialize(PrincipalCollection) # PrincipalCollection deserialize(byte[]) + void onFailedLogin(Subject, AuthenticationToken, AuthenticationException) + void onLogout(Subject) } Authorizer <|-- SecurityManager Authenticator <|-- SecurityManager SessionManager <|-- SecurityManager SessionKey --* SessionManager SessionContext --* SessionManager SecurityManager <|.. CachingSecurityManager CacheManagerAware <|.. CachingSecurityManager EventBusAware <|.. CachingSecurityManager CacheManagerAware *-- CacheManager CacheManager *-- Cache EventBus -* EventBusAware CachingSecurityManager <|-- RealmSecurityManager RealmSecurityManager <|-- AuthenticatingSecurityManager AuthenticatingSecurityManager <|-- AuthorizingSecurityManager AuthorizingSecurityManager <|-- SessionSecurityManager SessionSecurityManager <|-- DefaultSecurityManager RememberMeManager --* DefaultSecurityManager AbstractRememberMeManager ..|> RememberMeManager @enduml @startuml authenticator-structure !include ./class-settings.puml interface Authenticator { + AuthenticaionInfo authenticate(AuthenticationToken) } interface AuthenticationInfo { + PrincipalCollection getPrincipals() + Object getCredentials() } abstract class AbstractAuthenticator { - Collection listeners # void notifySuccess(AuthenticationToken, AuthenticationInfo) # void notifyFailure(AuthenticationToken, AuthenticationException) # void notifyLogout(PrincipalCollection) + void onLogout(PrincipalCollection) + AuthenticationInfo authenticate(AuthenticationToken) #{abstract} AuthenticationInfo doAuthenticate(AuthenticationToken) } class ModularRealmAuthenticator { - Collection realms - AuthenticationStrategy authenticationStrategy # void assertRealmsConfigured() # AuthenticationInfo doSingleRealmAuthentication(Realm, AuthenticationToken) # AuthenticationInfo doMultiRealmAuthentication(Collection, AuthenticationToken) } interface AuthenticationStrategy { + AuthenticationInfo beforeAllAttempts(Collection, AuthenticationToken) + AuthenticationInfo beforeAttempt(Realm, AuthenticationToken, AuthenticationInfo) + AuthenticationInfo afterAttempt(Realm, AuthenticationToken, AuthenticationInfo, AuthenticationInfo) + AuthenticationInfo afterAllAttempts(AuthenticationToken, AuthenticationInfo) } abstract class AbstractAuthenticationStrategy { # AuthenticationInfo merge(AuthenticationInfo, AuthenticationInfo } class FirstSuccessfulStrategy { - boolean stopAfterFirstSuccess } AuthenticationInfo --* Authenticator AbstractAuthenticator ..|> Authenticator ModularRealmAuthenticator --|> AbstractAuthenticator AuthenticationStrategy --* ModularRealmAuthenticator AbstractAuthenticationStrategy ..|> AuthenticationStrategy AllSuccessfulStrategy --|> AbstractAuthenticationStrategy AtLeastOneSuccessfulStrategy --|> AbstractAuthenticationStrategy FirstSuccessfulStrategy --|> AbstractAuthenticationStrategy @enduml @startuml realm-structure !include ./class-settings.puml interface Realm { + String getName() + boolean supports(AuthenticaitonToken) + AuthenticationInfo getAuthenticationInfo(AuthenticationToken) } interface LogoutAware { + void onLogout(PrincipalCollection) } abstract class CachingRealm { - String name - boolean cachingEnabled - CacheManager cacheManager # void afterCacheManagerSet() -{static} boolean isEmpty(PrincipalCollection) # void clearCache(PrincipalCollection) # void doClearCache(PrincipalCollection) # Object getAvailiablePrincipal(PrincipalCollection) } abstract class AuthenticatingRealm { - CreadentialMatcher credentialsMatcher - Cache uthenticationCache - boolean authenticationCachingEnabled - String authenticationCacheName - Class authenticationTokenClass + void init() # void onInit() - Cache getAuthenticationacheLazy() - AuthenticationInfo getCachedAuthenticaitonInfo(AuthenticationToken) - void cacheAuthenticaitonInfoIfPossible(AuthenticationToken, AuthenticationInfo) + AuthenticationInfo getAuthenticationInfo(AuthenticationToken) # void assertredentialsMatch(AuthenticationToken, AuthenticationInfo) # Object getAuthenticationCacheKey(AuthenticationToken) # Object getAuthenticationCacheKey(PrincipalCollection) # void clearCachedAuthenticationInfo(PrincipalCollection) #{abstract} AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken) } interface PermissionResolverAware { + void setPermissionResolver(PermissionResolver) } interface RolePermissionResolverAware { + void setRolePermisisonResolver(RolePermisisonResolver) } interface Authorizer abstract class AuthorizingRealm { - boolean authorizationCachingEnabled - Cache authorizationCache - String authorizationCacheName - PermissionResolver permissionResolver - RolePermissionResolver permissionRoleResolver } enum SaltStyle { NO_SALT CRYPT COLUMN EXTERNAL } class JdbcRealm { # DataSource dataSource # String authenticationQuery # String userRolesQuery # String permissionsQuery # boolean permissionsLookupEnabled # SaltStyle saltStyle # boolean saltIsBase64Encoded - String[] getPasswordForUser(Connection, String) # Set getRoleNamesForUser(Connection, String) # Set getPermissions(Connection, String, Collection) # String getSaltForUser(String) } class SimpleAccountRealm { # Map users # Map roles # ReadWriteLock USERS_LOCK # ReadWriteLock ROLES_LOCK # SimpleAccount getUser(String) + boolean accountExists(String) + void addAccount(String, String) + void addAccount(String, String, String...) # String getUsername(SimpleAccount) # String getUsername(PrincipalCollection) # void add(SimpleAccount) # SimpleRole getRole(String) + boolean roleExists(String) # void addRole(String) # void add(SimpleRole) #{static} Set toSet(String, String) } class SimpleAccount { - SimpleAuthenticationInfo authcInfo - SimpleAuthorizationInfo authzInfo - boolean locked - boolean credentialsExpired } interface MergableAuthenticationInfo { + void merge(AuthenticationInfo) } interface Account interface AuthenticationInfo interface AuthorizationInfo class SimpleRole { # String name # Set permissions + void add(Permission) + void addAll(Collection) + boolean isPermitted(Permisison) } CachingRealm ..|> Realm CachingRealm ..|> LogoutAware AuthenticatingRealm --|> CachingRealm AuthorizingRealm --|> AuthenticatingRealm AuthorizingRealm .|> Authorizer AuthorizingRealm ..|> PermissionResolverAware AuthorizingRealm ..|> RolePermissionResolverAware SimpleAccountRealm --|> AuthorizingRealm JdbcRealm --|> AuthorizingRealm SaltStyle -+ JdbcRealm SimpleAccount --* SimpleAccountRealm SimpleRole --* SimpleAccountRealm Account <|.. SimpleAccount MergableAuthenticationInfo <|.. SimpleAccount AuthenticationInfo <|-- Account AuthenticationInfo <|-- MergableAuthenticationInfo AuthorizationInfo <|-- Account @enduml @startuml shiro-filter-structure !include ./class-settings.puml class BearerHttpAuthenticationFilter { # AuthenticationToken createBearerToken(String, ServletRequest) } class BasicHttpAuthenticaitonFilter abstract class HttpAuthenticationFilter { - String applicationName - String authcScheme - String authzScheme # Set httpMethodsFromOptions(String[]) # boolean isLoginAttempt(ServletRequest, ServletResponse) # boolean isLoginAttempt(String) # String getAuthzHeader(ServletRequest) # boolean sendChallenge(ServletRequest, ServletResponse) # String[] getPrincipalsAndCredentials(String, ServletRequest) # String[] getPrincipalsAndCredentials(String, String) } abstract class AuthenticatingFilter { # boolean executeLogin(ServletRequest, ServletResponse) # AuthenticationToken createToken(ServletRequest, ServletResponse) # AuthenticationToken createToken(String, String, ServletRequest, ServletResponse) # AuthenticationToken createToken(String, String, boolean, String) # boolean onLoginSuccess(AuthenticationToken, Subject, ServletRequest, ServletResponse) # boolean onLoginFailure(AuthenticationToken, AuthenticationException, ServletRequest, ServletResponse) # String getHost(ServletRequest) # boolean isRememberMe(ServletRequest) # boolean isPermissive(Object) } abstract class AuthenticationFilter { - String successUrl # void isssueSuccessRedirect(ServletRequest, ServletResponse) } abstract class AccessControlFilter { - String loginUrl # Subject getSubject(ServletRequest, ServletResponse) # boolean isAccessAllowed(ServletRequest, ServletResponse, Object) # boolean onAccessDenied(ServletRequest, ServletResponse, Object) # boolean onAccessDenied(ServletRequest, ServletResponse) # boolean isLoginRequest(ServletRequest, ServletResponse) # void saveRequestAndRedirectToLogin(ServletRequest, ServletResponse) # void saveRequest(ServletRequest) # void redirectToLogin(ServletRequest, ServletResponse) } abstract class PathMatchingFilter { # PatternMatcher pathMatcher # Map appliedPaths # String getPathWithinApplication(ServletRequest) # boolean pathsMatch(String, ServletRequest) # boolean pathsMatch(String, String) - boolean isFilterChainContinued(ServletRequest, ServletResponse, String, Object) # boolean onPreHandle(ServletRequest, ServletResponse, Object) # boolean isEnabled(ServletRequest, ServletResponse, String, Object) } interface PathConfigProcessor { + Filter processPathConfig(String, String) } abstract class AdviceFilter { # boolean preHandle(ServletRequest, ServletResponse) # void postHandle(ServletRequest, ServletResponse) + void afterCompletion(ServletRequest, ServletResponse, Exception) # void executeChain(ServletRequest, ServletResponse, FilterChain) # void cleanup(ServletRequest, ServletResponse) } abstract class OncePerRequestFilter { - boolean enabled # boolean isEnabled(ServletRequest, ServletResponse) # String getAlreadyFilterAttributeName() # void doFilterInternal(ServletRequest, ServletResponse, FilterChain) } abstract class NameableFilter { # String name } abstract class AbstractFilter { # FilterConfig filterConfig # String getInitParam(String) # void onFilterConfigSet() } class ServletContextSupport { - ServletContext servletContext # String getContextInitParam(String) - ServletContext getRequiredServletContext() # void setContenxtAttribute(String, Object) # Object getContextAttribute(String) # void removeContextAttribute(String) } interface Filter <> { + void init(FilterConfig) + void doFilter(ServletRequest, ServletResponse, FilterChain) + void destroy() } interface Nameable { + void setName(String name) } class FormAuthenticationFilter { - String usernameParam - String passwordParam - String rememberMeParam - String failureKeyAttribute # boolean isLoginSubmission(ServletRequest, ServletResponse) # void setFailureAttributes(ServletRequest, AuthenticationException) - String getUsername(ServletRequest) - String getPassword(ServletRequest) } class LogoutFilter { - String redirectUrl - boolean postOnlyLogout # Subject getSubject(ServletRequest, ServletResponse) # void issueRedirect(ServletRequest, ServletResponse) # String getRedirectUrl(ServletRequest, ServletResponse) # boolean onLogoutRequestNotAPost(ServletRequest, ServletResponse) } class PassThruAuthentcationFilter class UserFilter abstract class AuthorizationFilter { - String unauthorizedUrl } class HostFilter { + Map authorizedIps + Map deniedIps + Map authorizedHostnames + Map deniedHostnames + void setAuthorizedHosts(String) + void setDeniedHosts(String) # boolean isIpv4Candidate(String) } class HttpMethodPermissionFilter { - Map httpMethodActions # String getHttpMethodAction(ServletRequest) # String getHttpMethodAction(String) # String[] buildPermissions(HttpServletRequest, String[], String) # String[] buildPermissions(String[], String) } class IpFilter { - List deniedIpMatchers - List authorizedIpMatchers - IpSource ipSource + void setAuthorizedips(String) + void setDeniedIps(String) + void setIpSource(IpSource) + String getHostFromRequest(ServletRequest) } class PermissionAuthorizationFilter class PortFilter { - int port # int toPort(Object) # String getScheme(String, int) } class RolesAuthorizationFilter class SslFilter { - HSTS hsts } abstract class AbstractShiroFilter { - WebSecurityManager securityManager - FilterChainResolver filterChainResolver - boolean staticSecurityManagerEnabled - void applyStaticSecurityManagerEnabledConfig() + void init() - void ensureSecurityManager() # WebSecurityManager createDefaultSecurityManager() # boolean isHttpSession() # ServletRequest wrapServletRequest(HttpServletRequest) # ServletRequest prepareServletRequest(ServletRequest, ServletResponse, FilterChain) # ServletResponse wrapServletResponse(HttpServletResponse, ShiroHttpServletRequest) # ServletResponse prepareServletResponss(ServletRequest, ServletResponse, FilterChain) # WebSubject createSubject(ServletRequest, ServletResponse) # void updateSessionLastAccessTime(ServletRequest, ServletResponse) # FilterChain getExecutableChain(ServletRequest, ServletResponse, FilterChain) # void executeChain(ServletRequest, ServletResponse, FilterChain) } class ShiroFilter BearerHttpAuthenticationFilter --|> HttpAuthenticationFilter BasicHttpAuthenticaitonFilter --|> HttpAuthenticationFilter PortFilter <|- SslFilter HttpAuthenticationFilter --|> AuthenticatingFilter FormAuthenticationFilter --|> AuthenticatingFilter AuthenticatingFilter --|> AuthenticationFilter PassThruAuthentcationFilter -|> AuthenticationFilter HttpMethodPermissionFilter --|> PermissionAuthorizationFilter IpFilter --|> AuthorizationFilter AuthorizationFilter <|- PortFilter AuthorizationFilter <|-- RolesAuthorizationFilter PermissionAuthorizationFilter --|> AuthorizationFilter HostFilter --|> AuthorizationFilter AuthenticationFilter --|> AccessControlFilter UserFilter --|> AccessControlFilter AuthorizationFilter --|> AccessControlFilter AccessControlFilter --|> PathMatchingFilter AnonymousFilter --|> PathMatchingFilter NoSessionCreationFilter -|> PathMatchingFilter PathMatchingFilter -|> AdviceFilter AdviceFilter <|- LogoutFilter PathMatchingFilter ..|> PathConfigProcessor ShiroFilter --|> AbstractShiroFilter AdviceFilter --|> OncePerRequestFilter OncePerRequestFilter <|-- AbstractShiroFilter OncePerRequestFilter --|> NameableFilter AbstractFilter <|-- NameableFilter NameableFilter ..|> Nameable ServletContextSupport <|- AbstractFilter AbstractFilter ..|> Filter @enduml @startuml shiro-authc-flow !include ./activity-settings.puml start :用户提交Web请求; if (用户请求是登录请求) then (是) partition Controller { :取得用户名及密码; :构建相应的AuthenticationToken; } else (否) partition Filter { :解析用户请求中提供的认证信息; :解析认证信息构建AuthenticationToken; } endif :使用SecurityUtils获取Subject实例; note right 所有的登录步骤都将存在于 Controller的登录处理方法 和Filter的executeLogin() 方法中。 end note :使用Subject对AuthenticationToken进行登录验证; partition SecurityManager { :使用Authenticator处理AuthenticationToken; repeat :Authenticator从Realm列表中获取一个Realm; :使用Realm对AuthenticationToken进行验证; if (Realm支持处理AuthenticationToken) then (支持) :从AuthenticationToken中获取Principal; :从用户信息存储中利用Principal获取完整的用户信息; :使用用户信息构建AuthenticationInfo; else (不支持) endif repeat while (所有Realm都循环过了) is (否) not (是) :根据返回的AuthenticationInfo设置Subject; } :获取保存在Subject中的AuthorizationInfo; :对比用户业务所需的权限; if (用户拥有所需的权限) then (是) :继续用户业务功能的执行; else (否) :阻止用户业务功能的执行; endif stop @enduml