记一次springboot+vue+shiro的前后端分离项目中碰到的坑。
前言
最近有了一个项目练手,一个简单的后台管理项目,本人学艺不精,和同学在前后端联调的时候碰上了跨域的问题,弄了一上午加半个下午才弄好,在此记录一下,希望可以帮到有需要的人。本篇文章因为作者本人技术的原因,大概率存在各种错误,仅供参考。
过程
- 在一个前后端分离的项目中,跨域问题是首要解决的,一开始尝试了在Controller上加上@CrossOrigin注解来解决,随后发现不起什么作用,于是去谷歌查了一下,说是可能和shiro有关,搜索到的部分资料说自定义过滤器然后重写onAccessDenied方法可以解决,试了一下发现不怎么行,当然不行的原因不在这里。
随后查询了前后端分离时,前端需要做的工作,一个普遍的答案是加上
axios.defaults.withCredentials = true;
但还是没有解决跨域的问题,于是按照网上的方法重写了过滤器,加了一些调试代码,如下:(记得注册这个自定义的过滤器,相关代码网上有)
public class MyFormAuthenticationFilter extends FormAuthenticationFilter { private static final Logger logger = LoggerFactory.getLogger(MyFormAuthenticationFilter.class); public MyFormAuthenticationFilter() { super(); } @Override public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { HttpServletRequest req = (HttpServletRequest)request; String authorization = ((HttpServletRequest) request).getHeader("Authorization"); System.out.println("==================================="); System.out.println("authorization: " + authorization); Subject subject = SecurityUtils.getSubject(); Serializable id = subject.getSession().getId(); System.out.println("这是subject: "+subject.toString()); System.out.println("这是SessionId: "+id.toString()); System.out.println("这是登录的用户信息:" + subject.getPrincipals()); 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 { HttpServletResponse res = (HttpServletResponse)response; HttpServletRequest req = (HttpServletRequest)request; //设置origin,此值不能为‘*’// String origin = req.getHeader("Origin"); if(origin == null) { origin = req.getHeader("Referer"); } res.setHeader("Access-Control-Allow-Origin", origin); System.out.println(origin); res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); res.setHeader("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With,token"); // 设置跨域// res.setHeader("Access-Control-Allow-Credentials","true"); res.setStatus(HttpServletResponse.SC_OK); res.setCharacterEncoding("UTF-8"); PrintWriter writer = res.getWriter(); Map<String, Object> map= new HashMap<>(); //打印未登录信息// map.put("code", 403); map.put("msg", "未登录"); writer.write(JSON.toJSONString(map); writer.close(); return false; } }
以上代码还不足以解决跨域的问题,还需要加上一段配置,如下:
@Configuration public class CorsConfig { private CorsConfiguration buildConfig() { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.addAllowedOrigin("*"); corsConfiguration.addAllowedHeader("*"); corsConfiguration.addAllowedMethod("*"); corsConfiguration.setAllowCredentials(true); return corsConfiguration; } @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", buildConfig()); return new CorsFilter(source); } }
以上两段代码可能会有不需要的地方或者是没有用的代码,我现在技术太差,还不能看出到底有什么可以改进的地方,如有高见还请评论区指示。
- 至此,跨域的问题已经解决了,但是随之而来的是JSESSIONID丢失的问题,因为http是无状态协议,所以前端提交请求时需要附带一个JSESSIONID给后端,让后端判断用户的身份信息,但是这个JSESSIONID是存储在Cookie里面的,跨域的时候这个Cookie不会跟着一起发送,看起来就像是JSESSIONID丢失了一样。然后服务端就会认为又是一个新的用户来了(因为没有带着JSESSIONID请求),就会创建一个新的Session,表现在前端就是报未登录错误(如果你有的话)
- 有一个想法是后端验证登录成功后返回一个JSESSIONID给前端,然后前端写入到Cookie里面,再带着请求发送到后端,这样就可以解决JSESSIONID丢失的问题了,但是尝试后发现,就算手动在请求头里面设置了Cookie,请求的时候这个Cookie还是不会被发出去,无奈之下放弃了(可能是我知识面太窄,可能还有我不知道的其他方法)
到这里离解决方案已经很近了,我们可以不设置Cookie,在请求头里面设置一个”Authorization",它的值就是后端传给前端的JSESSIONID,然后重写DefaultWebSessionManager类,把JSESSIONID改为接收到的Authorization,这样就绕过了用Cookie来获取JSESSIONID,如下
public class ShiroSessionManager extends DefaultWebSessionManager { private static final String AUTHORIZATION = "Authorization"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; public ShiroSessionManager(){ super(); } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response){ String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION); System.out.println("id:"+id); if(StringUtils.isEmpty(id)){ //如果没有携带id参数则按照父类的方式在cookie进行获取 System.out.println("super:"+super.getSessionId(request, response)); return super.getSessionId(request, response); }else{ //如果请求头中有 authToken 则其值为sessionId 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; } } }
然后注册一下就行了,如下:
@Bean public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("shiroConfigRealm") Realm realm){ DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); defaultWebSecurityManager.setRealm(realm); defaultWebSecurityManager.setSessionManager(sessionManager()); return defaultWebSecurityManager; } @Bean public SessionManager sessionManager(){ ShiroSessionManager shiroSessionManager = new ShiroSessionManager(); //这里可以不设置。Shiro有默认的session管理。如果缓存为Redis则需改用Redis的管理 shiroSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO()); return shiroSessionManager; }
结论
到此,跨域问题和JSESSIONID丢失的问题都解决了,以上解决方案可能不能解决你的问题,但起码可以作为一个参考。