springboot+shiro前后端分离过程中跨域问题、sessionId问题、302鉴权失败问题

写在前面:2020年2月29号修改该文章,之前针对302鉴权失败问题的解决方案存在 “WebUtils.toHttp 往返回response写返回值的时候出现回写跨域问题”。现已进行更正。

     

 

近期项目需要前后端分离,由于前后端分离后原来的适用的shiro配置无法满足现有系统要求。同时在前后端项目分离的项目中存在的跨域问题,cookies不再使用,通过token方式实现用户登陆鉴权。

 

下面记录在整个过程中涉及的几个大问题:1、跨域问题 2、sessionId问题  3、302鉴权问题

 

1、springboot跨域问题解决

package net.sinorock.aj.common.config;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
  * @Description
  * @Project aj-parent
  * @PageName  net.sinorock.aj.common.config
  * @ClassName CorsConfig
  * @author MengyuWu
  * @date 2019-8-229:59
   */
@Configuration
public class CorsConfig {

    @Bean
    public WebMvcConfigurer CORSConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("*")
                        .allowedMethods("*")
                        .allowedHeaders("*")
                        //设置是否允许跨域传cookie
                        .allowCredentials(true)
                        //设置缓存时间,减少重复响应
                        .maxAge(3600);
            }
        };
    }


    @Bean
    public FilterRegistrationBean corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        // 允许cookies跨域
        config.setAllowCredentials(true);
        // #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
        config.addAllowedOrigin("*");
        // #允许访问的头信息,*表示全部
        config.addAllowedHeader("*");
        // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
        config.setMaxAge(3600L);
        // 允许提交请求的方法,*表示全部允许
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("HEAD");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");
        source.registerCorsConfiguration("/**", config);

        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        // 设置监听器的优先级
        bean.setOrder(0);

        return bean;
    }
}

2、sessionId问题,因前后端分离无法使用cookie,所以修改为登陆校验后返回sessionId给前端,前端拿到sessionId后放在请求头中,后端重写 DefaultWebSessionManager 中的 getSessionId 方法,从请求头中获取对应的sessionId。

2.1 登陆返回sessionId ( token ) 

  

    @ResponseBody
    @PostMapping(value = "/web/login")
    @ApiOperation("登录")
    
    public R login(@RequestBody SysUserEntity user)
        throws AuthenticationException
    {
        SecurityUtils.getSubject().logout();// 此行代码用于修改会话标识未更新的BUG,作用清空登录之前产生的session信息
        MenuData menuData = new MenuData();
        try
        {
            Subject subject = ShiroUtils.getSubject();
            MyUserAuthenticationToken token = new MyUserAuthenticationToken(user.getUsername(),
                user.getPassword());
            subject.login(token);
            if (null != subject.getSession())
            {
                String sessionId = (String)subject.getSession().getId();
                menuData.setToken(sessionId);
            }
        }
        catch (LockedAccountException e)
        {
            return R.error(e.getMessage());
        }
        catch (DisabledAccountException e)
        {
            return R.error(e.getMessage());
        }
        catch (UnknownAccountException e)
        {
            return R.error(e.getMessage());
        }
        catch (IncorrectCredentialsException e)
        {
            SysUserEntity originUser = userService.queryByUsername(user.getUsername());
            userService.updateUserLoginAttempts(originUser,
                configService.getValue("sysLoginErrorNum"));
            return R.error("账号/密码不正确");
        }
        catch (Exception e)
        {
            return R.error("账户验证失败");
        }
     
        return R.ok().put("data", menuData);
    }

2.2 后台重写 DefaultWebSessionManager 中的 getSessionId方法

package net.sinorock.aj.modules.base.core.security.shiro;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
  * @Description
  * @Project aj-parent
  * @PageName  net.sinorock.aj.modules.base.core.security.shiro
  * @ClassName MySessionManager
  * @author MengyuWu
  * @date 2019-8-2616:08
   */
@Configuration
@Slf4j
public class MySessionManager extends DefaultWebSessionManager {

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public MySessionManager() {

        super();
    }


    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String id = WebUtils.toHttp(request).getHeader("Authorization");
        //如果请求头中有 Authorization (前端请求头中设置的名字)则其值为sessionId
        if (!StringUtils.isEmpty(id)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            //否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }


}

2.3 在shiro的配置类中注入自定义的session缓存管理器

/**
     * @Description: 自定义的 shiro session 缓存管理器
     * 用于跨域等情况下获取请求头中的sessionId
     * @method: sessionManager
     *
     * @author: MengyuWu
     * @date: 18:38 2019-8-26
     * @throws
     **/

    @Bean
    public SessionManager sessionManager()
    {
        // 将我们继承后重写的shiro session 注册
        MySessionManager sessionManager = new MySessionManager();

        sessionManager.setSessionDAO(redisSessionDAO());
        Collection<SessionListener> sessionListeners = new ArrayList<>();
        sessionListeners.add(customSessionListener());
        sessionManager.setSessionListeners(sessionListeners);
        // 单位为毫秒,600000毫秒为1个小时
        sessionManager.setSessionValidationInterval(3600000 * 12);
        // 3600000 milliseconds = 1 hour
        sessionManager.setGlobalSessionTimeout(3600000 * 12);
        // 是否删除无效的,默认也是开启
        sessionManager.setDeleteInvalidSessions(true);
        // 是否开启 检测,默认开启
        sessionManager.setSessionValidationSchedulerEnabled(true);
        // 创建会话Cookie
        Cookie cookie = new SimpleCookie(ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
        cookie.setName("WEBID");
        cookie.setHttpOnly(true);
        sessionManager.setSessionIdCookie(cookie);

        // 单位为毫秒,600000毫秒为1个小时
        sessionManager.setSessionValidationInterval(3600000 * 12);
        // 3600000 milliseconds = 1 hour
        sessionManager.setGlobalSessionTimeout(3600000 * 12);
        // 是否删除无效的,默认也是开启
        sessionManager.setDeleteInvalidSessions(true);
        return sessionManager;
    }

至此可解决前后端跨域后的sessionId的问题。前端调用后台接口后,发现没有鉴权的请求,直接302错误,并没有对应的返回值,所以下面针对302问题进行解决。

3、302问题解决(此问题解决参考该博文:https://blog.csdn.net/China_hdy/article/details/97154272 ,感谢博主的详细讲解)

3.1 自定义 FormAuthenticationFilter

package net.sinorock.aj.modules.base.core.security.shiro;


import com.alibaba.fastjson.JSONObject;
import net.sinorock.aj.common.constant.ErrorCodes;
import net.sinorock.aj.common.web.def.R;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;


/**
 * @Description:
 * @program: aj-parent
 * @Auther: CShi
 * @Date: 2019-8-11 14:28
 **/
public class UserFormAuthenticationFilter extends FormAuthenticationFilter
{
    public UserFormAuthenticationFilter()
    {
        super();
    }

    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response,
                                   Object mappedValue)
    {
        if (((HttpServletRequest)request).getMethod().toUpperCase().equals("OPTIONS"))
        {
            return true;
        }
        return super.isAccessAllowed(request, response, mappedValue);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
        throws Exception
    {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                return executeLogin(request, response);
            } else {
                return true;
            }
        } else {
            //解决 WebUtils.toHttp 往返回response写数据跨域问题
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            String origin = httpRequest.getHeader("Origin");
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader("Access-Control-Allow-Origin", origin);
            //通过对 Credentials 参数的设置,就可以保持跨域 Ajax 时的 Cookie
            //设置了Allow-Credentials,Allow-Origin就不能为*,需要指明具体的url域
            httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");

            // 返回固定的JSON串
            WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
            WebUtils.toHttp(response).getWriter().print(JSONObject.toJSONString(R.error(ErrorCodes.General.AUTH_EMPTY_ERROR.getCode(),ErrorCodes.General.AUTH_EMPTY_ERROR.getMsg())));
            return false;
        }
    }
}

3.2 自定义PermissionsAuthorizationFilter

package net.sinorock.aj.modules.base.core.security.shiro;

import com.alibaba.fastjson.JSONObject;
import net.sinorock.aj.common.constant.ErrorCodes;
import net.sinorock.aj.common.web.def.R;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

/**
  * @Description
  * @Project aj-parent
  * @PageName  net.sinorock.aj.modules.base.core.security.shiro
  * @ClassName CustomPermissionsAuthorizationFilter
  * @author MengyuWu
  * @date 2019-8-279:47
   */
public class CustomPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {
    /**
     * 根据请求接口路径进行验证
     * @param request
     * @param response
     * @param mappedValue
     * @return
     * @throws IOException
     */
    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
        // 获取接口请求路径
        String servletPath = WebUtils.toHttp(request).getServletPath();
        mappedValue = new String[]{servletPath};
        return super.isAccessAllowed(request, response, mappedValue);
    }

    /**
     * 解决权限不足302问题
     * @param request
     * @param response
     * @return
     * @throws IOException
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        Subject subject = getSubject(request, response);
        if (subject.getPrincipal() != null) {
            return true;
        } else {

            //解决 WebUtils.toHttp 往返回response写数据跨域问题
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            String origin = httpRequest.getHeader("Origin");
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader("Access-Control-Allow-Origin", origin);
            //通过对 Credentials 参数的设置,就可以保持跨域 Ajax 时的 Cookie
            //设置了Allow-Credentials,Allow-Origin就不能为*,需要指明具体的url域
            httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");

            WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
            WebUtils.toHttp(response).getWriter().print(JSONObject.toJSONString(R.error(ErrorCodes.General.AUTH_EMPTY_ERROR.getCode(),ErrorCodes.General.AUTH_EMPTY_ERROR.getMsg())));
        }
        return false;
    }
}

3.3 shiro配置类中添加自定义的PermissionsAuthorizationFilter、FormAuthenticationFilter

/**
     * ShiroFilterFactoryBean 处理拦截资源文件问题。
     * Filter Chain定义说明
     * 1、一个URL可以配置多个Filter,使用逗号分隔
     * 2、当设置多个过滤器时,全部验证通过,才视为通过
     * 3、部分过滤器可指定参数,如perms,roles
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager){
        if(log.isDebugEnabled()){
            log.debug("ShiroConfiguration.shirFilter()");
        }
        ShiroFilterFactoryBean shiroFilterFactoryBean  = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //增加自定义过滤
        Map<String, Filter> filters = new HashMap<>(5);
        filters.put("authc", new UserFormAuthenticationFilter());
        filters.put("perms", new CustomPermissionsAuthorizationFilter());
        filters.put("logout", new MyUserLogoutFilter());
        shiroFilterFactoryBean.setFilters(filters);
        //拦截器.
        Map<String,String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();

        //配置退出过滤器
        /**
         * anon(匿名)  org.apache.shiro.web.filter.authc.AnonymousFilter
         * authc(身份验证)       org.apache.shiro.web.filter.authc.FormAuthenticationFilter
         * authcBasic(http基本验证)    org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
         * logout(退出)        org.apache.shiro.web.filter.authc.LogoutFilter
         * noSessionCreation(不创建session) org.apache.shiro.web.filter.session.NoSessionCreationFilter
         * perms(许可验证)  org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
         * port(端口验证)   org.apache.shiro.web.filter.authz.PortFilter
         * rest  (rest方面)  org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
         * roles(权限验证)  org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
         * ssl (ssl方面)   org.apache.shiro.web.filter.authz.SslFilter
         * member (用户方面)  org.apache.shiro.web.filter.authc.UserFilter
         * user  表示用户不一定已通过认证,只要曾被Shiro记住过登录状态的用户就可以正常发起请求,比如rememberMe
         */

        //<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边
        //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
        filterChainDefinitionMap.put("/main/web/login", "anon");
        filterChainDefinitionMap.put("/main/web/logout", "logout");
        // 使用该过滤器过滤所有的链接
        filterChainDefinitionMap.putAll(ShiroConfigConstant.filterMap);
        //配置记住我或认证通过可以访问的地址
        filterChainDefinitionMap.put("/**", "authc,perms");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

至此,302问题已解决,前端可通过后台接口返回 50011 (ErrorCodes.General.AUTH_EMPTY_ERROR)进行判断是否鉴权成功,不成功的话调转登陆页面。

 

此文为我在改造springboot+shiro整合适用前后端分离项目中的记录,如果有错误的地方,还请及时指出。

  • 10
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值