隐约雷鸣,阴霾天空。

记一次springboot+vue+shiro的前后端分离项目中碰到的坑。

记一次springboot+vue+shiro的前后端分离项目中碰到的坑。

  1. 前言

    ​ 最近有了一个项目练手,一个简单的后台管理项目,本人学艺不精,和同学在前后端联调的时候碰上了跨域的问题,弄了一上午加半个下午才弄好,在此记录一下,希望可以帮到有需要的人。本篇文章因为作者本人技术的原因,大概率存在各种错误,仅供参考。

  2. 过程

    1. 在一个前后端分离的项目中,跨域问题是首要解决的,一开始尝试了在Controller上加上@CrossOrigin注解来解决,随后发现不起什么作用,于是去谷歌查了一下,说是可能和shiro有关,搜索到的部分资料说自定义过滤器然后重写onAccessDenied方法可以解决,试了一下发现不怎么行,当然不行的原因不在这里。
    2. 随后查询了前后端分离时,前端需要做的工作,一个普遍的答案是加上

      axios.defaults.withCredentials = true;
    3. 但还是没有解决跨域的问题,于是按照网上的方法重写了过滤器,加了一些调试代码,如下:(记得注册这个自定义的过滤器,相关代码网上有)

      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;
          }
      
      }
    4. 以上代码还不足以解决跨域的问题,还需要加上一段配置,如下:

      @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);
          }
      }

      以上两段代码可能会有不需要的地方或者是没有用的代码,我现在技术太差,还不能看出到底有什么可以改进的地方,如有高见还请评论区指示。

    5. 至此,跨域的问题已经解决了,但是随之而来的是JSESSIONID丢失的问题,因为http是无状态协议,所以前端提交请求时需要附带一个JSESSIONID给后端,让后端判断用户的身份信息,但是这个JSESSIONID是存储在Cookie里面的,跨域的时候这个Cookie不会跟着一起发送,看起来就像是JSESSIONID丢失了一样。然后服务端就会认为又是一个新的用户来了(因为没有带着JSESSIONID请求),就会创建一个新的Session,表现在前端就是报未登录错误(如果你有的话)
    6. 有一个想法是后端验证登录成功后返回一个JSESSIONID给前端,然后前端写入到Cookie里面,再带着请求发送到后端,这样就可以解决JSESSIONID丢失的问题了,但是尝试后发现,就算手动在请求头里面设置了Cookie,请求的时候这个Cookie还是不会被发出去,无奈之下放弃了(可能是我知识面太窄,可能还有我不知道的其他方法)
    7. 到这里离解决方案已经很近了,我们可以不设置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;
          }    
  3. 结论

    到此,跨域问题和JSESSIONID丢失的问题都解决了,以上解决方案可能不能解决你的问题,但起码可以作为一个参考。

添加新评论