Shiro是如何拦截未登录请求的(二)

前言

在上一篇文章{% post_link Shiro是如何拦截未登录请求的(一) %}中提到了,我们在实际的项目中采用了基于token的方式来实现用户的身份鉴权,但是由于开发的时候对shiro的内部机制不太了解导致那一块的代码实现不够完善、整洁并且还对业务造成了影响,经过了对shiro源码的跟踪分析之后,我们已经知道shiro是如何拦截未登录请求的了,那么接下来我们开始来针对问题制定相应的解决方案.

解决方案

第一种方案
由于最初在app端是使用传输cookie的方式来实现身份鉴权的,跨域问题也已经解决了,为了尽量不改动已经写好的代码,我们可以想办法来让h5应用也能在跨域的情况下传输cookie,首先服务端在使用cors协议时需要设置响应消息头Access-Control-Allow-Credentials的值为true即允许在ajax访问时携带cookie,客户端方面也需通过js设置withCredentials为true才能真正实现跨域传输cookie.另外为了安全,在cors标准里不允许Access-Control-Allow-Origin设置为*,而是必须指定明确的、与请求网页一致的域名.cookie也依然遵循“同源策略”,只有用目标服务器域名设置的cookie才会上传,而且使用document.cookie也无法读取目标服务器域名下的cookie.接下来我们来看看代码是怎么实现的:
1.我们原先在springboot中关于支持跨域有多种实现方式,我们采用最后的一种:

    @Bean
    public FilterRegistrationBean corsFilter() {
        return new FilterRegistrationBean(new Filter() {
            public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
                    throws IOException, ServletException {
                HttpServletRequest request = (HttpServletRequest) req;
                HttpServletResponse response = (HttpServletResponse) res;
                String method = request.getMethod();
                String origin = request.getHeader("Origin");
                if(origin == null) {
                    origin = request.getHeader("Referer");
                }
                // this origin value could just as easily have come from a database
                response.setHeader("Access-Control-Allow-Origin", origin);            // 允许指定域访问跨域资源
                //response.setHeader("Access-Control-Allow-Origin", "*");
                response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, OPTIONS");
                response.setHeader("Access-Control-Max-Age", "3600");
                response.setHeader("Access-Control-Allow-Credentials", "true");
                response.setHeader("Access-Control-Allow-Headers", "Accept, Origin, X-Requested-With, Content-Type,Last-Modified,device,token");
                if ("OPTIONS".equals(method)) {
                    response.setStatus(HttpStatus.OK.value());
                } else {
                    chain.doFilter(req, res);
                }
            }

            public void init(FilterConfig filterConfig) {
            }

            public void destroy() {
            }
        });
    }

2.客户端也不再需要在请求头中带上token了,只要登录之后不管调什么接口都会自动带上cookie到后端校验的,代码如下:

    $.ajax({
        url:'http://localhost:8080/win/api/test/cors',
        type:'post',
         beforeSend:(xhr)=> {
           //xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
           //xhr.setRequestHeader("token", "web_session_key-5ce2ae9c-8f79-4f83-9b47-1510da4b2fb0");
           xhr.setRequestHeader("device","APP");
        },
        xhrFields:{
            withCredentials:true,
            useDefaultXhrHeader:false
        },
        corssDomain:true,
        success:function(data){
        console.log(data);
        }
    });

这样接口就可以正常返回数据了,控制台也不再报错(注意request header中的cookie).

image.png

第二种方案
从上一篇文章中我们知道shiro是在其默认的会话管理器DefaultWebSessionManager中获取请求携带过来的cookie的,我们可以通过继承这个类来扩展其中相关的代码来实现我们的需求,之前在项目中我们已经扩展过这个类了,当时是为了重写其中定时验证session有效性的部分以便在session失效时做一些数据清理工作,下面贴出的是shiro从cookie中获取sessionid的主要源代码:

    @Override
    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 = getSessionId(request, response);
        }
        return id;
    }

    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        return getReferencedSessionId(request, response);
    }
    private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
        String id = getSessionIdCookieValue(request, response);
        .......
        return id;
    }
    private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
        .......
        //getSessionIdCookie().readValue()操作的是cookie对象.
        return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
    }

那么我们只要在扩展类中覆写这些方法,通过在请求头传输过来的device标识便可以区分出不同的调用端来源,即pc端后台依然采用shiro原有的认证方式,而app端或者h5应用则可以使用基于token的身份认证方式,达到两者共存的目的.下面来看看我们自定义的CustomerWebSessionManager类,其继承了shiro的DefaultWebSessionManager类.

public class CustomerWebSessionManager extends DefaultWebSessionManager {

    private static final Logger logger = LoggerFactory.getLogger(CustomerWebSessionManager.class);

    private static final String AUTH_TOKEN = "token";

    public CustomerWebSessionManager() {
        super();
    }
    @Override
    public void validateSessions() {
        if (logger.isInfoEnabled()) {
            logger.info("Validating all active sessions...");
        }
        ......
    }

其中定义的类静态变量AUTH_TOKEN为请求头中需要携带的会话id的名称,validateSessions方法是我们重写的用来实现当session失效时做数据清理的.由于DefaultWebSessionManager中的大部分方法为私有的方法,无法为其子类所继承,所以只好重写其中所有用到的protected方法,代码如下:

    /**
     * 重写父类获取sessionID的方法,若请求为APP或者H5则从请求头中取出token,若为PC端后台则从cookie中获取
     *
     * @param request
     * @param response
     * @return
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        if (!(request instanceof HttpServletRequest)) {
            logger.debug("Current request is not an HttpServletRequest - cannot get session ID.  Returning null.");
            return null;
        }
        HttpServletRequest httpRequest = WebUtils.toHttp(request);
        if (StringHelpUtils.isNotBlank(httpRequest.getHeader("device"))
                && (httpRequest.getHeader("device").equals("APP") || httpRequest
                .getHeader("device").equals("H5"))) {
            //从header中获取token
            String token = httpRequest.getHeader(AUTH_TOKEN);
            // 每次读取之后都把当前的token放入response中
            HttpServletResponse httpResponse = WebUtils.toHttp(response);
            if (StringHelpUtils.isNotEmpty(token)) {
                httpResponse.setHeader(AUTH_TOKEN, token);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            }
            //sessionIdUrlRewritingEnabled的配置为false,不会在url的后面带上sessionID
            request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
            return token;
        }
        return getReferencedSessionId(request, response);
    }
    /**
     * shiro默认从cookie中获取sessionId
     *
     * @param request
     * @param response
     * @return
     */
    private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
        String id = getSessionIdCookieValue(request, response);
        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                    ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
        } else {
            //not in a cookie, or cookie is disabled - try the request URI as a fallback (i.e. due to URL rewriting):
            //try the URI path segment parameters first:
            id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
            if (id == null) {
                //not a URI path segment parameter, try the query parameters:
                String name = getSessionIdName();
                id = request.getParameter(name);
                if (id == null) {
                    //try lowercase:
                    id = request.getParameter(name.toLowerCase());
                }
            }
            if (id != null) {
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                        ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
            }
        }
        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            //automatically mark it valid here.  If it is invalid, the
            //onUnknownSession method below will be invoked and we'll remove the attribute at that time.
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
        }
        // always set rewrite flag - SHIRO-361
        request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
        return id;
    }

    //copy from DefaultWebSessionManager
    private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
        if (!isSessionIdCookieEnabled()) {
            logger.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie.");
            return null;
        }
        if (!(request instanceof HttpServletRequest)) {
            logger.debug("Current request is not an HttpServletRequest - cannot get session ID cookie.  Returning null.");
            return null;
        }
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
    }

    //since 1.2.2 copy from DefaultWebSessionManager
    private String getUriPathSegmentParamValue(ServletRequest servletRequest, String paramName) {
        if (!(servletRequest instanceof HttpServletRequest)) {
            return null;
        }
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String uri = request.getRequestURI();
        if (uri == null) {
            return null;
        }
        int queryStartIndex = uri.indexOf('?');
        if (queryStartIndex >= 0) { //get rid of the query string
            uri = uri.substring(0, queryStartIndex);
        }
        int index = uri.indexOf(';'); //now check for path segment parameters:
        if (index < 0) {
            //no path segment params - return:
            return null;
        }
        //there are path segment params, let's get the last one that may exist:
        final String TOKEN = paramName + "=";
        uri = uri.substring(index + 1); //uri now contains only the path segment params
        //we only care about the last JSESSIONID param:
        index = uri.lastIndexOf(TOKEN);
        if (index < 0) {
            //no segment param:
            return null;
        }
        uri = uri.substring(index + TOKEN.length());
        index = uri.indexOf(';'); //strip off any remaining segment params:
        if (index >= 0) {
            uri = uri.substring(0, index);
        }
        return uri; //what remains is the value
    }

    //since 1.2.1 copy from DefaultWebSessionManager
    private String getSessionIdName() {
        String name = this.getSessionIdCookie() != null ? this.getSessionIdCookie().getName() : null;
        if (name == null) {
            name = ShiroHttpSession.DEFAULT_SESSION_ID_NAME;
        }
        return name;
    }

当shiro取不到sessionid时,会调用DelegatingSubject类中的getSession(true)方法创建一个新的session.

    public Session getSession(boolean create) {
        if (log.isTraceEnabled()) {
            log.trace("attempting to get session; create = " + create +
                    "; session is null = " + (this.session == null) +
                    "; session has id = " + (this.session != null && session.getId() != null));
        }

        if (this.session == null && create) {

            //added in 1.2:
            if (!isSessionCreationEnabled()) {
                String msg = "Session creation has been disabled for the current subject.  This exception indicates " +
                        "that there is either a programming error (using a session when it should never be " +
                        "used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " +
                        "for the current Subject.  See the " + DisabledSessionException.class.getName() + " JavaDoc " +
                        "for more.";
                throw new DisabledSessionException(msg);
            }

            log.trace("Starting session for host {}", getHost());
            SessionContext sessionContext = createSessionContext();
            Session session = this.securityManager.start(sessionContext);
            this.session = decorate(session);
        }
        return this.session;
    }

上面的第22行this.securityManager.start最终调用的是DefaultWebSessionManager中的onStart方法,所以我们要重写这个方法,将产生的sessionid放到response header中.另外当session失效或销毁时的相关方法也需重新实现,具体代码如下:

    //存储会话id到response header中
    private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) {
        if (currentId == null) {
            String msg = "sessionId cannot be null when persisting for subsequent requests.";
            throw new IllegalArgumentException(msg);
        }
        String idString = currentId.toString();
        if (StringHelpUtils.isNotBlank(request.getHeader("device"))
                && (request.getHeader("device").equals("APP") || request
                .getHeader("device").equals("H5"))) {
            response.setHeader(AUTH_TOKEN, idString);
        } else {
            Cookie template = getSessionIdCookie();
            Cookie cookie = new SimpleCookie(template);
            cookie.setValue(idString);
            cookie.saveTo(request, response);
        }
        logger.trace("Set session ID cookie for session with id {}", idString);
    }

    //设置deleteMe到response header中
    private void removeSessionIdCookie(HttpServletRequest request, HttpServletResponse response) {
        if (StringHelpUtils.isNotBlank(request.getHeader("device"))
                && (request.getHeader("device").equals("APP") || request
                .getHeader("device").equals("H5"))) {
            response.setHeader(AUTH_TOKEN, Cookie.DELETED_COOKIE_VALUE);
        } else {
            getSessionIdCookie().removeFrom(request, response);
        }
    }

    /**
     * 会话创建
     * Stores the Session's ID, usually as a Cookie, to associate with future requests.
     *
     * @param session the session that was just {@link #createSession created}.
     */
    @Override
    protected void onStart(Session session, SessionContext context) {
        super.onStart(session, context);
        if (!WebUtils.isHttp(context)) {
            logger.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response " +
                    "pair. No session ID cookie will be set.");
            return;
        }
        HttpServletRequest request = WebUtils.getHttpRequest(context);
        HttpServletResponse response = WebUtils.getHttpResponse(context);
        if (isSessionIdCookieEnabled()) {
            Serializable sessionId = session.getId();
            storeSessionId(sessionId, request, response);
        } else {
            logger.debug("Session ID cookie is disabled.  No cookie has been set for new session with id {}", session.getId());
        }
        request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
    }
    //会话失效
    @Override
    protected void onExpiration(Session s, ExpiredSessionException ese, SessionKey key) {
        super.onExpiration(s, ese, key);
        onInvalidation(key);
    }

    @Override
    protected void onInvalidation(Session session, InvalidSessionException ise, SessionKey key) {
        super.onInvalidation(session, ise, key);
        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)) {
            logger.debug("Referenced session was invalid.  Removing session ID cookie.");
            removeSessionIdCookie(WebUtils.getHttpRequest(key), WebUtils.getHttpResponse(key));
        } else {
            logger.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.");
        }
    }
    //会话销毁
    @Override
    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);
            logger.debug("Session has been stopped (subject logout or explicit stop).  Removing session ID cookie.");
            removeSessionIdCookie(request, response);
        } else {
            logger.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.");
        }
    }

最后再在springboot中做如下配置:

    @Bean
    public CustomerWebSessionManager sessionManager() {
        CustomerWebSessionManager sessionManager = new CustomerWebSessionManager();
        //会话验证器调度时间
        sessionManager.setSessionValidationInterval(1800000);
        //定时检查失效的session
        sessionManager.setSessionValidationSchedulerEnabled(true);
        //是否在会话过期后会调用SessionDAO的delete方法删除会话 默认true
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionDAO(redisSessionDAO());
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        sessionManager.setSessionIdCookie(wapsession());
        sessionManager.setSessionIdCookieEnabled(true);
        return sessionManager;
    }

我们来看看在postman中的接口访问测试结果:


image.png

而在h5应用中已无需再支持跨域传输cookie了,但需重新在请求头中传输token,js代码稍微做如下修改:

    $.ajax({
        url:'http://localhost:8080/win/api/test/cors',
        type:'post',
         beforeSend:(xhr)=> {
           //xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
           xhr.setRequestHeader("token", "web_session_key-26653f18-1d81-4bd3-a039-301870788abb");
           xhr.setRequestHeader("device","APP");
        },
        xhrFields:{
            //withCredentials:true,
            //useDefaultXhrHeader:false
        },
        //corssDomain:true,
        success:function(data){
        console.log(data);
        }
    });

通过浏览器可以看到已经成功访问接口了,控制台也没有报错,结果如下图(注意其中request header和response header中的token)


image.png

好了,到此我们已经完成了对shiro支持token身份认证的全部改造了.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容

  • 问题描述 之前在公司搭项目平台的时候权限框架采用的是shiro,由于系统主要面向的是APP端的用户,PC端仅仅是公...
    Briseis阅读 33,929评论 4 28
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,511评论 18 139
  • 多久没画了,说好的坚持呢!这就是我从来都停步不前的原因,没有持之以恒的耐心
    日落荷栖阅读 302评论 3 2
  • 终于下班了,端午节的前一天,我依然奋斗前线中,有句话说的好,只有闲出来的病,很喜欢这种忙碌的生活,明天端午节了,祝...
    杨美玲阅读 145评论 0 0