问题描述

将项目升级为前后端分离的过程中,关于shiro的权限控制遇到了一些问题,这里记录下问题以及解决方法;

使用springboot+shiro前后端分离后出现的跨域问题

在项目进行前后端分离后,前端发送给后端的请求总是报跨域问题
Access to XMLHttpRequest at ‘http://localhost:8081/******’ from origin ‘http://localhost:9090’ has been blocked by CORS policy: Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.

原因是项目前后端分离以后,前端在发送给后端的每个请求中都会在header字段中附上Authorization:*********;来进行权限的验证,这样等于自定义了header字段,导致前端发送的真正请求前都会默认发送OPTIONS请求,来探测服务器。如果返回状态200成功信息,则继续实际的POST/GET正常请求,否则返回跨域问题。
而OPTIONS请求不会带上认证信息,会被项目中的shiro的拦截器拦截下来,导致OPTIONS请求不能返回前端成功,所以导致跨域问题出现。

解决方法

自定义拦截器CorsFilter

@Override
    public void init(FilterConfig config) throws ServletException {
        this.config = config;
    }
    @Override
    public void destroy() {
        this.config = null;
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        response.setHeader( "Access-Control-Allow-Origin", request.getHeader("Origin"));
        // 允许请求的方法
        response.setHeader( "Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PUT" );
        // 多少秒内,不需要再发送预检验请求,可以缓存该结果
        response.setHeader( "Access-Control-Max-Age", "3600" );
        // 表明它允许跨域请求包含xxx头
        response.setHeader( "Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
        //是否允许浏览器携带用户身份信息(cookie)
        response.setHeader( "Access-Control-Allow-Credentials", "true" );
        // response.setHeader( "Access-Control-Expose-Headers", "*" );
        if (request.getMethod().equals( "OPTIONS" )) {
            response.setStatus( 200 );
            return;
        }
        filterChain.doFilter( servletRequest, response );

所有请求都允许跨域,并且只要是OPTIONS请求直接返回成功,而且要将拦截器CorsFilter的优先级设为最高,这样请求会先到拦截器CorsFilter进行处理,不然会被shiro拦截器拦截掉。

项目改为前后端分离后各种登录、鉴权的状态返回

之前的项目前端都是jsp,各种没有访问权限、跳转登录、登出都是返回一个页面然后跳转就行了,现在前后端分离后,就要返回前端约定的状态码,让前端控制页面的跳转,这样就涉及到重写shiro的各种过滤器

解决方法

如果用户未登录,访问了请求连接,就需要返回前端403状态吗,这里需要重写FormAuthenticationFilter里的onAccessDenied方法
NewFormAuthenticationFilter:

protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (this.isLoginRequest(request, response)) {
            if (this.isLoginSubmission(request, response)) {
                if (log.isTraceEnabled()) {
                    log.trace("Login submission detected.  Attempting to execute login.");
                }

                return this.executeLogin(request, response);
            } else {
                if (log.isTraceEnabled()) {
                    log.trace("Login page view.");
                }
			WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
            WebUtils.toHttp(response).getWriter().print(new Response(403,"请登录"));
                return true;
            }
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Attempting to access a path which requires authentication.  Forwarding to the Authentication url [" + this.getLoginUrl() + "]");
            }

            WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
            WebUtils.toHttp(response).getWriter().print(new Response(403,"请登录"));

            return false;
        }
    }

如果用户访问未授权资源,就要返回状态吗401,这是重写PermissionsAuthorizationFilter里的onAccessDenied方法
NewPermissionsAuthorizationFilter:

protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
        WebUtils.toHttp(response).getWriter().print(new Response(401,"你没有权限访问该资源"));
        return false;
    }

如果用户登出,就要返回登出成功信息,重写LogoutFilter里的preHandle方法
NewLogoutFilter:

protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = this.getSubject(request, response);
        String redirectUrl = this.getRedirectUrl(request, response, subject);

        try {
            subject.logout();
            WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
            WebUtils.toHttp(response).getWriter().print(new Response(000000,"成功登出")); 
        } catch (SessionException var6) {
            log.debug("Encountered session exception during logout.  This can generally safely be ignored.", var6);
        }
        return false;
    }

重写这三个过滤器时出现的一些问题,当时写好后过滤器注册进项目中后,发现所有连接都被拦截了,就算是标明annon的资源也不例外;最后发现的将这三个拦截器设为单例模式放入的springboot容器中了,这样不仅shiro中有着三个拦截器,spring中也有这些拦截器,而发过来的请求被spring中的拦截器拦截掉了。
这里将拦截器的注册改为如下形式:

Map<String, Filter> filters = factoryBean.getFilters();
        filters.put("authc",new NewFormAuthenticationFilter());
        filters.put("logout",new NewLogoutFilter());
        filters.put("perms",new NewPermissionsAuthorizationFilter());
        factoryBean.setFilters(filters);

这样就只注册进shiro里。