Spring Boot + Vue + Shiro 实现前后端分离

2023-11-19

前后端分离思想:前端只利用 Json 来和后端进行交互,后端不返回页面,只返回 Json 数据。前后端之间完全通过 public API 约定。

Springboot 就不再赘述了,Controller 层返回 Json 数据。

@RequestMapping(value = "/add", method = RequestMethod.POST)
@ResponseBody
public JSONResult addClient(@RequestBody String param) {
    JSONObject jsonObject = JSON.parseObject(param);
    String task = jsonObject.getString("task");
    List<Object> list = jsonObject.getJSONArray("attributes");
    List<String> attrList = new LinkedList(list);
    Client client = JSON.parseObject(jsonObject.getJSONObject("client").toJSONString(),new TypeReference<Client>(){});
    clientService.addClient(client, task, attrList);
    return JSONResult.ok();
}

三、前端 Vue + ElementUI + Vue router + Vuex + axios + webpack
主要参考:

https://cn.vuejs.org/v2/guide/
https://github.com/PanJiaChen/vue-admin-template/blob/master/README-zh.md
https://github.com/PanJiaChen/vue-element-admin

这里主要说一下开发工程中遇到的问题:

解决跨域分两种:

1、server 端是自己开发的,这样可以在在后端增加一个拦截器

@Component
public class CommonIntercepter implements HandlerInterceptor {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public boolean preHandle(HttpServletRequest request,
                         HttpServletResponse response, Object handler) throws Exception {
    //允许跨域,不能放在postHandle内
    response.setHeader("Access-Control-Allow-Origin", "*");
    if (request.getMethod().equals("OPTIONS")) {
        response.addHeader("Access-Control-Allow-Methods", "GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,PATCH");
        response.addHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization");
    }
    return true;
}

}
response.setHeader(“Access-Control-Allow-Origin”, “*”);

主要就是在 Response Header 中增加 “Access-Control-Allow-Origin: *”

if (request.getMethod().equals(“OPTIONS”)) {
response.addHeader(“Access-Control-Allow-Methods”, “GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,PATCH”);
response.addHeader(“Access-Control-Allow-Headers”, “Content-Type, Accept, Authorization”);
}
由于我们在前后端分离中集成了 shiro,因此需要在 headers 中自定义一个’Authorization’字段,此时普通的 GET、POST 等请求会变成 preflighted request,即在 GET、POST 请求之前会预先发一个 OPTIONS 请求,这个后面再说。推荐一篇博客介绍 preflighted request。

https://blog.csdn.net/cc1314_/article/details/78272329

2、server 端不是自己开发的,可以在前端加 proxyTable。

不过这个只能在开发的时候用,后续部署,可以把前端项目作为静态资源放到后端,这样就不存在跨域(由于项目需要,我现在是这么做的,根据网上博客介绍,可以使用 nginx,具体怎么做可以在网上搜一下)。

遇到了网上很多人说的,proxyTable 无论如何修改,都没效果的现象。

1、(非常重要)确保 proxyTable 配置的地址能访问,因为如果不能访问,在浏览器 F12 调试的时候看到的依然会是提示 404。

并且注意,在 F12 看到的 js 提示错误的域名,是 js 写的那个域名,并不是代理后的域名。(l 楼主就遇到这个问题,后端地址缺少了查询参数,代理设置为后端地址,然而 F12 看到的错误依然还是本地的域名,并不是代理后的域名)

2、就是要手动再执行一次 npm run dev

四、前后端分离项目中集成 shiro
可以参考:

blog.csdn.net/u013615903/article/details/78781166

这里说一下实际开发集成过程中遇到的问题:

1、OPTIONS 请求不带’Authorization’请求头字段:

前后端分离项目中,由于跨域,会导致复杂请求,即会发送 preflighted request,这样会导致在 GET/POST 等请求之前会先发一个 OPTIONS 请求,但 OPTIONS 请求并不带 shiro 的’Authorization’字段(shiro 的 Session),即 OPTIONS 请求不能通过 shiro 验证,会返回未认证的信息。

解决方法:给 shiro 增加一个过滤器,过滤 OPTIONS 请求

public class CORSAuthenticationFilter extends FormAuthenticationFilter {

private static final Logger logger = LoggerFactory.getLogger(CORSAuthenticationFilter.class);

public CORSAuthenticationFilter() {
    super();
}

@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    //Always return true if the request's method is OPTIONSif (request instanceof HttpServletRequest) {
        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;
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setStatus(HttpServletResponse.SC_OK);
    res.setCharacterEncoding("UTF-8");
    PrintWriter writer = res.getWriter();
    Map<String, Object> map= new HashMap<>();
    map.put("code", 702);
    map.put("msg", "未登录");
    writer.write(JSON.toJSONString(map));
    writer.close();
    return false;
}

}
贴一下我的 config 文件:

@Configuration
public class ShiroConfig {

@Bean
public Realm realm() {
    return new DDRealm();
}

@Bean
public CacheManager cacheManager() {
    return new MemoryConstrainedCacheManager();
}

/**
 * cookie对象;
 * rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。
 * @return
 */
@Bean
public SimpleCookie rememberMeCookie(){
    //System.out.println("ShiroConfiguration.rememberMeCookie()");
    //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
    SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
    //<!-- 记住我cookie生效时间30天 ,单位秒;-->
    simpleCookie.setMaxAge(259200);
    return simpleCookie;
}

/**
 * cookie管理对象;
 * rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
 * @return
 */
@Bean
public CookieRememberMeManager rememberMeManager(){
    //System.out.println("ShiroConfiguration.rememberMeManager()");
    CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
    cookieRememberMeManager.setCookie(rememberMeCookie());
    //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
    cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
    return cookieRememberMeManager;
}

@Bean
public SecurityManager securityManager() {
    DefaultWebSecurityManager sm = new DefaultWebSecurityManager();
    sm.setRealm(realm());
    sm.setCacheManager(cacheManager());
    //注入记住我管理器
    sm.setRememberMeManager(rememberMeManager());
    //注入自定义sessionManager
    sm.setSessionManager(sessionManager());
    return sm;
}

//自定义sessionManager
@Bean
public SessionManager sessionManager() {
    return new CustomSessionManager();
}

public CORSAuthenticationFilter corsAuthenticationFilter(){
    return new CORSAuthenticationFilter();
}

@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
    shiroFilter.setSecurityManager(securityManager);
    //SecurityUtils.setSecurityManager(securityManager);
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    //配置不会被拦截的链接,顺序判断
    filterChainDefinitionMap.put("/", "anon");
    filterChainDefinitionMap.put("/static/js/**", "anon");
    filterChainDefinitionMap.put("/static/css/**", "anon");
    filterChainDefinitionMap.put("/static/fonts/**", "anon");
    filterChainDefinitionMap.put("/login/**", "anon");
    filterChainDefinitionMap.put("/corp/call_back/receive", "anon");
    //authc:所有url必须通过认证才能访问,anon:所有url都可以匿名访问
    filterChainDefinitionMap.put("/**", "corsAuthenticationFilter");
    shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
    //自定义过滤器
    Map<String, Filter> filterMap = new LinkedHashMap<>();
    filterMap.put("corsAuthenticationFilter", corsAuthenticationFilter());
    shiroFilter.setFilters(filterMap);

    return shiroFilter;
}

/**
 * Shiro生命周期处理器 * @return
 */
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    return new LifecycleBeanPostProcessor();
}

/**
 * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能 * @return
 */
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    advisorAutoProxyCreator.setProxyTargetClass(true);
    return advisorAutoProxyCreator;
}

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
    return authorizationAttributeSourceAdvisor;
}

}
2、设置 session 失效时间

shiro session 默认失效时间是 30min,我们在自定义的 sessionManager 的构造函数中设置失效时间为其他值

public class CustomSessionManager extends DefaultWebSessionManager {

private static final Logger logger = LoggerFactory.getLogger(CustomSessionManager.class);

private static final String AUTHORIZATION = "Authorization";

private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

public CustomSessionManager() {
    super();
    setGlobalSessionTimeout(DEFAULT_GLOBAL_SESSION_TIMEOUT * 48);
}

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

}
五、部署项目
前端项目部署主要分两种方法:

1、将前端项目打包(npm run build)成静态资源文件,放入后端,一起打包。后端写一个 Controller 返回前端界面(我使用 Vue 开发的是单页面应用),但是这样其实又将前后端耦合在一起了,不过起码做到前后端分离开发,方便开发的目的已经达成,也初步达成了要求,由于项目的需要,我是这样做的,并且免去了跨域问题。

@RequestMapping(value = {"/", “/index”}, method = RequestMethod.GET)
public String index() {
return “/index”;
}
2. 将前端工程另启一个服务(tomcat,nginx,nodejs),这样有跨域的问题。

说一下我遇到的问题:

1、nginx 反向代理,导致当访问无权限的页面时,shiro 302 到 unauth 的 controller,访问的地址是 https,重定向地址是 http,导致了无法访问。

不使用 shiro 的 shiroFilter.setLoginUrl("/unauth");

当页面无权限访问时,我们在过滤器里直接返回错误信息,不利用 shiro 自带的跳转。看过滤器中的 onAccessDenied 函数

public class CORSAuthenticationFilter extends FormAuthenticationFilter {

private static final Logger logger = LoggerFactory.getLogger(CORSAuthenticationFilter.class);

public CORSAuthenticationFilter() {
    super();
}

@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    //Always return true if the request's method is OPTIONS
    if (request instanceof HttpServletRequest) {
        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;
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setStatus(HttpServletResponse.SC_OK);
    res.setCharacterEncoding("UTF-8");
    PrintWriter writer = res.getWriter();
    Map<String, Object> map= new HashMap<>();
    map.put("code", 702);
    map.put("msg", "未登录");
    writer.write(JSON.toJSONString(map));
    writer.close();
    return false;
}

}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring Boot + Vue + Shiro 实现前后端分离 的相关文章

随机推荐

  • Kibana报错:Kibana server is not ready yet解决方法

    环境及版本 elasticsearch和kibana均为包安装的7 6 2 系统为unbutu22 04 1 部署完后访问kibana的web界面 出现kibana server is not ready yet 遇到这个问题后也是搜索了一
  • python注意事项

    python注意事项 1 缩进问题 每一个缩进都可能会导致有bug 因此要格外注意缩进 对齐 空格问题 尤其是循环体下的空格 一定要对齐 一般是缩进4格 2 标点符号的使用小结 逗号后面要有空格 冒号也是 等号前后都要有空格 3 字符串使用
  • MBA-day31 绝对值的几何意义

    绝对值的几何意义 1 x 2 x 4 由图可知 x 有 3 处取值区间 x gt 2 无最大值 x gt 4 无最大值 2 lt x lt 4 当x取值为 2和4时 存在几何意义中的最小值为 6 2 x 2 x 4 3 是否有根 由题1中
  • JAVAWEB编程题

    1 登陆验证代码
  • 人工智能大模型加速数据库存储模型发展 行列混合存储下的破局

    数据存储模型 专栏内容 postgresql内核源码分析 手写数据库toadb 并发编程 toadb开源库 个人主页 我的主页 座右铭 天行健 君子以自强不息 地势坤 君子以厚德载物 概述 在数据库的发展过程中 关系型数据库是一个里程碑式的
  • STL标准模板库学习笔记三(STL哈希容器)

    关联式容器 排序 的底层实现采用的树存储结构 更确切的说是红黑树结构 无序容器 哈希 的底层实现采用的是哈希表的存储结构 基于底层实现采用了不同的数据结构 因此和关联式容器相比 无序容器具有以下 2 个特点 无序容器内部存储的键值对是无序的
  • 欢迎访问阿里云Go Module代理仓库服务

    简介 go module公共代理仓库 代理并缓存go模块 你可以利用该代理来避免DNS污染导致的模块拉取缓慢或失败的问题 加速你的构建 地址 https mirrors aliyun com goproxy 使用帮助 1 使用go1 11以
  • leetcode刷题__删除有序数组中的重复项

    文章目录 题目描述 Java解决方法 题目描述 Java解决方法 class Solution public int removeDuplicates int nums int len nums length if len 0 return
  • Day81-爱心代码音乐版

    项目地址 截止到2023 9月有效 点击跳转 先看效果 素材来自网络 自己加了播放音乐的效果 直接上代码
  • 快速排序算法及其改进算法实现

    快速排序算法不稳定 快速排序算法在大多数的计算机上运行得都比其他排序算法快 而且排序算法消耗资源少 就平均时间而言快排是所有内部排序中最好的一个 对于已经排好的数组 最速排序有最坏时间复杂度为o n 2 当数组长度很小时 快排往往比其他排序
  • 【openGL2021版】链接FBX模型库

    openGL2021版 链接FBX模型库 大家好 我是Lampard猿奋 欢迎来到船新的openGL基础系列的博客 今天主要实现的是链接FBX模型第三方库 一 demo回顾 上周我已经实现了粒子系统 可以看到场景中已经存在着像萤火虫一样的闪
  • Wsl 开发环境配置

    Apt换源 切换清华源 sudo sed i s http archive ubuntu com https mirrors tuna tsinghua edu cn g etc apt sources list sudo sed i s
  • 图文详解 排序算法对比以及能解决的问题

    我们会使用的排序算法 总结源自 极客时间 王争老师 地址 https time geekbang org column intro 126 能看到所有的算法和数据结构的网站 https visualgo net en 这里是关于排序算法的链
  • 六大设计原则--开闭原则

    定义 software entities like classes modules and functions should be open for extension but closed for modifications 一个软件实体
  • php background,CSS BACKGROUND定位背景上下左右偏移

    css中我们经常使用background加载图片作为背景 这个背景图片可以通过具体属性的设置调整其上下左右偏移 调整背景图的上下左右偏移可以通过设置background属性或background position属性来实现 我们以div加背
  • Linux:非root、普通用户安装rpm(fontconfig、fc-cache命令)、中文字体

    前提 非root帐号 普通帐号 不能使用yum rpm命令 没root权限 安装fontconfig 测试是否安装 安装字体后需要使用fontconfig包的fc cache fv命令刷新字体缓存 可以使用下面命令测试 二选一就行 fc l
  • java版kafka producer实现

    需求 1 kafka server已经配置完全 且设定了访问限制 基于这一点 必须要设定认证 及预先分配的账号密码 2 由于项目开发环境是java 且不允许使用LogStash 基于这一点 必须实现一个java版的producer 先贴一份
  • error C4430: 缺少类型说明符 - 假定为 int。注意: C++ 不支持默认 int

    原因是你两个类的头文件互相包含了 这样是有问题的 参考博客 error C4430 缺少类型说明符 假定为 int 注意 C 不支持默认 int 很多不懂呀 博客园
  • CountDownLatch、CyclicBarrier、Semaphore源码解析

    1 CountDownLatch 计数器 CountDownLatch CountDownLatch 类位于java util concurrent包下 利用它可以实现类似计数器的功能 比如有一个任务A 它要等待其他4个任务执行完毕之后才能
  • Spring Boot + Vue + Shiro 实现前后端分离

    前后端分离思想 前端只利用 Json 来和后端进行交互 后端不返回页面 只返回 Json 数据 前后端之间完全通过 public API 约定 Springboot 就不再赘述了 Controller 层返回 Json 数据 Request