本文介绍的内容需要对Shiro有一定了解,学习Shiro可查看跟开涛我学Shiro

解决问题步骤
  • 重写 DefaultWebSessionManager 命名为 DefaultHeaderSessionManager
  • 重写 CookieRememberMeManager 命名为 HeaderRememberMeManager
  • 重写 ShiroFilterFactoryBean,修改其中的默认Filters;
  • 修改配置文件,指定为重写的类。
重写DefaultWebSessionManager

DefaultWebSessionManager:用于Web环境的实现,可以替代ServletContainerSessionManager,自己维护着会话,直接废弃了Servlet容器的会话管理。
DefaultWebSessionManager默认实现中,是通过Cookie确定sessionId,重写时,只需要把获取sessionid的方式变更为在request header中获取即可。
新建 DefaultHeaderSessionManager 类并 extends DefaultSessionManager 以及 implements WebSessionManager

//省略 import 信息
/**
 * @author Created by yangyang on 2018/1/18.
 * e-mail :yangyang_666@icloud.com ; tel :18580128658 ;QQ :296604153
 */
public class DefaultHeaderSessionManager extends DefaultSessionManager implements WebSessionManager {

}

request header中,我使用x-auth-token进行sessionid标识,接下来直接展示当前类的详细实现

//省略 import 信息
public class DefaultHeaderSessionManager extends DefaultSessionManager implements WebSessionManager {

    // slf4j  logback
    private static final Logger log = LoggerFactory.getLogger(DefaultHeaderSessionManager.class);

    private final String X_AUTH_TOKEN = "x-auth-token";

    // 请求头中获取 sessionId 并把sessionId 放入 response 中
    private String getSessionIdHeaderValue(ServletRequest request, ServletResponse response) {
        if (!(request instanceof HttpServletRequest)) {
            log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie.  Returning null.");
            return null;
        } else {
            HttpServletRequest httpRequest = (HttpServletRequest) request;

            // 在request 中 读取 x-auth-token 信息  作为 sessionId

            String sessionId = httpRequest.getHeader(this.X_AUTH_TOKEN);

            // 每次读取之后 都把当前的 sessionId 放入 response 中

            HttpServletResponse httpResponse = (HttpServletResponse) response;

            if (StringUtils.isNotEmpty(sessionId)) {
                httpResponse.setHeader(this.X_AUTH_TOKEN, sessionId);
                log.info("Current session ID is {}", sessionId);
            }

            return sessionId;
        }
    }

    //获取sessionid
    private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
        String id = this.getSessionIdHeaderValue(request, response);

        //DefaultWebSessionManager 中代码 直接copy过来
        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
        }
        //不会把sessionid放在URL后
        request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, Boolean.FALSE);
        return id;
    }

    // 移除sessionid 并设置为 deleteMe 标识
    private void removeSessionIdHeader(HttpServletRequest request, HttpServletResponse response) {
        response.setHeader(this.X_AUTH_TOKEN, "deleteMe");
    }

    /**
     * 把sessionId 放入 response header 中
     * onStart时调用
     * 没有sessionid时 会产生sessionid 并放入 response header中
     */
    private void storeSessionId(Serializable currentId, HttpServletRequest ignored, HttpServletResponse response) {
        if (currentId == null) {
            String msg = "sessionId cannot be null when persisting for subsequent requests.";
            throw new IllegalArgumentException(msg);
        } else {
            String idString = currentId.toString();

            response.setHeader(this.X_AUTH_TOKEN, idString);

            log.info("Set session ID header for session with id {}", idString);

            log.trace("Set session ID header for session with id {}", idString);
        }
    }

    // 创建session
    protected Session createExposedSession(Session session, SessionContext context) {
        if (!WebUtils.isWeb(context)) {
            return super.createExposedSession(session, context);
        } else {
            ServletRequest request = WebUtils.getRequest(context);
            ServletResponse response = WebUtils.getResponse(context);
            SessionKey key = new WebSessionKey(session.getId(), request, response);
            return new DelegatingSession(this, key);
        }
    }

    protected Session createExposedSession(Session session, SessionKey key) {
        if (!WebUtils.isWeb(key)) {
            return super.createExposedSession(session, key);
        } else {
            ServletRequest request = WebUtils.getRequest(key);
            ServletResponse response = WebUtils.getResponse(key);
            SessionKey sessionKey = new WebSessionKey(session.getId(), request, response);
            return new DelegatingSession(this, sessionKey);
        }
    }

    protected void onStart(Session session, SessionContext context) {
        super.onStart(session, context);
        if (!WebUtils.isHttp(context)) {
            log.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response pair. No session ID cookie will be set.");
        } else {
            HttpServletRequest request = WebUtils.getHttpRequest(context);
            HttpServletResponse response = WebUtils.getHttpResponse(context);
            Serializable sessionId = session.getId();
            this.storeSessionId(sessionId, request, response);
            request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
        }
    }

    //获取sessionid
    public Serializable getSessionId(SessionKey key) {
        Serializable id = super.getSessionId(key);
        if (id == null && WebUtils.isWeb(key)) {
            ServletRequest request = WebUtils.getRequest(key);
            ServletResponse response = WebUtils.getResponse(key);
            id = this.getSessionId(request, response);
        }
        return id;
    }

    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        return this.getReferencedSessionId(request, response);
    }

    protected void onExpiration(Session s, ExpiredSessionException ese, SessionKey key) {
        super.onExpiration(s, ese, key);
        this.onInvalidation(key);
    }

    protected void onInvalidation(Session session, InvalidSessionException ise, SessionKey key) {
        super.onInvalidation(session, ise, key);
        this.onInvalidation(key);
    }

    private void onInvalidation(SessionKey key) {
        ServletRequest request = WebUtils.getRequest(key);
        if (request != null) {
            request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID);
        }

        if (WebUtils.isHttp(key)) {
            log.debug("Referenced session was invalid.  Removing session ID header.");
            this.removeSessionIdHeader(WebUtils.getHttpRequest(key), WebUtils.getHttpResponse(key));
        } else {
            log.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response pair. Session ID cookie will not be removed due to invalidated session.");
        }

    }

    protected void onStop(Session session, SessionKey key) {
        super.onStop(session, key);
        if (WebUtils.isHttp(key)) {
            HttpServletRequest request = WebUtils.getHttpRequest(key);
            HttpServletResponse response = WebUtils.getHttpResponse(key);
            log.debug("Session has been stopped (subject logout or explicit stop).  Removing session ID cookie.");
            this.removeSessionIdHeader(request, response);
        } else {
            log.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response pair. Session ID cookie will not be removed due to stopped session.");
        }
    }

    public boolean isServletContainerSessions() {
        return false;
    }
重写CookieRememberMeManger

默认情况下,Shiro会把rememberMe信息放入set-cookie中,保存在浏览器上。这里,重写Cookie方式,把rememberMe信息放入response header中。
创建 HeaderRememberMeManager 类并extends AbstractRememberMeManager ,代码如下

//省略 import 信息
public class HeaderRememberMeManager extends AbstractRememberMeManager {

    private static final transient Logger log = LoggerFactory.getLogger(HeaderRememberMeManager.class);

    // header 中 固定使用的 key
    public static final String DEFAULT_REMEMBER_ME_HEADER_NAME = "remember-me";


    protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
        if (!WebUtils.isHttp(subject)) {
            if (log.isDebugEnabled()) {
                String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet request and response in order to set the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
                log.debug(msg);
            }

        } else {
            HttpServletResponse response = WebUtils.getHttpResponse(subject);
            String base64 = Base64.encodeToString(serialized);
            // 设置 rememberMe 信息到 response header 中
            response.setHeader(DEFAULT_REMEMBER_ME_HEADER_NAME, base64);
        }
    }

    private boolean isIdentityRemoved(WebSubjectContext subjectContext) {
        ServletRequest request = subjectContext.resolveServletRequest();
        if (request == null) {
            return false;
        } else {
            Boolean removed = (Boolean) request.getAttribute(ShiroHttpServletRequest.IDENTITY_REMOVED_KEY);
            return removed != null && removed;
        }
    }

    protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
        if (!WebUtils.isHttp(subjectContext)) {
            if (log.isDebugEnabled()) {
                String msg = "SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a servlet request and response in order to retrieve the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
                log.debug(msg);
            }

            return null;
        } else {
            WebSubjectContext wsc = (WebSubjectContext) subjectContext;
            if (this.isIdentityRemoved(wsc)) {
                return null;
            } else {
                HttpServletRequest request = WebUtils.getHttpRequest(wsc);
                // 在request header 中获取 rememberMe信息
                String base64 = request.getHeader(DEFAULT_REMEMBER_ME_HEADER_NAME);
                if ("deleteMe".equals(base64)) {
                    return null;
                } else if (base64 != null) {
                    base64 = this.ensurePadding(base64);
                    if (log.isTraceEnabled()) {
                        log.trace("Acquired Base64 encoded identity [" + base64 + "]");
                    }

                    byte[] decoded = Base64.decode(base64);
                    if (log.isTraceEnabled()) {
                        log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
                    }

                    return decoded;
                } else {
                    return null;
                }
            }
        }
    }

    private String ensurePadding(String base64) {
        int length = base64.length();
        if (length % 4 != 0) {
            StringBuilder sb = new StringBuilder(base64);

            for (int i = 0; i < length % 4; ++i) {
                sb.append('=');
            }

            base64 = sb.toString();
        }

        return base64;
    }

    protected void forgetIdentity(Subject subject) {
        if (WebUtils.isHttp(subject)) {
            HttpServletRequest request = WebUtils.getHttpRequest(subject);
            HttpServletResponse response = WebUtils.getHttpResponse(subject);
            this.forgetIdentity(request, response);
        }

    }

    public void forgetIdentity(SubjectContext subjectContext) {
        if (WebUtils.isHttp(subjectContext)) {
            HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
            HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
            this.forgetIdentity(request, response);
        }
    }

    private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
    //设置删除标示
        response.setHeader(DEFAULT_REMEMBER_ME_HEADER_NAME, "deleteMe");
    }

}
重写ShiroFilterFactoryBean

默认Fileter链中,user名称的过滤器在为登陆状态下会返回到登录界面,这里修改一下,为登陆状态直接放回Json字符串,不用跳转至登录页面。
如果使用了authc过滤 需要对重写 FormAuthenticationFilter ,为了适配App客户端,这里不推荐使用authc,可以在必须重新验证用户登陆信息时(使用rememberMe信息登陆无效)预先请求一下服务端或者通过记录的x-auth-token有效期进行判断。
新建 MyUserFilter 类 extends UserFilter

public class MyUserFilter extends org.apache.shiro.web.filter.authc.UserFilter {

    // isAccessAllowed return false 执行
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        // 这里也可以不用保存 保存当前request 可在登陆后重新请求当前 request
        this.saveRequest(request);
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.getWriter().write("{\"code\":-1,\"message\":\"no.login\"}");
        return false;
    }

}

新建 MyDefaultFilter enum

// 代码 为修改部分 只需要更改 user 执行为新建的 Filter
// 具体代码 可查看 org.apache.shiro.web.filter.mgt.DefaultFilter
public enum MyDefaultFilter {

    user(MyUserFilter.class);

    private final Class<? extends Filter> filterClass;

    private MyDefaultFilter(Class<? extends Filter> filterClass) {
        this.filterClass = filterClass;
    }
}

新建 MyDefaultFilterChainManager 类 extends DefaultFilterChainManager

public class MyDefaultFilterChainManager extends DefaultFilterChainManager {

    protected void addDefaultFilters(boolean init) {

        //使用我们创建的 DefaultFilter
        MyDefaultFilter[] var2 = MyDefaultFilter.values();
        int var3 = var2.length;

        for (int var4 = 0; var4 < var3; ++var4) {
            MyDefaultFilter defaultFilter = var2[var4];
            super.addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
        }

    }

}

新建 MyShiroFilterFactoryBean 类 extends ShiroFilterFactoryBean

public class MyShiroFilterFactoryBean extends ShiroFilterFactoryBean {

    protected FilterChainManager createFilterChainManager() {
        // 只要修改这里 使用我们创建的 DefaultFilterChainManager
        MyDefaultFilterChainManager manager = new MyDefaultFilterChainManager();

        //省略代码  请在 ShiroFilterFactoryBean 中 copy
    }
    // 缺省的private方法 需在 ShiroFilterFactoryBean 中 copy
}
修改配置信息,指向重写类

这里只展示和上述有关的配置信息,采用的注解@Bean 方式

@Bean(name = "rememberMeManager")
    public HeaderRememberMeManager rememberMeManager() {
        HeaderRememberMeManager headerRememberMeManager = new HeaderRememberMeManager();
        // base64Encoded 自行生成一个 用于rememberMe加密
        headerRememberMeManager.setCipherKey(base64Encoded);
        return headerRememberMeManager;
    }

    @Bean
    public DefaultHeaderSessionManager defaultWebSessionManager(SessionDAO sessionDAO) {
        DefaultHeaderSessionManager defaultHeaderSessionManager = new DefaultHeaderSessionManager();
        // 设立不使用 调取器验证 session 是否过期 作者使用了 redis ,这里根据SessionDAO实际情况设置
        defaultHeaderSessionManager.setSessionValidationSchedulerEnabled(false);
        defaultHeaderSessionManager.setSessionDAO(sessionDAO);
        return defaultHeaderSessionManager;
    }

    @Bean(name = "shiroFilter")
    public MyShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
        // 使用自行创建的 FactoryBean
        MyShiroFilterFactoryBean shiroFilterFactoryBean = new MyShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setFilterChainDefinitions("/api/v1/login = anon\n" +
                "/ = anon\n" +
                "/api/v1/website/article/** = anon\n" +
                "/api/v1/** = cors,user\n");
        return shiroFilterFactoryBean;
    }
@Bean(name = "securityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager(                                                               DefaultHeaderSessionManager sessionManager,
                                                               RememberMeManager rememberMeManager) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //其他配置这里未列出
        //DefaultHeaderSessionManager 重写的 sessionManager
        defaultWebSecurityManager.setSessionManager(sessionManager);
        // rememberMeManager 重写的 rememberMeManager
        defaultWebSecurityManager.setRememberMeManager(rememberMeManager);
        SecurityUtils.setSecurityManager(defaultWebSecurityManager);
        return defaultWebSecurityManager;
    }

完成以上配置信息就可以在 把sessionid或者rememberMe写入response header中和在 request header中读取,session超时自动销毁时间前端需和服务端保持一致,rememberMe有效时间由前端自行控制。

结束

本文介绍了一些项目中使用shiro的技巧,如果有错误或者有更好的方式,希望能与笔者联系,