前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >二十分钟了解Shiro登录流程

二十分钟了解Shiro登录流程

作者头像
秃头哥编程
发布2020-05-07 10:36:55
1.5K0
发布2020-05-07 10:36:55
举报
文章被收录于专栏:秃头哥编程秃头哥编程

前言

本文假设读者能正常使用Shiro, 并对知道相关类是做什么用的。

这里截取部分代码来追踪, 为了尽可能的简单, 这里没有使用Spring等其他框架, 纯粹的Shiro代码。

本文使用ini配置, 但不解析IniRealm内部逻辑。

单元测试例子

具体可以看IniRealmTest

代码语言:javascript
复制
public class IniRealmTest {    @Test    public void test() {        // 1. 加载 ini 配置, 初始化 SecurityManager        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");        SecurityManager securityManager = factory.getInstance();        SecurityUtils.setSecurityManager(securityManager);
        // 2. 获取 Subject        Subject subject = SecurityUtils.getSubject();                // 3. 使用帐号密码登录, 并创建 Session        UsernamePasswordToken trueToken = new UsernamePasswordToken("username", "password");        try {            subject.login(trueToken);        } catch (AuthenticationException e) {            Assertions.fail();        }
        // 4. 角色判断        Assertions.assertTrue(subject.hasRole("role1"));
        // 5. 权限判断        Assertions.assertTrue(subject.isPermitted("permission1"));
        // 6. 登出注销        subject.logout();    }}
代码语言:javascript
复制
[users]username = password,role1,role2
[roles]role1=permission1,permission2role2=permission3,permission4

登录逻辑就分为上述的六个步骤, 接下来一个个拆解。

获取安全管理器securityManager

首先我们看SecurityManager的获取方法factory.getInstance();

以下代码省略部分代码, 保留核心逻辑。

代码语言:javascript
复制
public abstract class AbstractFactory<T> implements Factory<T> {    private T singletonInstance;    public T getInstance() {        // 1. 交由子类实现代码        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() {        // 1. 从 classpath:shiro.ini 获取 ini 配置        Ini ini = resolveIni();        // 2. 根据 ini 配置初始化 SecurityManager        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) {        // 1. 创建默认的 DefaultSecurityManager 和 IniRealm, 保存到这个 Map 里        Map<String, ?> defaults = createDefaults(ini, mainSection);        // 2. 初始化 ReflectionBuilder, 并往 Map 里塞了一个 DefaultEventBus        Map<String, ?> objects = buildInstances(mainSection, defaults);            // 3. 获取上面创建的 DefaultSecurityManager        SecurityManager securityManager = getSecurityManagerBean();            // 4. 获取上面的 IniRealm 注入到 DefaultSecurityManager 中        Collection<Realm> realms = getRealms(objects);        ((RealmSecurityManager) securityManager).setRealms(realms);            return securityManager;    }}

可以看到, 创建SecurityManager的过程主要做了三件事

  1. 创建默认的DefaultSecurityManager
  2. 根据shiro.ini配置文件, 初始化IniRealm
  3. IniRealm注入到DefaultSecurityManager中。

获取当前主体Subject

接下来获取Subject

代码语言:javascript
复制
public abstract class SecurityUtils {    private static SecurityManager securityManager;    public static Subject getSubject() {        // 1. 从 ThreadLocal 获取 Subject, 第一次肯定获取不到, 需要去创建        Subject subject = ThreadContext.getSubject();        if (subject == null) {            // 2. 初始化 Subject            subject = (new Subject.Builder()).buildSubject();            // 3. 缓存到 ThreadLocal 中            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, 所以需要创建, 跟踪源码可以看到调用了安全管理器SecurityManagercreateSubject方法。

代码语言:javascript
复制
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(principals, authenticated, host, session, sessionCreationEnabled, securityManager);        return new DelegatingSubject(null, false, null, null, true, securityManager);    }}

找到最后, 是调用了一个DefaultSubjectFactory工厂, 来创建DelegatingSubject 因为我们什么高大上的配置都没填, 所以就直接nullfalse来填充所需字段了。

重头戏身份认证Login

全部准备就绪了, 就开始登录吧subject.login(trueToken)。

代码语言:javascript
复制
public class DelegatingSubject implements Subject {    public void login(AuthenticationToken token) throws AuthenticationException {        // 调用 SecurityManager        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 {        // 调用 ModularRealmAuthenticator 认证器        return this.authenticator.authenticate(token);    }}

我们可以看到login()方法实际上是调用了ModularRealmAuthenticator类的authenticate()方法。

ModularRealmAuthenticator认证器默认内置了AtLeastOneSuccessfulStrategy的认证策略。

看名字可以猜到, 只要有一个Realm验证通过, 那就验证通过了. 我们目前只有一个IniRealm, 所以不用管这个认证策略。

代码语言:javascript
复制
public abstract class AbstractAuthenticator implements Authenticator, LogoutAware {    public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {        // 交由 ModularRealmAuthenticator 实现        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) {            // 我们目前只有一个 IniRealm             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;    }}

可以看到我们调用了RealmgetAuthenticationInfo()方法, 但是这个方法和我们平常开发时重写的doGetAuthenticationInfo()方法不同.getAuthenticationInfo()内部肯定是调用了doGetAuthenticationInfo()方法. 我们继续往里面。

代码语言:javascript
复制
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {        // 1. 获取缓存中的 Authentication, 第一次肯定获取不到        AuthenticationInfo info = getCachedAuthenticationInfo(token);        if (info == null) {            // 2. 自定义 Realm 的实现            info = doGetAuthenticationInfo(token);        }        // 3. 用户输入的密码和数据库中的密码进行比较, 可以在这里做加盐加密        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 {        // 默认实现类 SimpleCredentialsMatcher        CredentialsMatcher cm = getCredentialsMatcher();        if (!cm.doCredentialsMatch(token, info)) {            throw new IncorrectCredentialsException("错误");        }    }}

doGetAuthenticationInfo()就是我们自定义Realm要实现的方法。 至此, 整个身份验证流程就走通了。

权限认证

我们重写Realm除了doGetAuthenticationInfo()还要重写doGetAuthorizationInfo().但是我们上面身份认证只执行了doGetAuthenticationInfo(). 可以很容易猜到, Shiro使用了懒加载的方式去加载角色权限。

还是老办法看源码, 看subject.hasRole()方法。

代码语言:javascript
复制
public class DelegatingSubject implements Subject {    public boolean hasRole(String roleIdentifier) {        // 又是调用 SecurityManager        return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier);    }}public abstract class AuthorizingSecurityManager extends AuthenticatingSecurityManager {    public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {        // 又是调用 ModularRealmAuthenticator, 然后调用 AuthorizingRealm        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);    }}

鉴权就比较简单了, 我们直接一撸到底, 方法调来调去, 最后就是调用到RealmgetAuthorizationInfo()方法。 和之前一样, 内部肯定也是调用了doGetAuthorizationInfo()方法。

代码语言:javascript
复制
public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {    protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {        AuthorizationInfo info = null;
        // 1. 从缓存中获取, 第一次肯定没有        Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();        info = cache.get(key);
        // 2. 调用 自定义 Realm 的 doGetAuthorizationInfo(), 然后缓存        if (info == null) {            info = doGetAuthorizationInfo(principals);        }        return info;    }}

注销logout

注销也很简单, 就是把之前初始化的数据都清空就好了。调用Subject.logout()注销。

代码语言:javascript
复制
public class DelegatingSubject implements Subject {    public void logout() {        try {            clearRunAsIdentitiesInternal();            this.securityManager.logout(this);        } finally {            this.session = null;            this.principals = null;            this.authenticated = false;            //https://issues.apache.org/jira/browse/JSEC-22            //this.securityManager = null;        }    }}public class DefaultSecurityManager extends SessionsSecurityManager {    public void logout(Subject subject) {        // 1. 删除 remember me        beforeLogout(subject);
        // 2. 清除缓存        PrincipalCollection principals = subject.getPrincipals();        if (principals != null && !principals.isEmpty()) {            Authenticator authc = getAuthenticator();            if (authc instanceof LogoutAware) {                ((LogoutAware) authc).onLogout(principals);            }        }        // 3. 从持久层删除 subject        delete(subject);        // 4. 停止 session        stopSession(subject);    }}

总结

还有一些高级特性, 比如多Realm登陆, 单点登录, Redis持久化Session. 这里就不说了。Shrio源码比起Spring的简单多了, 用久了其实都知道的七七八八, 阅读源码也就是个查缺补漏。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 秃头哥编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档