diff --git a/drawings/activity-settings.puml b/drawings/activity-settings.puml new file mode 100644 index 0000000..efd5bf6 --- /dev/null +++ b/drawings/activity-settings.puml @@ -0,0 +1,6 @@ +skinparam Shadowing false +skinparam BackgroundColor transparent +skinparam { + ActivityBackgroundColor transparent + ActivityDiamondBackgroundColor transparent +} diff --git a/drawings/class-settings.puml b/drawings/class-settings.puml new file mode 100644 index 0000000..e733b7d --- /dev/null +++ b/drawings/class-settings.puml @@ -0,0 +1,5 @@ +skinparam Shadowing false +skinparam class { + BackgroundColor White +} +hide empty members diff --git a/drawings/shiro.puml b/drawings/shiro.puml new file mode 100644 index 0000000..fb32599 --- /dev/null +++ b/drawings/shiro.puml @@ -0,0 +1,819 @@ +@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 diff --git a/source/_posts/shiro-spring.md b/source/_posts/shiro-spring.md new file mode 100644 index 0000000..6c06d67 --- /dev/null +++ b/source/_posts/shiro-spring.md @@ -0,0 +1,556 @@ +--- +title: 使用Shiro替换Spring Security +tags: + - Java + - Spring + - Spring Security + - Shiro + - Spring MVC + - 安全认证 +categories: + - - JVM + - Spring +keywords: 'Spring,Spring Security,Shiro,Realm,Subject,Authentication,Authorization' +date: 2021-09-29 17:57:29 +--- + +之前用了几篇文章分析和记录了Spring Security的用法,但是相比其他安全框架来说,Spring Security虽然与Spring Framework结合紧密,却无法称得上轻量。所以在很多项目中,Spring Security就被很自然的放在一边了,转而采用其他的安全框架。Shiro就是这样一个轻量级的选择。 + +Shiro是一款不依赖于任何容器的,可以通用于Java SE和Jakarta EE的安全框架。根据官网文档的描述,SHiro设计有四大功能:Authentication(认证)、Authorization(授权)、Session Management(会话管理)、Cryptography(密码学)。在将Shiro应用到用户安全管理中,最常用的就是使用其Authentication和Authorization两个功能。 + +这片文章将会对Shiro的基础概念和使用进行一个简单的分析,并最后给出一个能够对用户进行鉴权并提供用户使用Token操作的示例。 + +## 基础概念 + +跟Spring Security中一样,Shiro也提供了一些设施来完成用户鉴权的功能。这些常用的功能设施主要有以下这些。 + +1. `UsernamePasswordToken`,用于保存用户录入的用户名和密码,之后交由Shiro进行认证。 +1. `BearerToken`,用于保存用户提供的令牌,之后也同样交由Shiro进行认证。 +1. `Subject`,Shiro抽象出来的用于包含用户信息的对象。 +1. `SecurityManager`,用于提供认证与授权功能。 +1. `Realm`,由用户自行定义的用于执行鉴权和授权逻辑的实现模块,并对用户的访问进行控制。 +1. `AuthenticationInfo`,用户的角色信息集合,主要用于认证。 +1. `AuthorizationInfo`,用户的权限信息集合,主要用于授权。 +1. `SecurityManager`,Shiro的安全管理器,用于管理所有系统中存在的`Subject`。 +1. `ShiroFilterFactoryBean`,Shiro的过滤器工厂,是由用户自定义的处理流水线。用户定义的处理流程就是由一个个的`Filter`组成的。 +1. `SecurityUtils`,Shiro提供的一个用于常用操作的工具类,其中集成了常用功能设施的创建和获取操作。 + +在项目中使用Shiro的时候,一般也是通过定义这些内容来完成用户的认证和授权的过程的。在开始使用Shiro之前,还是照旧先看一下以上每种设施的类继承结构,方便对Shiro的整体架构有一个初步的了解。 + +### AuthenticationToken + +Token是一个用于承载用于用户认证信息的容器,常用的Token只有`UsernamePasswordToken`和`BearerToken`两个,但是支撑它们的还有三个接口,如下图所示。 + +{% oss_image shiro-spring/token-structure.svg AuthenticationToken类继承结构 450 %} + +所有的Token都间接实现了`AuthenticationToken`接口,而`AuthenticationToken`中定义的`getPrincipal()`则是用于获取用户信息的,`getCredentials()`则是用来获取用户密钥之类的认证信息的。这就为如何使用`AuthenticationToken`指明了一条路线。 + +### Subject + +`Subject`要比`AuthenticationToken`保存的内容要更多,但增加的内容主要是用户是否经过了认证和授权,以及用户所具有的权限信息。在整个项目中,对于用户的认证与授权信息,都是通过获取`Subject`实例来得到的。所以`Subject`也是在整个项目中获取当前登录用户信息的首要途径。`Subject`接口中的内容比较多,但是并不复杂,而且`Subject`接口相关的类也相对比较少,主要相关的类之间的关系如下图所示。 + +{% oss_image shiro-spring/subject-structure.svg Subject类的相关结构 1350 %} + +`Subject`在整个项目中通常代表一个用户,不管这个用户是已经登录成功了,还是被拒绝访问了,都会在系统中存在一个对应的Subject。而且从上面的结构图上也可以看到,`Subject`基本上是Shiro在处理用户登录与授权时的一个核心类。 + +### SecurityManager + +`SecurityManager`在整个系统中启动并执行了所有针对于`Subject`的安全操作。但是在实际的代码编写中,我们并不会直接去调用`securityManager`的实例来完成什么操作,一般都是使用`SecurityUtils`工具类获取一个`Subject`实例来进行一系列的操作的。但是对于`SecurityMananger`的结构还是需要了解一下的。 + +{% oss_image shiro-spring/securitymanager-structure.svg SecurityManager类的相关结构 1350 %} + +经过图中所示的层层继承和实现,在项目应用的通常都是`DefaultSecurityManager`的实例,它的功能已经足够满足日常项目所需。在使用的时候,只需要使用其中的`setRealm()`方法将自定义的`Realm`放置进去即可。 + +### Authenticator + +`Authenticator`是用来对用户提交的`AuthenticationToken`进行认证的,`AuthenticationToken`经过`Authenticator`处理以后,会返回一个`AuthenticationInfo`实例,其中包括了已经完成认证的用户的所有账户信息。此外,`Authenticator`还通过`AuthenticationStrategy`控制着项目中有多个`Realm`的时候如何完成用户认证操作的策略。 + +常用的`Authenticator`相关的类结构,可以参考下图。 + +{% oss_image shiro-spring/authenticator-structure.svg Authenticator类的相关结构 850 %} + +在配置项目中所使用的`SecurityManager`的时候,一般并不需要手工实例化`Authenticator`,只需要通过`SecurityManager`提供的`getAuthenticator()`方法获取其中持有的`Authenticator`实例,然后再配置其中所需的`AuthenticationStrategy`即可。这里需要注意的是,如果整个项目中只使用了一个`Realm`,那么就无需配置`Authenticator`的`AuthenticationStrategy`。 + +### Realm + +`Realm`是一个由用户自定义的专门用于确定用户身份和授权信息的实体。`Realm`通常可以访问项目中的所有安全相关信息,包括用户信息、权限信息等,在整个项目中通常都扮演一个特殊的DAO角色。在Shiro的设计中,每一个`Realm`都只实现一种认证功能,`Realm`可以根据所传入的`AuthenticationToken`的具体类型来确定自身是否能对给定的认证信息进行认证;而`Authenticator`也将根据所设置的`AuthenticationStrategy`确定多个`Realm`之间的组合认证方式。 + +在Shiro中`Realm`只是一个非常简练的接口,在实际使用中一般不会直接去实现这个接口,而是会选择继承其已有的实现。常用的有关`Realm`的类结构,可以参考下图。 + +{% oss_image shiro-spring/realm-structure.svg Realm类的相关结构 1350 %} + +根据这张`Realm`类的结构图,可以看出,如果需要自定义`Realm`实现,一般可以直接继承`AuthorizingRealm`类,这个类中已经继承并实现了大部分的用户验证逻辑,可以直接复用。如果定义了多个`Realm`,那么可以通过`Realm`中的`support()`方法来决定自身所能够处理的`AuthenticationToken`类型。 + +### SecurityUtils + +如果在其他的网站上看过Shiro的教程,那么一般会发现`SecurityUtils`这个工具类出现的次数十分频繁。这是因为这个工具类通常是被用来创建`Subject`以及获取当前项目中的`SecurityManager`的。使用`SecurityUtils`而不是手动创建`Subject`这些类,可以确保类实例的从属关系。在`SecurityUtils`中,主要的方法有以下两个。 + +1. `getSubject()`,从当前的`ThreadContext`中获取一个`Subject`实例或者创建一个新的`Subject`实例。 +1. `getSecurityManager()`,从当前的`ThreadContext`中获取一个`SecurityManager`实例。 + +在后面的整合示例中,将会看到`SecurityUtils`的使用方式。但是有一点是需要注意的,如果需要使用`Securityutils`中的功能,必须先使用`setSecurityManager()`方法为Shiro设置一个全局单例的`SecurityManager`实例。 + +### 过滤器 + +将Shiro的认证与授权处理流程融入进Web项目中也同样是依靠Servlet Filter的。过滤器在Shiro的处理流程里可以承担对请求进行预处理的工作,并且可以对请求中提供的令牌、密钥等进行认证与授权的操作。所以,过滤器也是Shiro处理流程中一个主要的生成`AuthenticationToken`的起点。Shiro中的过滤器也是一个比较庞大的体系,如果在项目中需要自己实现一个过滤器,那么可以参考下图来选择所要继承的类。 + +{% oss_image shiro-spring/shiro-filter-structure.svg "Shiro过滤器类的相关结构" 1350 %} + +通常我们都需要在上面这张图中,选择比较靠上的类来继承,一般会选择`BasicHttpAuthenticationFilter`。这样可以利用`createToken()`方法来创建用于代表用户登录信息的`AuthenticationToken`,再利用`AuthenticatingFilter`中提供的`executeLogin()`方法将自动生成的`AuthenticationToken`登入进来。 + +## 与Spring整合 + +既然要使用Shiro替代Spring Security来完成用户认证与授权功能,那么就要在Spring框架下构建和组装Shiro完成认证所需要用到的类实例。在目前的Spring框架开发过程中,都是使用Spring Boot脚手架框架来支持Spring应用的快速建立和配置。所以在构建使用Shiro进行用户认证和授权的应用时,也需要先在项目中加入用于自动化配置Shiro的Starter。 + +以下是Shiro-Spring Starter的GAV坐标。 + +```xml + + org.apache.shiro + shiro-spring + ${shiro.spring.version} + + + + org.apache.shiro + shiro-web + ${shiro.spring.version} + +``` + +!!! info "" + 在本文进行编写的时候,Shiro-Spring的版本是1.8.0。 + +### 认证与授权功能流程 + +在开始使用Shiro进行用户认证功能的编写之前,需要先对Shiro结合到Web项目中以后的整个工作流程有一个大概的了解。下图从一个Web请求开始,绘制了用户认证信息与授权信息的形成和流转流程。 + +{% oss_image shiro-spring/shiro-authc-flow.svg Shiro认证与授权概要流程 550 %} + +在这个过程中,实际上有许多过程是不需要我们去实现的,我们所需要实现的内容主要是对于用户信息的获取、`AuthenticationToken`与`AuthenticationInfo`的组织等。当然在Controller中对用户提供的用户名和密码等认证信息的鉴别还是需要我们编码来执行的。 + +!!! info "" + 其实对于用户认证处理最核心的就是,只要调用`subject.login(token)`时不抛出任何异常,那么用户就已经被系统认证通过了,可以在之后的操作中通过获取`Subject`实例来得到用户的相关信息。 + +### Shiro配置 + +对Shiro的配置,主要是配置Shiro的处理流程,通过定义其中的关键部件,使上图中的流程完全打通。在加入其他的功能类之前,可以首先先在项目中添加一个仅有基本功能的配置类。 + +```java +@Configuration +@Import({ + ShiroBeanConfiguration.class, + ShiroAnnotationProcessorConfiguration.class, + ShiroWebConfiguration.class, + ShiroWebFilterConfiguration.class, + ShiroRequestMappingConfig.class +}) +public class WebShiroConfig { + private final SecurityManager securityManager; + + public WebShiroConfig(SecurityManager securityManager) { + this.securityManager = securityManager; + SecurityUtils.setSecurityManager(this.securityManager); + } + + @Bean + public ShiroFilterChainDefinition shiroFilterChainDefinition() { + var chainDefinition = new DefaultShiroFilterChainDefinition(); + // 将所有路径都设置为允许匿名访问,可以使Shiro的注解控制生效 + chainDefinition.addPathDefinition("/**", "anon"); + // 或者可以全部允许访问的方式来使Shiro注解控制生效 + // 两种方法选择其中的一种即可 + chainDefinition.addPathDefinition("/**", "authcBasic[permissive]"); + + // 当然如果不使用Shiro的注解控制,就可以在这里编写根据路径的权限控制,例如: + // 设置用户必须登入系统,并且拥有admin角色 + chainDefinition.addPathDefinition("/admin/**", "authc, roles[admin]"); + // 设置用户必须拥有stroage:write权限 + chainDefinition.addPathDefinition("/store/**", "perms[storage:write]"); + // 设置用户必须登入系统 + chainDifinition.addPathDefinition("/api/**", "authc"); + + return chainDefinition; + } +} +``` + +#### 关闭Session + +关闭Session的支持可以让Shiro以Stateless的方式接入Spring MVC中。要关闭Session,需要从`SubjectFactory`中的`createSubject()`方法进行设置,这就需要重写一套`SubjectFactory`,但是这个类并不麻烦,只是需要记得将其注入到Shiro的设置中。 + +```java +@Component +@Order(1) +public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory { + @Override + public Subject createSubject(SubjectContext context) { + // 设置SubjectContext,不为Subject创建Session + context.setSessionCreationEnabled(false); + + return super.createSubject(context); + } +} +``` + +另外还需要在上面的`WebShiroConfig`类中增加以下几个Bean,以关闭Session相关的其他设施。 + +```java +@Bean +@Order(1) +public SessionManager sessionManager() { + var sessionManager = new DefaultSessionManager(); + // 关闭Session验证 + sessionManager.setSessionValidationSchedulerEnabled(false); + return sessionManager; +} + +@Bean +@Order(1) +public SessionStorageEvaluator sessionStorageEvaluator() { + var evaluator = new DefaultSessionStorageEvaluator(); + // 关闭Session存储 + evaluator.setSessionStorageEnabled(false); + return evaluator; +} +``` + +!!! caution "" + Shiro的Spring Boot Starter中大部分Bean都使用了`@ConditionOnMissingBean`的注解,所以如果需要覆盖某一个Bean,只需要直接定义自己的Bean即可。 + +因为Shiro的Spring Boot Starter中没有采用依赖注入的方式创建`SubjectDAO`、`SecurityManager`等实例,所以这些必须的实例也需要在配置类中手动创建。 + +```java +@Bean +public SubjectDAO subjectDAO(SessionStorageEvaluator evaluator) { + var dao = new DefaultSubjectDAO(); + dao.setSessionStorageEvaluator(evaluator); + return dao; +} + +@Bean +public ModularRealmAuthenticator modularRealmAuthenticator() { + var authenticator = new ModularRealmAuthenticator(); + authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy()); + return authenticator; +} + +// 这里先加一个剧透,把后面要用的两个用于认证的Realm加入进来供SecurityManager使用。 +@Bean(name = "realms") +public List authenticationRealms() { + List realms = new LinkedList<>(); + realms.add(new UsernameRealm()); + realms.add(new TokenRealm()); + return realms; +} +``` + +之后建立一个SecurityManager的实例来组织认证所需要的内容。 + +```java +@Component +public class MultiRealmsSecurityManager extends DefaultSecurityManager { + + public MultiRealmsSecurityManager( + SubjectDAO subjectDAO, + SubjectFactory subjectFactory, + RememberMeManager rememberMeManager, + ModularRealmAuthenticator authenticator, + @Qualifier("realms") List realmList + ) { + this.setSubjectDAO(subjectDAO); + this.setSubjectFactory(subjectFactory); + if (Objects.nonNull(rememberMeManager)) { + this.setRememberMeManager(rememberMeManager); + } + this.setAuthenticator(authenticator); + super(realmList); + } + +} +``` + +#### 构建对用户名与密码的验证 + +首先假设应用中的用户表结构是如下面这样定义的。 + +```java +@Getter +@Setter +@Builder +@NoArgsConstructor +@RequiredArgsConstructor +@Entity +public class Member { + @Id + @NonNull + private Long id; + + @Column(length = 50, nullable = false) + @NonNull + private String username; + + @Column(length = 150, nullable = false) + @NonNull + private String password; + + @Column(length = 70) + private String fullName; + + @Column(nullable = false) + @NonNull + private boolean enabled; + + @ManyToMany(cascade = CascadeType.ALL) + @JoinTable( + name = "member_roles", + joinColumns = {@JoinColumn(name = "mid")}, + inverseJoinColumns = {@JoinColumn(name = "rl_keyword")} + ) + private Set roles; +} +``` + +因为假定Java应用采用Spring Data JPA对数据库访问进行包装,这样应用中就会存在一个名为`MemberRepository`的接口来进行数据库的访问操作。 + +对于用于在Shiro中承载待验证信息的`AuthenticationToken`类,可以直接使用Shiro内置的`UsernamePasswordToken`类,也可以选择自行实现一个具有特定功能的`AuthenticationToken`类。在这个示例中,将直接使用`UsernamePasswordToken`类。有了这两个支持类,就可以定义一个专门用于验证用户名和密码的`Realm`类了。 + +```java +@Component +@RequiredArgsConstructor +public class UsernameRealm extends AuthoringRealm { + private final MemberRepository memberRepository; + + @Override + public boolean supports(AuthenticationToken token) { + return token instanceof UsernamePasswordToken; + } + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { + var username = (String) principalCollection.getPrimaryPrincipal(); + var roles = memberRepository.findMemberByUsername(username) + .stream() + .map(Member::getRoles) + .flatMap(Collection::stream) + .map(Role::getKeyword) + .collect(Collectors.toSet()); + return new SimpleAuthorizationInfo(roles); + } + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { + var token = (UsernamePasswordToken) authenticationToken; + return memberRepository.findMemberByUsername((String) token.getPrincipal()) + .map(member -> new SimpleAuthenticationInfo(member, member.getPassword(), member.getFulLName())) + .orElse(null); + } +} +``` + +#### 构建对用户令牌的验证 + +如果要支持用户令牌的鉴权方式,那么就需要在应用中构建一个用于鉴别用户令牌真伪的机制,在这个示例中将采用将已经颁发的用户令牌保存到数据库的方式。与用户信息一样,这种方式也需要一个数据库实体类。 + +```java +@Getter +@Setter +@Builder +@NoArgsConstructor +@RequiredArgsConstructor +@Entity +public class TokenStore { + @Id + @NonNull + private String token; + + @NonNull + @Column(nullable = false) + LocalDateTime expiresAt; + + @OneToOne(targetEntity = Member.class, fetch = FetchType.LAZY, cascade = CascadeType.MERGE) + @JoinColumn(name = "mid") + Member member; +} +``` + +同样的,在应用中还将存在一个用于操作`TokenStore`实体的`TokenStoreRepository`接口。因为对用户令牌的鉴权是由另一个`Realm`类完成的,所以携带用户令牌的`AuthenticationToken`类就需要与承载用户名和密码的`UsernamePasswordToken`类区分开,这里直接采用Shiro内置的`BearerToken`类来承载跟随HTTP请求发来的用户令牌。 + +```java +@Component +@RequiredArgsConstructor +public class TokenRealm extends AuthorizingRealm { + private final TokenStoreRepository storeRepository; + + @Override + public boolean supports(AuthorizationToken token) { + return token instanceof BearerToken; + } + + // 关于授权信息的获取,基本上所有的Realm都是一样的。 + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { + var username = (String) principalCollection.getPrimaryPrincipal(); + var roles = memberRepository.findMemberByUsername(username) + .stream() + .map(Member::getRoles) + .flatMap(Collection::stream) + .map(Role::getKeyword) + .collect(Collectors.toSet()); + return new SimpleAuthorizationInfo(roles); + } + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { + var token = (BearerToken) authenticationToken; + // 在BearerToken中,无论是principal还是crendentials里保存的都是用户令牌信息 + return storeRepository.findTokenById(token.getToken()) + .filter(t -> t.getExpires().isAfter(LocalDateTime.now())) + .map(TokenStore::getMember()) + .map(member -> new SimpleAuthenticationInfo(member, token.getToken(), member.getFulLName())) + .orElse(null); + } +} +``` + +!!! info "对用户密码的验证" + 在以上两个`Realm`类的示例中,都没有显式对用户提供的认证信息进行密码验证,这一步实际上是在`AuthenticatingRealm`的`getAuthenticationInfo()`方法中利用`assertCredentialsMatch()`来完成的。`AuthenticatingRealm`利用`CredentialsMatcher`来完成用户提供的认证信息的验证。所以在每个自定义的`Realm`中,还需要指定对于用户认证信息的加密或者散列方法。例如对于用于用户密码进行验证的`UsernameRealm`,可以采用SHA-512等散列方法来处理用户密码,而对于`BearerRealm`,则可以直接采用透明比对的方式。 + +与对用户名和密码的验证不同,对于用户令牌的验证并不是在特定的Spring MVC Controller中完成的,而是需要在Shiro的Filter中完成,所以还需要构建一个用于从HTTP请求中提取用户令牌并形成`BearerToken`实例的过滤器。因为这里选择使用了标准的Bearer Token认证方式,所以可以直接使用Shiro内置的`BearerHttpAuthenticationFilter`。 + +#### 完成SecurityManager的构建 + +上面这些类都已经具备以后,现在就需要对之前定义的`WebShiroConfig`类进行一次整理,其中最主要的事情就是把所需要的Filter指派给相应的路径。整理之后的`WebShiroConfig`是下面这个样子。 + +```java +@Configuration +@Import({ + ShiroBeanConfiguration.class, + ShiroAnnotationProcessorConfiguration.class, + ShiroWebConfiguration.class, + ShiroWebFilterConfiguration.class, + ShiroRequestMappingConfig.class +}) +public class WebShiroConfig { + private final SecurityManager securityManager; + + public WebShiroConfig(SecurityManager securityManager) { + this.securityManager = securityManager; + SecurityUtils.setSecurityManager(this.securityManager); + } + + @Bean + @Autowired + public ShiroFilterFactoryBean shiroFilterFactory( + ShiroFilterChainDefainition filterChainDefinition + ) { + var filterFactory = new ShiroFilterFactoryBean(); + filterFactory.setSecurityManager(this.securityManager); + + Map filters = new HashMap<>(); + filters.put("bearer", bearerFilter()); + filterFactory.setFilters(filters); + + filterFactory.setLoginUrl("/login"); + filterFactory.setFilterChainDefinitionMap(filterChainDefinition.getFilterChainMap()); + return filterFactory; + } + + @Bean + public ShiroFilterChainDefinition shiroFilterChainDefinition() { + var chainDefinition = new DefaultShiroFilterChainDefinition(); + + // 这里所有路径的声明顺序是有重要意义的,应该遵循先特殊Filter,再匿名Filter,最后通用Filter的顺序,保证路径的匹配顺序 + chainDefinition.addPathDefinition("/logout", "logout"); + chainDefinition.addPathDefinition("/login", "anon"); + // 要求所有需要携带用户令牌的请求都必须经过BearerHttpAuthenticationFilter + chainDefinition.addPathDefinition("/**", "bearer"); + + return chainDefinition; + } +} +``` + +### 处理用户登录 + +用户登录主要是一个收集用户提供的认证信息并进行校验的过程,在Shiro提供的Realm加持下,处理用户登录的过程被简化成了只需要组装`AuthenticationToken`,不必再次实现认证信息的校验的过程。以下是一个登录Controller的示例。 + +```java +@PostMapping("/login") +@Transactional +public LoginResponse doLogin(@RequestBody VLoginForm form) { + var subject = SecurityUtils.getSubject(); + var token = new UsernamePasswordToken(form.getUsername(), form.getPassword()); + try { + // 这一句就可以用来执行登录过程,只要这个方法不抛出异常,那么登录就是成功的 + subject.login(token); + Token userToken = tokenService.generate(token); + storeRepository.save(userToken); + return LoginResponse.builder().code(200).message("登录成功。").token(userToken.getToken()).build(); + } catch (UnknownAccountException e) { + return LoginResponse.builder().code(403).message("用户不存在。").build(); + } catch (DisabledAccountException e) { + return LoginResponse.builder().code(403).message("用户被禁止登录").build(); + } catch (IncorrectCredentialsException e) { + return LoginResponse.builder().code(403).message("用户名或者密码不正确。").build(); + } catch (Throwable e) { + e.printStackTrace(); + return LoginResponse.builder().code(500).message("服务器错误").build(); + } +} +``` + +### 处理用户登出 + +在Shiro中,用户登出的逻辑已经是比较完善了,但是如果需要在用户登录的时候额外完成其他的操作,那么就需要自行编写一个Controller来进行处理了。比如在下面的示例中,登出操作将删除已经存储的用户令牌。 + +```java +@PostMapping("/logout") +@Transactional +public BaseResponse doLogout() { + var subject = SecurityUtils.getSubject(); + var token = storeRepository.findTokenByMember((Member) subject.getPrincipal()); + storeRepository.delete(token); + return BaseResponse.builder().code(200).build(); +} +``` + +### 控制用户访问 + +在某一个路径上对用户进行控制,可以通过两种方法完成: + +1. 在`ShiroFilterChainDefinition`实例中定义。 +1. 使用Shiro提供的注解定义。 + +#### 在FilterChainDefinition中定义 + +这种定义方式主要是通过`.addPathDefinition()`方法来完成,这个方法的第二个参数不仅可以定义使用哪个Filter来进行处理,还可以利用逗号分隔的描述声明这条路径所需要的角色或者权限限制。角色或者权限的描述可以使用以下表达式来书写。 + +1. `roles[role_name,role_name]`,定义访问路径所需要具备的角色名称。 +1. `perms[perm_name,perm_name]`,定义访问路径所需要具备的权限名称。 + +!!! info "关于多级权限描述" + Shiro的权限名称是支持多级描述的,每一级描述之间使用`:`隔开,例如`document:read`和`document:write`。多级权限描述可以使用通配符进行广泛的匹配,例如`document:*`可以匹配`document:read`和`document:write`。 + +例如可以这样来定义一个路径所需的权限。 + +```java +filterChainDefinition.addPathDefinition("/document/:id", "authc,perms[document:read]") +``` + +#### 使用注解定义 + +注解一般都是使用在Controller上的,这一点跟Spring Security是一样的。注解中所使用的角色和权限的描述表达式,跟在FilterChainDefinition中是一样的,只是将`[]`中的内容转移到了注解中。Shiro提供了以下两个注解来定义访问某一个Controller所需要的权限。 + +1. `@RequiresRoles()`,定义Controller所需要的角色名称。 +1. `@requiredPermissions()`,定义Controller所需要的权限名称。 + +例如在Controller里需要`document:read`权限的Controller方法就可以这样编写。 + +```java +@RequiresPermissions("document:read") +@GetMapping("/document/:id") +public DocuemtResponse readDocument(@PathVariable String docId) { + // ... +} +```