spring集成shiro登陆流程(上)

Posted qiaozhuangshi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了spring集成shiro登陆流程(上)相关的知识,希望对你有一定的参考价值。

上一篇已经分析了shiro的入口filter是SpringShiroFilter, 那么它的doFilter在哪儿呢?

技术图片

我们看到它的直接父类AbstractShrioFilter继承了OncePerRequestFilter类,该类是shiro内置的大部分filter的父类(抽像公共部分),在该类中定义了doFilter方法

OncePerRequestFilte

public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
    //当前过滤器的名字 String alreadyFilteredAttributeName
= getAlreadyFilteredAttributeName();
     //如果该过滤器执行过,那么将不执行同一个名字的过滤器 直接执行过滤链中的下一个过滤器
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) { filterChain.doFilter(request, response);
}
else if (!isEnabled(request, response) || shouldNotFilter(request) ) {
        //如果当前过滤器设置了enabled属性为false,则不执行,直接执行过滤链中的下一个过滤器
filterChain.doFilter(request, response); } else { //标志当前过滤器已经执行过 request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try {
          //1、 核心方法 doFilterInternal(request, response, filterChain); }
finally { //过滤链执行完毕后,清空request中的过滤链执行记录 request.removeAttribute(alreadyFilteredAttributeName); } } }

由于我们是通过SpringShiroFilter拦截进来的那么会调用AbstractShrioFilter中的doFilterInternal

AbstractShrioFilter

protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
        throws ServletException, IOException {
            
     //封装容器的request和response为shiro自己的 其中在request中标识了当前不为servlet容器的session (在创建session时会用到servlet容器调用getSession()时 )
    final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
    final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
    //2、 创建subject(可以看出每次请求都会创建一个Subject对象)
    final Subject subject = createSubject(request, response);

    //执行过滤链
    subject.execute(new Callable() {
        public Object call() throws Exception {
            updateSessionLastAccessTime(request, response);  //修改session的最后活动时间
            executeChain(request, response, chain);  //执行过滤链
            return null;
        }
    });
}
//创建subject对象
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
    return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}
//7、执行过滤链
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
        throws IOException, ServletException {
    //获取当前请求对应的过滤链
    FilterChain chain = getExecutionChain(request, response, origChain);
    chain.doFilter(request, response);
}

WebSubject

//3、
public
Builder(SecurityManager securityManager, ServletRequest request, ServletResponse response) { //每次都创建subject上下文(subjectContext), 并设置securityManager对象 super(securityManager); //将request和response添加到subject上下文(subjectContext), 即上面创建的对象 setRequest(request); setResponse(response); } //4、创建 public WebSubject buildWebSubject() { Subject subject = super.buildSubject(); return (WebSubject) subject; }

Subject


//5、调用DefaultSecurityManager的createSubject方法
public Subject buildSubject() {
  
return this.securityManager.createSubject(this.subjectContext); }

 DefaultSecurityManager

// 6
public
Subject createSubject(SubjectContext subjectContext) { //web的subjectContext时,会重新创建一个新的,其他的(ini等),只是copy SubjectContext context = copy(subjectContext); //验证是否subject上下文中有securityMangary对象,如果没有创建一个 context = ensureSecurityManager(context); //将session放入subjectContext 该session会从cookie或者rul上带的JSESSIONID(默认) 注意:第一次访问项目来到这儿没有session context = resolveSession(context); //校验用户登陆信息, 并放入context, 如果subject,session,和授权认证AuthenticationInfo中都没有,将会从rememberMeManager(cookie)中获取
   //注意:第一次访问项目来到这儿没有这些信息
context = resolvePrincipals(context); //创建一个WebDelegatingSubject对象 Subject subject = doCreateSubject(context); //保存当前认证的用户信息(一般是用户名)在session里,并标记当前用户已经被认证过 为了下次remember使用 save(subject); return subject; }
// 从context中获取session protected SubjectContext resolveSession(SubjectContext context) { Session session = resolveContextSession(context); if (session != null) { context.setSession(session); } return context; } protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException { //调用的下面子类DefaultWebSecurityManager的方法 SessionKey key = getSessionKey(context); if (key != null) { //调用 SessionsSecurityManager#getSession return getSession(key); } return null; }
//登陆 public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { info = authenticate(token); } catch (AuthenticationException ae) { onFailedLogin(token, ae, subject); } Subject loggedIn = createSubject(token, info, subject); //登陆成功后 根据配置的"记住我" 保存认证信息 onSuccessfulLogin(token, info, loggedIn); return loggedIn; } protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) { rememberMeSuccessfulLogin(token, info, subject); } protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) { //获取 rememberMeManager管理器 RememberMeManager rmm = getRememberMeManager(); rmm.onSuccessfulLogin(subject, token, info); }

 DefaultWebSecurityManager 

//创建sessionKey
@Override
protected SessionKey getSessionKey(SubjectContext context) { //从context中获取sessonId和request,response if (WebUtils.isWeb(context)) { Serializable sessionId = context.getSessionId(); ServletRequest request = WebUtils.getRequest(context); ServletResponse response = WebUtils.getResponse(context); return new WebSessionKey(sessionId, request, response); } else { ... } } // 第一次调用时 getSession方法最后会调用到这儿来 返回null protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException { Serializable sessionId = getSessionId(sessionKey); if (sessionId == null) { return null; } Session s = retrieveSessionFromDataSource(sessionId); if (s == null) { //session ID was provided, meaning one is expected to be found, but we couldn‘t find one: String msg = "Could not find session with ID [" + sessionId + "]"; throw new UnknownSessionException(msg); } return s; }

 

现在开始执行请求路径对应的过滤器

 由于过滤链中的过滤器也是OncePerRequestFilte的子类,继续走OncePerRequestFilte#doFilter方法 然后会调用第一步doFilterInternal方法

  我们自定义的方法一般也是继承了AdviceFilter过滤器

AdviceFilter

public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        Exception exception = null;
        try {
       //8、执行前置方法
boolean continueChain = preHandle(request, response);        if (continueChain) { executeChain(request, response, chain); } postHandle(request, response); if (log.isTraceEnabled()) { log.trace("Successfully invoked postHandle method"); } } catch (Exception e) { exception = e; } finally { cleanup(request, response, exception); } }

 

AccessControlFilter

public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
    //9、是登陆的rul或者已经认证过 否则重定向到登陆页面
    return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}

 

UserFilter

(这里以user过滤器为例,如果没有认证过,直接重定向到登陆url)

该过滤器重写了这两个方法

//10、判断是否认证过
protected
boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
  //判断当前请求的路径是否为当前过滤器配置的登陆路径(登陆不需要任何权限,返回true)
if (isLoginRequest(request, response)) { return true; } else {
    //判断是否已经认证过 Subject subject
= getSubject(request, response); return subject.getPrincipal() != null; } } //11、返回false protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
  //重定向到登陆rul saveRequestAndRedirectToLogin(request, response);
return false; }
//12、
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException { //调用webUtils的方法,将当前请求失败的的信息保存起来(便于下次认证成功后直接重定向到该路径) saveRequest(request); //重定向的时候会生成session(shrio的) redirectToLogin(request, response); }

 

 WebUtils

//保存
public
static void saveRequest(ServletRequest request) { Subject subject = SecurityUtils.getSubject(); //13、这里会创建一个 StoppingAwareProxiedSession AbstractNativeSessionManager#start是创建simpleSession并调用session监听器
  //会将sessinID存在cookie中和sessionDao中(默认时ehcache缓存,可以自己实现redis等)(在DefaultSessionManager#create(Session session)方法中)
Session session = subject.getSession(); HttpServletRequest httpRequest = toHttp(request); //将当前目标路径的请求信息保存起来 SavedRequest savedRequest = new SavedRequest(httpRequest); //存到session中,跳到登陆页面后登陆成功后会重定向到此次失败的路径 session.setAttribute(SAVED_REQUEST_KEY, savedRequest); } //重定向到savedRequet保存的路径 如果是直接访问的登陆url,则直接重定向到当前过滤器配置的登陆成功url //成功后的重定向可是不生成session的 public static void redirectToSavedRequest(ServletRequest request, ServletResponse response, String fallbackUrl) throws IOException { String successUrl = null; boolean contextRelative = true; //从session中获取,并清空上一次失败保存的信息 SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request); //上一次请求失败的保存的对象 而且是get请求(这里一般是直接浏览器输入的url) 如果是post请求过来的(一般是表单),直接返回目标路径 if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase(AccessControlFilter.GET_METHOD)) { successUrl = savedRequest.getRequestUrl(); contextRelative = false; }
  //第一次请求时,successUrl为null, 登陆成功后,有值(上一次失败的url)
if (successUrl == null) { successUrl = fallbackUrl; }
  //15、发出重定向 WebUtils.issueRedirect(request, response, successUrl,
null, contextRelative); } public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative) throws IOException { issueRedirect(request, response, url, queryParams, contextRelative, true); } // 会把sessionID写在rul上, 下面的内容就不带大家看了 public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative, boolean http10Compatible) throws IOException { RedirectView view = new RedirectView(url, contextRelative, http10Compatible); view.renderMergedOutputModel(queryParams, toHttp(request), toHttp(response)); }

 

 SavedRequest

// SavedRequest的构造方法
public SavedRequest(HttpServletRequest request) {
  //当前请求的方式(get|post...)
this.method = request.getMethod();
  //当前请求的参数
this.queryString = request.getQueryString();
  //当前请求失败的路径
this.requestURI = request.getRequestURI(); }

 

RedirectView (拼接重定向的参数请求头、url加sessionID,url编码等操作都由这儿进入)

public final void renderMergedOutputModel(
            Map model, HttpServletRequest request, HttpServletResponse response) throws IOException {

        // Prepare name URL.
        StringBuilder targetUrl = new StringBuilder();
        if (this.contextRelative && getUrl().startsWith("/")) {
            targetUrl.append(request.getContextPath());
        }
        targetUrl.append(getUrl());
     //拼接请求参数
appendQueryProperties(targetUrl, model, this.encodingScheme); sendRedirect(request, response, targetUrl.toString(), this.http10Compatible); }

 

AbstractNativeSessionManager

//14、创建session时会调用
public
Session start(SessionContext context) { //创建simpleSession Session session = createSession(context); //重置session时间 applyGlobalSessionTimeout(session);
  //会将sessionID存到cookie onStart(session, context);
//调用session的Listner notifyStart(session); //Don‘t expose the EIS-tier Session object to the client-tier: return createExposedSession(session, context); }

 

那么此时一个没有授权的请求就执行完毕,现在就来到了我们的登陆界面

  登陆使用authc过滤器

 

FormAuthenticationFilter

和上面的过程一样,会判断是否认证,如果没有会执行onAccessDenied方法

// 这里需要该过滤器的 登陆url 和登陆所在的界面的url一样
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    //条件: 配置的该过滤器的登陆路径和请求路径相同
    if (isLoginRequest(request, response)) {
        //1、HttpServletRequest  2、post请求
        if (isLoginSubmission(request, response)) {
            return executeLogin(request, response);
        } else {
            //登陆页面的url 请求方式为get
            return true;
        }
    } else {
        //如果一个请求路径配置的authc过滤器,然后没有登陆直接调用,会走到这里
        //重定向到登陆页面  会创建一个StoppingAwareProxiedSession类型的session 并把sessionId放在登陆页面的url上
        saveRequestAndRedirectToLogin(request, response);
        return false;
    }
}
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { //调用 new UsernamePasswordToken(username, password, rememberMe, host); AuthenticationToken token = createToken(request, response); try { Subject subject = getSubject(request, response); subject.login(token); return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) { return onLoginFailure(token, e, request, response); } } //登陆成功后 重定向到上一次重定向过来的路径或者当前过滤器的登陆路径 //可以重写该方法登陆后直接跳到当前过滤器配置的url 而不是上一次失败的url protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { //调用父类AuthenticationFilter的issueSuccessRedirect方法 issueSuccessRedirect(request, response); //重定向后,阻止过滤连调用 return false; }

 

AuthenticationFilter

protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws Exception {
    //当前过滤器配置的登陆url
    WebUtils.redirectToSavedRequest(request, response, getSuccessUrl());
}

DelegatingSubject

public void login(AuthenticationToken token) throws AuthenticationException {
    clearRunAsIdentitiesInternal();
    //委托给securiManager登陆
    Subject subject = securityManager.login(this, token);
    PrincipalCollection principals;
    String host = null;
    if (subject instanceof DelegatingSubject) {
        DelegatingSubject delegating = (DelegatingSubject) subject;
        //认证信息
        principals = delegating.principals;
        host = delegating.host;
    } else {
        principals = subject.getPrincipals();
    }
    this.principals = principals;
    //标记已经登陆过
    this.authenticated = true;
    if (token instanceof HostAuthenticationToken) {
        host = ((HostAuthenticationToken) token).getHost();
    }
    if (host != null) {
        this.host = host;
    }
    //获取登陆时的session
    Session session = subject.getSession(false);
    if (session != null) {
        //执行new StoppingAwareProxiedSession(session, this);  登陆后的session封装成StoppingAwareProxiedSession代理对象
        this.session = decorate(session);
    } else {
        this.session = null;
    }
}

 

AbstractRememberMeManager  处理 remeberme

public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
    //清空之前的认证信息
    forgetIdentity(subject);

    //如果是rememberMe类型的token
    if (isRememberMe(token)) {
        //记录
        rememberIdentity(subject, token, info);
    }
}
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
    //从认证后的信息中获取
    PrincipalCollection principals = getIdentityToRemember(subject, authcInfo);
    rememberIdentity(subject, principals);
}
// 加密处理
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
    byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
    rememberSerializedIdentity(subject, bytes);
}
//使用CipherService类进行处理
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
    byte[] bytes = serialize(principals);
    if (getCipherService() != null) {
        bytes = encrypt(bytes);
    }
    return bytes;
}
// 返回加密后的认证信息
protected byte[] encrypt(byte[] serialized) {
    byte[] value = serialized;
    CipherService cipherService = getCipherService();
    if (cipherService != null) {
        // getEncryptionCipherKey()  获取的是rememberMe cookie加密和解密的密钥
        ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
        value = byteSource.getBytes();
    }
    return value;
}
//加密
protected byte[] encrypt(byte[] serialized) {
    byte[] value = serialized;
    CipherService cipherService = getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
        value = byteSource.getBytes();
    }
    return value;
}

 

CookieRememberMeManager

protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
    HttpServletRequest request = WebUtils.getHttpRequest(subject);
    HttpServletResponse response = WebUtils.getHttpResponse(subject);

    //base 64 encode it and store as a cookie:
    String base64 = Base64.encodeToString(serialized);
    //rememberMe的cookie模板  key为自定义的名字  我这儿是rememberMe
    Cookie template = getCookie();
    Cookie cookie = new SimpleCookie(template);
    cookie.setValue(base64);
    cookie.saveTo(request, response);
}

 

下面是remember的配置

<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <constructor-arg value="rememberMe"/>
    <property name="httpOnly" value="true"/>
    <property name="maxAge" value="2592000"/><!-- 30天 -->
</bean>

<!-- rememberMe管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
    <!-- rememberMe cookie加密和解密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)-->
    <property name="cipherKey"
              value="#{T(org.apache.shiro.codec.Base64).decode(‘4AvVhmFLUs0KTA3Kprsdag==‘)}"/>
    <property name="cookie" ref="rememberMeCookie"/>
</bean>

 

由于篇幅原因,本节详细介绍的是登陆需要验证的请求跳转到登陆界面的源码解析

小结:

  1、当第一次请求失败后,会重定向到当前过滤器的登陆界面,并创建一个session,将sessinID存在cookie,重定向的url,还会存放在sessionDao中(默认是ehcache, 可自定义)

  2、当请求的路径为不用认证(anon等自定义preHandle返回true的路径),也会由servlet容器调用shiroRequest的getSession方法创建一个session,保存位置同1

 

以上是关于spring集成shiro登陆流程(上)的主要内容,如果未能解决你的问题,请参考以下文章

Shiro:Spring-boot如何集成Shiro(上)

shiro集成spring&工作流程&DelegatingFilterProxy

Spring Security+JWT简述

shiro退出登陆清空缓存实现

Spring Boot 入门:集成 AOP 进行日志管理

SpringBoot集成Shiro