Shiro源码解析

一、从登录到调取数据源

1.获取主体、生成认证token

Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
	user.getUserName(),
	user.getPassword()
);

2.进行登录验证

subject.login(usernamePasswordToken);

3.DelegatingSubject 代理主体
里面有 login() 的默认实现

public void login(AuthenticationToken token) throws AuthenticationException {
    clearRunAsIdentitiesInternal();  // 清除内部身份运行标识。。。什么鬼
    // 由此下探到安全管理器登录,携带上 代理主体 和 登录认证信息
    Subject subject = securityManager.login(this, token);
    PrincipalCollection principals;
    String host = null;
    // 强转成代理主体 / 
    if (subject instanceof DelegatingSubject) {
        DelegatingSubject delegating = (DelegatingSubject) subject;
        //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
        principals = delegating.principals;
        host = delegating.host;
    } else {
        principals = subject.getPrincipals();
    }
    if (principals == null || principals.isEmpty()) {
        String msg = "Principals returned from securityManager.login( token ) returned a null or " +
                "empty value.  This value must be non null and populated with one or more elements.";
        throw new IllegalStateException(msg);
    }
    this.principals = principals;
    this.authenticated = true;
    if (token instanceof HostAuthenticationToken) {
        host = ((HostAuthenticationToken) token).getHost();
    }
    if (host != null) {
        this.host = host;
    }
    Session session = subject.getSession(false);
    if (session != null) {
        this.session = decorate(session);
    } else {
        this.session = null;
    }
}

4.DefaultSecurityManager 默认安全管理器

 public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
        	// 获取数据源返回的认证信息对象
            info = authenticate(token);
        } catch (AuthenticationException ae) {
            try {
                onFailedLogin(token, ae, subject);
            } catch (Exception e) {
                if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an " +
                            "exception.  Logging and propagating original AuthenticationException.", e);
                }
            }
            throw ae; //propagate
        }

        Subject loggedIn = createSubject(token, info, subject);

        onSuccessfulLogin(token, info, loggedIn);

        return loggedIn;
    }

5.ModularRealmAuthenticator 调取数据源进行身份认证

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        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)) { // 判断 token 支持
            String msg = "Realm [" + realm + "] does not support authentication token [" +
                    token + "].  Please ensure that the appropriate Realm implementation is " +
                    "configured correctly or that the realm accepts AuthenticationTokens of this type.";
            throw new UnsupportedTokenException(msg);
        }
        // 通过数据源获取认证需要的数据信息对象(此处由自定义认证数据源返回)
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        if (info == null) {
            String msg = "Realm [" + realm + "] was unable to find account data for the " +
                    "submitted AuthenticationToken [" + token + "].";
            throw new UnknownAccountException(msg);
        }
        return info;
    }

二、自定义数据源

自定义数据源通常直接继承 AuthorizingRealm(授权数据源(抽象)),因为 AuthorizingRealm 直接继承 AuthenticatingRealm(认证数据源(抽象)),这样一次继承能就能重写 AuthorizingRealm 的获取授权信息方法 doGetAuthorizationInfo()AuthenticatingRealm的获取认证信息方法 doGetAuthenticationInfo()

public class UserRealm extends AuthorizingRealm {
	@Autowired
	private IService service;	// 注入业务

	/**
     * 授权
     */
 	@Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		// 授权逻辑,如查询角色、权限
		info.addRole(String role) // 往 info 对象里装填角色 (String形式)
		info.addStringPermission(String permission) // 往 info 对象里装填权限 (String形式)
		return info;
	}

	/**
     * 登录认证
     * AuthenticationException: 异常抛出用来做登录失败的后续处理
     * token: 由登录入口处 Subject.login(AuthenticationToken token) 传入
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		UsernamePasswordToken upToken = (UsernamePasswordToken) token; // 强转子类 UsernamePasswordToken,方便拿到用户名、密码
		try {
            user = loginService.login(username, password);        // 登录逻辑通常写在业务中,直接调用。也可以直接将业务写在此方法中
        } catch (CaptchaException e) { // 验证码异常         
            throw new AuthenticationException(e.getMessage(), e); // 抛出不同异常区分登录失败原因,方便后续处理
        } catch (UserNotExistsException e) { // 用户不存在
            throw new UnknownAccountException(e.getMessage(), e);
        } catch (UserPasswordNotMatchException e) { // 密码不匹配
            throw new IncorrectCredentialsException(e.getMessage(), e);
        } catch (UserPasswordRetryLimitExceedException e) { // 超出密码次数限制
            throw new ExcessiveAttemptsException(e.getMessage(), e);
        } catch (UserBlockedException e) { // 用户锁定
            throw new LockedAccountException(e.getMessage(), e);
        } catch (RoleBlockedException e) { // 用户所在角色锁定
            throw new LockedAccountException(e.getMessage(), e);
        } catch (Exception e) {
            log.info("对用户[" + username + "]进行登录验证..验证未通过{}", e.getMessage());
            throw new AuthenticationException(e.getMessage(), e);
        }
        // 走到这里就是通过了登录认证,登录成功。封装信息对象返回即可
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
        return info;
	}
}

三、自定义(接驳)过滤器

对于 Shiro 框架中的 AccessControlFilter,用接驳过滤器这个名称非常合适。就像轨道,一个接驳过滤器就是一个三叉轨道,控制数据的走向,多个过滤器连起来就形成了一条过滤器链,一条控制链。

AccessControlFilter 过程控制过滤器**

一般自定义一个过滤器插入 Shiro 框架的过滤链中,通常就是继承 AccessControlFilter 然后实现其控制方法。

1.内部函数
在这里插入图片描述
2.主要控制方法(也是需要我们写逻辑的方法)
AccessControlFilter的控制方法主要是指 onPreHandle()isAccessAllowed()onAccessDenied() 这三大方法

// 预处理方法,控制请求是否放行的主逻辑
// 先走 isAccessAllowed() 的逻辑,如果返回 true,直接放行,毋需经过 onAccessDenied() 处理
// 如果 isAccessAllowed() 返回 false,则后续经由 onAccessDenied() 处理,返回 true 放行, 返回 false 拦截
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
	return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}

一般我们不会动 onPreHandle(),也就是直接用 AccessControlFilteronPreHandle() 的默认实现逻辑;
需要我们自己实现的是isAccessAllowed() onAccessDenied() 这两个抽象方法,只需要写好自己需要的逻辑,用 boolean 返回值控制放行即可

protected abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;
protected abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;

四、Session处理

会话是 http 请求协议下服务器存储请求信息的载体,通过 Session 可以使 http 请求实现一种 keep alive 的伪长链接

1.java web 框架的请求处理

简化后大概是这个鬼样,在 springboot 框架里把 Servlet 当成 Controller 就好
在这里插入图片描述
可见 HttpServletRequest 对象就是个信息载体

2.HttpServletRequest 与 ServletRequest 与 Session 的关系

HttpServletRequest 接口中可以发现其继承了 ServletRequest 接口

public interface HttpServletRequest extends ServletRequest {...}

进入 ServletRequest 接口观察其声明的方法没有一个与 Session 有关
HttpServletRequest 接口中声明了两个与 Session 相关的方法

public interface HttpServletRequest extends ServletRequest {
	...
	public HttpSession getSession(boolean create);
	public HttpSession getSession();
	...
}

也就是说 Session 对象是专门为服务HttpServletRequest 接口而生的,也就是专门服务与 http 请求的,是为了解决 http 单向传递无法保存的缺陷,实现伪长链接而生

3.ShiroHttpServletRequest 对 HttpServletRequest 的实现

在 shiro 框架中,用 ShiroHttpServletRequest 类实现了 HttpServletRequest接口

public class ShiroHttpServletRequest extends HttpServletRequestWrapper {...}

实现了 getSession() 方法
在这里插入图片描述

public class ShiroHttpServletRequest extends HttpServletRequestWrapper {
	
 	public HttpSession getSession() {
        return getSession(true);
    }
    
 	public HttpSession getSession(boolean create) {

        HttpSession httpSession;

        if (isHttpSessions()) { // 是否为 HttpSession (默认为 HttpSession)
            httpSession = super.getSession(false); // 调取父类的 getSession(),但为空时不生成 session 对象
            if (httpSession == null && create) {   // session 为空
                //Shiro 1.2: assert that creation is enabled (SHIRO-266):
                if (WebUtils._isSessionCreationEnabled(this)) { // 允许 session 生成
                    httpSession = super.getSession(create); // 调取父类的 getSession(),为空时生成 session 对象
                } else {
                    throw newNoSessionCreationException();
                }
            }
        } else {
            boolean existing = getSubject().getSession(false) != null;
            
            if (this.session == null || !existing) {
                Session shiroSession = getSubject().getSession(create);
                if (shiroSession != null) {
                    this.session = new ShiroHttpSession(shiroSession, this, this.servletContext);
                    if (!existing) {
                        setAttribute(REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
                    }
                } else if (this.session != null) {
                    this.session = null;
                }
            }
            httpSession = this.session;
        }

        return httpSession;
    }
}

重写的 getSession() 方法就是当请求进来时,查询是否有对应的 Session 对象存在,如果有直接返回,没有则创建一个 Session 对象

4.Session的来源与用户绑定

现在知道服务器接收请求后会主动创建一个对应的 Session 对象保存对应的用户数据,但是 Session 是怎么和用户对接上的呢?是怎么准确地取到某个请求对应的 Session 对象呢?
先看 Session 的数据结构,这里的 Session 对象是指 shiro 框架的 ShiroHttpSession 类,它实现了 HttpSession 接口

HttpSession 接口是 Session 的标准接口,规定了一个 Session 应该有的基础方法,不同框架都是基于此接口去实现定义自己的 Session 对象,实现接口、行业的统一对接

先看 HttpSession 定义的基本方法
在这里插入图片描述
再看 ShiroHttpSession 里面除了对 HttpSession 的实现,还定义了一个常量

// 默认 session id 名
public static final String DEFAULT_SESSION_ID_NAME = "JSESSIONID";

来了,打开浏览器,访问登录页
可以看到请求头中的 Cookie 属性中包含了一个键值对,key 就是 “JSESSIONID”
ShiroHttpSession 中的 DEFAULT_SESSION_ID_NAME 常量的值对应上了
这就是 Session 和用户能对应上的秘密 , 靠的是 Cookie 这鬼东西
在这里插入图片描述

5.Cookie 哪里来

清空浏览器 Cookie 记录,重新访问服务
在过滤器中尝试获取请求中携带的 Cookie

HttpServletRequest httpServletRequest = (HttpServletRequest) request;
Cookie[] cookies = httpServletRequest.getCookies();

结果为空,就是第一次请求是没有携带 Cookie
在这里插入图片描述
再看浏览器控制台,响应头携带上了 JSESSIONID
在这里插入图片描述

不登陆,只是刷新登录页,再请求一次
这次请求就携带上 Cookie 了,而且 Cookie 对象里还有个 JSESSIONID
(可以看到 Cookie 就是一个键值对,request 对象是以数组的形式存储 Cookie的)
在这里插入图片描述
在这里插入图片描述
可以猜测,请求第一次进来,服务端接收到请求就创建了 Session 对象和 Cookie 对象
问题来了,shiro 框架中 Cookie 是什么时候创建的呢
我们追着 DEFAULT_SESSION_ID_NAME 这个常量找
在这里插入图片描述
发现 ShiroHttpServletResponse 有引用到这个常量,这个是 shiro 实现的响应对象
结合上面的请求,首先猜测是 Cookie 对象就是在请求进来,服务器创建 ShiroHttpServletResponse 对象的时候,在初始化的过程中完成了 Cookie对象(针对JSESSIONID)的创建
但看了下 ShiroHttpServletResponse 的代码发现不是的

ShiroHttpServletResponse 只有一个构造器,初始化时只是创建了 ServletContext 对象和 ShiroHttpServletRequest 对象 ,并没有做任何与 Cookie 相关的操作

 public ShiroHttpServletResponse(HttpServletResponse wrapped, ServletContext context, ShiroHttpServletRequest request) {
        super(wrapped);
        this.context = context;
        this.request = request;
}

而是在其转编码函数中的逻辑才出现了对 DEFAULT_SESSION_ID_NAME 常量的引用,设置 (针对JSSIONID) 的 Cookie

protected String toEncoded(String url, String sessionId) {
       //...
        StringBuilder sb = new StringBuilder(path);
        if (sb.length() > 0) { // session id param can't be first.
            sb.append(";");
            sb.append(DEFAULT_SESSION_ID_PARAMETER_NAME);
            sb.append("=");
            sb.append(sessionId);
        }
        //...
        return (sb.toString());
    }

也就是说, (针对JSSIONID) 的 Cookie 对象 是在调用 ShiroHttpServletResponse 的编码方法 toEncoded() 设置的。

结论
①请求进来服务时,就会创建对应的 ShiroHttpServletRequestShiroHttpServletResponseShiroHttpSession 三个对象
②当服务响应返回时,调用 ShiroHttpServletResponse 对象的转URL编码方法,期间把 (针对JSSIONID) 的 Cookie 加入其中,进行响应输出,发送给用户(浏览器)。
③浏览器拿到(针对JSSIONID) 的 Cookie 对象后保存为临时 Cookie,后面的访问将自动带上这个 Cookie

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值