前言
本文假设读者能正常使用Shiro
, 并对知道相关类是做什么用的.
这里截取部分代码来追踪, 为了尽可能的简单, 这里没有使用Spring
等其他框架, 纯粹的Shiro
代码.
本文使用ini
配置, 但不解析IniRealm
内部逻辑.
单元测试例子
具体可以看IniRealmTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class IniRealmTest { @Test public void test() { Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken trueToken = new UsernamePasswordToken("username", "password"); try { subject.login(trueToken); } catch (AuthenticationException e) { Assertions.fail(); }
Assertions.assertTrue(subject.hasRole("role1"));
Assertions.assertTrue(subject.isPermitted("permission1"));
subject.logout(); } }
|
1 2 3 4 5 6
| [users] username = password,role1,role2
[roles] role1=permission1,permission2 role2=permission3,permission4
|
登录逻辑就分为上述的六个步骤, 接下来一个个拆解.
获取安全管理器 SecurityManager
首先我们看SecurityManager
的获取方法factory.getInstance();
.
以下代码省略部分代码, 保留核心逻辑.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| public abstract class AbstractFactory<T> implements Factory<T> { private T singletonInstance; public T getInstance() { singletonInstance = createInstance(); return singletonInstance; } protected abstract T createInstance(); } public abstract class IniFactorySupport<T> extends AbstractFactory<T> { public static final String DEFAULT_INI_RESOURCE_PATH = "classpath:shiro.ini"; public T createInstance() { Ini ini = resolveIni(); T instance = createInstance(ini); return instance; } protected abstract T createInstance(Ini ini); } public class IniSecurityManagerFactory extends IniFactorySupport<SecurityManager> { public static final String MAIN_SECTION_NAME = "main"; public static final String SECURITY_MANAGER_NAME = "securityManager"; public static final String INI_REALM_NAME = "iniRealm"; protected SecurityManager createInstance(Ini ini) { SecurityManager securityManager = createSecurityManager(ini); return securityManager; } private SecurityManager createSecurityManager(Ini ini) { Ini.Section mainSection = ini.getSection(MAIN_SECTION_NAME); if (CollectionUtils.isEmpty(mainSection)) { mainSection = ini.getSection(Ini.DEFAULT_SECTION_NAME); } return createSecurityManager(ini, mainSection); } private SecurityManager createSecurityManager(Ini ini, Ini.Section mainSection) { Map<String, ?> defaults = createDefaults(ini, mainSection); Map<String, ?> objects = buildInstances(mainSection, defaults); SecurityManager securityManager = getSecurityManagerBean(); Collection<Realm> realms = getRealms(objects); ((RealmSecurityManager) securityManager).setRealms(realms); return securityManager; } }
|
可以看到, 创建SecurityManager
的过程主要做了三件事
- 创建默认的
DefaultSecurityManager
- 根据
shiro.ini
配置文件, 初始化IniRealm
- 将
IniRealm
注入到DefaultSecurityManager
中.
获取当前主体 Subject
接下来获取Subject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public abstract class SecurityUtils { private static SecurityManager securityManager; public static Subject getSubject() { Subject subject = ThreadContext.getSubject(); if (subject == null) { subject = (new Subject.Builder()).buildSubject(); ThreadContext.bind(subject); } return subject; } } public interface Subject { public static class Builder { SubjectContext subjectContext = new DefaultSubjectContext(); public Subject buildSubject() { return SecurityUtils.securityManager.createSubject(this.subjectContext); } } }
|
第一次我们肯定获取不到Subject
, 所以需要创建, 跟踪源码可以看到调用了安全管理器SecurityManager
的createSubject
方法.
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class DefaultSecurityManager extends SessionsSecurityManager { public Subject createSubject(SubjectContext subjectContext) { SubjectContext context = new DefaultSubjectContext(subjectContext); Subject subject = subjectFactory.createSubject(context); return subject; } } public class DefaultSubjectFactory implements SubjectFactory { public Subject createSubject(SubjectContext context) {
return new DelegatingSubject(null, false, null, null, true, securityManager); } }
|
找到最后, 是调用了一个DefaultSubjectFactory
工厂, 来创建DelegatingSubject
.
因为我们什么高大上的配置都没填, 所以就直接null
和false
来填充所需字段了.
重头戏身份认证 login
全部准备就绪了, 就开始登录吧subject.login(trueToken)
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class DelegatingSubject implements Subject { public void login(AuthenticationToken token) throws AuthenticationException { Subject subject = securityManager.login(this, token); } } public class DefaultSecurityManager extends SessionsSecurityManager { private Authenticator authenticator = new ModularRealmAuthenticator(); public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = authenticate(token); return loggedIn; } public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { return this.authenticator.authenticate(token); } }
|
我们可以看到login()
方法实际上是调用了ModularRealmAuthenticator
类的authenticate()
方法.
ModularRealmAuthenticator
认证器默认内置了AtLeastOneSuccessfulStrategy
的认证策略.
看名字可以猜到, 只要有一个Realm
验证通过, 那就验证通过了. 我们目前只有一个IniRealm
, 所以不用管这个认证策略.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public abstract class AbstractAuthenticator implements Authenticator, LogoutAware { public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = doAuthenticate(token); return info; } } public class ModularRealmAuthenticator extends AbstractAuthenticator { protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { Collection<Realm> realms = getRealms(); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } } protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { if (!realm.supports(token)) { throw new UnsupportedTokenException(msg); } AuthenticationInfo info = realm.getAuthenticationInfo(token); if (info == null) { throw new UnknownAccountException(msg); } return info; } }
|
可以看到我们调用了Realm
的getAuthenticationInfo()
方法, 但是这个方法和我们平常开发时重写的doGetAuthenticationInfo()
方法不同.
getAuthenticationInfo()
内部肯定是调用了doGetAuthenticationInfo()
方法. 我们继续往里面.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public abstract class AuthenticatingRealm extends CachingRealm implements Initializable { public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = getCachedAuthenticationInfo(token); if (info == null) { info = doGetAuthenticationInfo(token); } if (info != null) { assertCredentialsMatch(token, info); } return info; } protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException; protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { CredentialsMatcher cm = getCredentialsMatcher(); if (!cm.doCredentialsMatch(token, info)) { throw new IncorrectCredentialsException("错误"); } } }
|
doGetAuthenticationInfo()
就是我们自定义Realm
要实现的方法.
至此, 整个身份验证流程就走通了.
权限认证
我们重写Realm
除了doGetAuthenticationInfo()
还要重写doGetAuthorizationInfo()
.
但是我们上面身份认证只执行了doGetAuthenticationInfo()
. 可以很容易猜到, Shiro
使用了懒加载的方式去加载角色权限.
还是老办法看源码, 看subject.hasRole()
方法.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class DelegatingSubject implements Subject { public boolean hasRole(String roleIdentifier) { return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier); } } public abstract class AuthorizingSecurityManager extends AuthenticatingSecurityManager { public boolean hasRole(PrincipalCollection principals, String roleIdentifier) { return this.authorizer.hasRole(principals, roleIdentifier); } } public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware { public boolean hasRole(PrincipalCollection principal, String roleIdentifier) { AuthorizationInfo info = getAuthorizationInfo(principal); return hasRole(roleIdentifier, info); } protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) { return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier); } }
|
鉴权就比较简单了, 我们直接一撸到底, 方法调来调去, 最后就是调用到Realm
的getAuthorizationInfo()
方法.
和之前一样, 内部肯定也是调用了doGetAuthorizationInfo()
方法.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware { protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) { AuthorizationInfo info = null;
Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache(); info = cache.get(key);
if (info == null) { info = doGetAuthorizationInfo(principals); } return info; } }
|
注销 logout
注销也很简单, 就是把之前初始化的数据都清空就好了. 调用Subject.logout()
注销.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class DelegatingSubject implements Subject { public void logout() { try { clearRunAsIdentitiesInternal(); this.securityManager.logout(this); } finally { this.session = null; this.principals = null; this.authenticated = false; } } } public class DefaultSecurityManager extends SessionsSecurityManager { public void logout(Subject subject) { beforeLogout(subject);
PrincipalCollection principals = subject.getPrincipals(); if (principals != null && !principals.isEmpty()) { Authenticator authc = getAuthenticator(); if (authc instanceof LogoutAware) { ((LogoutAware) authc).onLogout(principals); } } delete(subject); stopSession(subject); } }
|
总结
还有一些高级特性, 比如多Realm
登陆, 单点登录, Redis
持久化Session
. 这里就不说了.
Shrio
源码比起Spring
的简单多了, 用久了其实都知道的七七八八, 阅读源码也就是个查缺补漏.