写在前面: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 {@Beanpublic WebMvcConfigurer CORSConfigurer() {return new WebMvcConfigurerAdapter() {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("*").allowedMethods("*").allowedHeaders("*")//设置是否允许跨域传cookie.allowCredentials(true)//设置缓存时间,减少重复响应.maxAge(3600);}};}@Beanpublic FilterRegistrationBean corsFilter() {final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();final CorsConfiguration config = new CorsConfiguration();// 允许cookies跨域config.setAllowCredentials(true);// #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Originconfig.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();}@Overrideprotected Serializable getSessionId(ServletRequest request, ServletResponse response) {String id = WebUtils.toHttp(request).getHeader("Authorization");//如果请求头中有 Authorization (前端请求头中设置的名字)则其值为sessionIdif (!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取sessionIdreturn super.getSessionId(request, response);}}}

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

/*** @Description: 自定义的 shiro session 缓存管理器* 用于跨域等情况下获取请求头中的sessionId* @method: sessionManager** @author: MengyuWu* @date: 18:38 2019-8-26* @throws**/@Beanpublic 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 hoursessionManager.setGlobalSessionTimeout(3600000 * 12);// 是否删除无效的,默认也是开启sessionManager.setDeleteInvalidSessions(true);// 是否开启 检测,默认开启sessionManager.setSessionValidationSchedulerEnabled(true);// 创建会话CookieCookie 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 hoursessionManager.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();}@Overridepublic boolean isAccessAllowed(ServletRequest request, ServletResponse response,Object mappedValue){if (((HttpServletRequest)request).getMethod().toUpperCase().equals("OPTIONS")){return true;}return super.isAccessAllowed(request, response, mappedValue);}@Overrideprotected 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*/@Overridepublic 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*/@Overrideprotected 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();// 必须设置 SecurityManagershiroFilterFactoryBean.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整合适用前后端分离项目中的记录,如果有错误的地方,还请及时指出。

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

  1. 多学一招总没错吧?SpringBoot解决前后端分离的跨域问题

  2. Springboot整合Shiro前后端分离跨域问题

    Springboot整合Shiro前后端分离跨域问题 前言:SpringBoot整合shiro进行前后端分离开发时(前端是Vue),项目做了跨域配置,但还是前端请求会出现cros err–显示的跨域问 ...

  3. shiro+php,一套基于SpringBoot+Vue+Shiro 前后端分离 开发的代码生成器

    一.前言 最近花了一个月时间完成了一套基于Spring Boot+Vue+Shiro前后端分离的代码生成器,目前项目代码已基本完成 止步传统CRUD,进阶代码优化: 该项目可根据数据库字段动态生成 c ...

  4. springboot集成shiro 前后端分离 统一处理shiro异常

    springboot集成shiro 前后端分离 统一处理shiro异常 参考文章: (1)springboot集成shiro 前后端分离 统一处理shiro异常 (2)https://www.cnbl ...

  5. SpringBoot+vue前后端分离博客项目

    SpringBoot+vue前后端分离博客项目 Java后端接口开发 1.前言 2.新建Springboot项目 3.整合mybatis plus 第一步:导入jar包 第二步:然后去写配置文件: 第 ...

  6. SpringBoot + Vue前后端分离开发:全局异常处理及统一结果封装

    SpringBoot + Vue前后端分离开发:全局异常处理及统一结果封装 文章目录 SpringBoot + Vue前后端分离开发:全局异常处理及统一结果封装 前后端分离开发中的异常处理 统一结果封 ...

  7. 适合新手拿来练习的springboot+vue前后端分离小Demo

    前言: 作者:神的孩子在歌唱 大家好,我叫智 练习springboot+vue前后端分离的Demo 一. 设计数据库 二 . springboot项目创建 2.1 基本配置 2.2 创建dao层 三. ...

  8. SpringBoot+Vue前后端分离,使用SpringSecurity完美处理权限问题(二)

    关注公众号[江南一点雨],专注于 Spring Boot+微服务以及前后端分离等全栈技术,定期视频教程分享,关注后回复 Java ,领取松哥为你精心准备的 Java 干货! 当前后端分离时,权限问题的 ...

  9. SpringBoot+Vue前后端分离

    文章目录 前言 一.vue项目创建 二.SpringBoot项目创建 使用IDEA创建一个简单的SpringBoot项目,这里随便百度,参考很多 三.前后端整合 1.vue配置跨域跳转 2.sprin ...

最新文章

  1. 戴着口罩如何进行人脸识别?快进来看看吧!
  2. Snap和Flatpak 通吃所有发行版的打包方式。
  3. flowJS源码个人分析
  4. lvs的调度算法有几种_LVS:三种负载均衡方式比较
  5. JQuery实现树的功能doc
  6. 详解网络摄像机中的IR-CUT
  7. [转]Python 获取Windows管理员权限
  8. centos7下docker启动失败解决
  9. 【ElasticSearch】如何使用 ElasticSearch 搜索单词的一部分 模糊搜索 正则匹配 前缀匹配
  10. oracle+技术面试,Oracle技术面试问题
  11. Python 基础——range() 与 np.arange()
  12. poj 3630 Phone List(字典树)
  13. oneproxy出现2103错误代码解决方案
  14. Protel 介绍 protel99se正式汉化版下载 Protel DXP2004简体中文版
  15. 股票软件c++源代码
  16. Google Earth Engine(GEE)计算雷达植被指数RVI
  17. LimeSDR官方系列教程(二):LMS7002M RX和I/Q
  18. C#里的进制与ASCII转换
  19. 网络编程 socket函数参数介绍
  20. ubuntu16.04安装libiconv

热门文章

  1. python中的pickle是什么意思_python中pickle模块浅析
  2. DA接口测试工装研究
  3. python_faker使用
  4. 全栈开发之前端开发-欧阳桫-专题视频课程
  5. SpringBoot(一):什么是SpringBoot?
  6. 网络安全专业应届生必备的几个实用工具,快收藏
  7. 【读书笔记】汇编语言程序设计
  8. STC89C52RC - 2 - 开发环境搭建
  9. 居家学习:新冠肺炎疫情下中国高校基于直播的远程教育体验的混合方法分析
  10. 千兆以太网网络变压器进行 poe 供电的原理是什么?