springboot集成shiro自定义登陆过滤器

简介: springboot集成shiro自定义登陆过滤器

在上一篇博客springboot简单集成shiro权限管理中,用户在登录的过程中,有以下几个问题:

  • 用户在没有登陆的情况下,访问需要权限的接口,服务器自动跳转到登陆页面,前端无法控制;
  • 用户在登录成功后,服务器自动跳转到成功页,前端无法控制;
  • 用户在登录失败后,服务器自动刷新登录页面,前端无法控制;

很显然,这样的交互方式,用户体验上不是很好,并且在某些程度上也无法满足业务上的要求。所以,我们要对默认的FormAuthenticationFilter进行覆盖,实现我们自定义的Filter来解决用户交互的问题。

自定义UsernamePasswordAuthenticationFilter

  • 首先我们需要继承原先的FormAuthenticationFilter之所以继承这个FormAuthenticationFilter,有以下几点原因:
  1. FormAuthenticationFilter是默认拦截登录功能的过滤器,我们本身就是要改造登录功能,所以继承它很正常;
  2. 我们自定义的Filter需要复用里面的逻辑;
public class UsernamePasswordAuthenticationFilter extends FormAuthenticationFilter{}
复制代码
  • 其次,为了解决第一个问题,我们需要重写saveRequestAndRedirectToLogin方法
/**
 * 没有登陆的情况下,访问需要权限的接口,需要引导用户登陆
 *
 * @param request
 * @param response
 * @throws IOException
 */
@Override
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
    //  保存当前请求,以便后续登陆成功后重新请求
    this.saveRequest(request);
    // 1. 服务端直接跳转
    //   - 服务端重定向登陆页面
    if (autoRedirectToLogin) {
        this.redirectToLogin(request, response);
    } else {
        // 2. json模式
        //   - json数据格式告知前端需要跳转到登陆页面,前端根据指令跳转登陆页面
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        Map<String, String> metaInfo = new HashMap<>();
        // 告知前端需要跳转的登陆页面
        metaInfo.put("loginUrl", getLoginUrl());
        // 告知前端当前请求的url;这个信息也可以保存在前端
        metaInfo.put("currentRequest", req.getRequestURL().toString());
        ResultWrap.failure(802, "请登陆后再操作!", metaInfo)
          .writeToResponse(res);
    }
}
复制代码
  • 在这个方法中,我们通过配置autoRedirectToLogin参数的方式,既保留了原来服务器自动跳转的功能,又增强了服务器返回json给前端,让前端根据返回结果跳转到登陆页面的功能。这样就增强了应用程序的可控性和灵活性了。
  • 重写登陆成功的处理方法onLoginSuccess
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
    // 查询当前用户自定义的登陆成功需要跳转的页面,可以更加灵活控制用户页面跳转
    String successUrl = loginSuccessPageFetch.successUrl(token, subject);
    // 如果没有自定义的成功页面,那么跳转默认成功页
    if (StringUtils.isEmpty(successUrl)) {
        successUrl = this.getSuccessUrl();
    }
    if (loginSuccessAutoRedirect) {
        // 服务端直接重定向到目标页面
        WebUtils.redirectToSavedRequest(request, response, successUrl);
    } else {
        SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request);
        if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase("GET")) {
            successUrl = savedRequest.getRequestUrl();
        }
        // 返回json数据格式告知前端跳转目标页面
        HttpServletResponse res = (HttpServletResponse) response;
        Map<String, String> data = new HashMap<>();
        // 登陆成功后跳转的目标页面
        data.put("successUrl", successUrl);
        ResultWrap.success(data).writeToResponse(res);
    }
    return false;
}
复制代码
  1. 登陆成功后,我们内置了一个个性化的成功页,用于保证针对不同的用户会有定制化的登陆成功页。
  2. 通过自定义的loginSuccessAutoRedirect属性来决定用户登陆成功后是直接由服务端控制页面跳转还是返回json让前端控制交互行为。
  3. 我们在用户登陆成功后,会获取前面保存的请求,以便用户在登录成功后能直接回到登录前点击的页面。
  • 重写用户登录失败的方法onLoginFailure
@Override
  protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
    if (log.isDebugEnabled()) {
      log.debug("Authentication exception", e);
    }
    this.setFailureAttribute(request, e);
    if (!loginFailureAutoRedirect) {
      // 返回json数据格式告知前端跳转目标页面
      HttpServletResponse res = (HttpServletResponse) response;
      ResultWrap.failure(803, "用户名或密码错误,请核对后无误后重新提交!", null).writeToResponse(res);
    }
    return true;
  }
复制代码
  1. 登陆失败我们使用自定义属性loginFailureAutoRedirect来控制失败的动作是由服务端直接跳转页面还是返回json由前端控制用户交互。
  2. 在这个方法的逻辑里面没有看到跳转的功能,是因为我们直接把父类的默认实现拿过来了,在原有的逻辑上做了修改。既然默认是服务端跳转的功能,那么我们只需要补充返回json的功能即可。

覆盖默认的FormAuthenticationFilter

现在我们已经写好了自定义的用户名密码登陆过滤器,下面我们就把它加入到shiro的配置中去,这样才能生效:

@Bean
  public ShiroFilterFactoryBean shiroFilterFactoryBean() {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager());
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    // 设置不需要权限的url
    String[] permitUrls = properties.getPermitUrls();
    if (ArrayUtils.isNotEmpty(permitUrls)) {
      for (String permitUrl : permitUrls) {
        filterChainDefinitionMap.put(permitUrl, "anon");
      }
    }
    // 设置退出的url
    String logoutUrl = properties.getLogoutUrl();
    filterChainDefinitionMap.put(logoutUrl, "logout");
    // 设置需要权限验证的url
    filterChainDefinitionMap.put("/**", "authc");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    // 设置提交登陆的url
    String loginUrl = properties.getLoginUrl();
    shiroFilterFactoryBean.setLoginUrl(loginUrl);
    // 设置登陆成功跳转的url
    String successUrl = properties.getSuccessUrl();
    shiroFilterFactoryBean.setSuccessUrl(successUrl);
    // 添加自定义Filter
    shiroFilterFactoryBean.setFilters(customFilters());
    return shiroFilterFactoryBean;
  }
/**
   * 自定义过滤器
   *
   * @return
   */
  private Map<String, Filter> customFilters() {
    Map<String, Filter> filters = new LinkedHashMap<>();
    // 自定义FormAuthenticationFilter,用于管理用户登陆的,包括登陆成功后的动作、登陆失败的动作
    // 可查看org.apache.shiro.web.filter.mgt.DefaultFilter,可覆盖里面对应的authc
    UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter();
    SecurityProperties.UsernamePasswordAuthenticationFilterConfig usernamePasswordAuthenticationFilterConfig = properties.getUsernamePasswordAuthenticationFilter();
    usernamePasswordAuthenticationFilter.setAutoRedirectToLogin(usernamePasswordAuthenticationFilterConfig.isAutoRedirectToLogin());
    usernamePasswordAuthenticationFilter.setLoginSuccessAutoRedirect(usernamePasswordAuthenticationFilterConfig.isLoginSuccessAutoRedirect());
    usernamePasswordAuthenticationFilter.setLoginFailureAutoRedirect(usernamePasswordAuthenticationFilterConfig.isLoginFailureAutoRedirect());
    filters.put("authc", usernamePasswordAuthenticationFilter);
    return filters;
  }
复制代码

上面的代码重点看 【添加自定义Filte】 ,其实原理就是把默认的authc过滤器给覆盖掉,换成我们自定义的过滤器,这样的话,我们的过滤器才能生效。

完整UsernamePasswordAuthenticationFilter代码

import com.example.awesomespring.vo.ResultWrap;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.SavedRequest;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
 * @author zouwei
 * @className UsernamePasswordAuthenticationFilter
 * @date: 2022/8/2 上午12:14
 * @description:
 */
@Data
@Slf4j
public class UsernamePasswordAuthenticationFilter extends FormAuthenticationFilter {
  //  如果用户没有登陆的情况下访问需要权限的接口,服务端是否自动调整到登陆页面
  private boolean autoRedirectToLogin = true;
  // 登陆成功后是否自动跳转
  private boolean loginSuccessAutoRedirect = true;
  // 登陆失败后是否跳转
  private boolean loginFailureAutoRedirect = true;
  /**
   * 个性化定制每个登陆成功的账号跳转的url
   */
  private LoginSuccessPageFetch loginSuccessPageFetch = new LoginSuccessPageFetch(){};
  public UsernamePasswordAuthenticationFilter() {
  }
  /**
   * 没有登陆的情况下,访问需要权限的接口,需要引导用户登陆
   *
   * @param request
   * @param response
   * @throws IOException
   */
  @Override
  protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
    //  保存当前请求,以便后续登陆成功后重新请求
    this.saveRequest(request);
    // 1. 服务端直接跳转
    //   - 服务端重定向登陆页面
    if (autoRedirectToLogin) {
      this.redirectToLogin(request, response);
    } else {
      // 2. json模式
      //   - json数据格式告知前端需要跳转到登陆页面,前端根据指令跳转登陆页面
      HttpServletRequest req = (HttpServletRequest) request;
      HttpServletResponse res = (HttpServletResponse) response;
      Map<String, String> metaInfo = new HashMap<>();
      // 告知前端需要跳转的登陆页面
      metaInfo.put("loginUrl", getLoginUrl());
      // 告知前端当前请求的url;这个信息也可以保存在前端
      metaInfo.put("currentRequest", req.getRequestURL().toString());
      ResultWrap.failure(802, "请登陆后再操作!", metaInfo)
          .writeToResponse(res);
    }
  }
  @Override
  protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
    // 查询当前用户自定义的登陆成功需要跳转的页面,可以更加灵活控制用户页面跳转
    String successUrl = loginSuccessPageFetch.successUrl(token, subject);
    // 如果没有自定义的成功页面,那么跳转默认成功页
    if (StringUtils.isEmpty(successUrl)) {
      successUrl = this.getSuccessUrl();
    }
    if (loginSuccessAutoRedirect) {
      // 服务端直接重定向到目标页面
      WebUtils.redirectToSavedRequest(request, response, successUrl);
    } else {
      SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request);
      if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase("GET")) {
        successUrl = savedRequest.getRequestUrl();
      }
      // 返回json数据格式告知前端跳转目标页面
      HttpServletResponse res = (HttpServletResponse) response;
      Map<String, String> data = new HashMap<>();
      // 登陆成功后跳转的目标页面
      data.put("successUrl", successUrl);
      ResultWrap.success(data).writeToResponse(res);
    }
    return false;
  }
  @Override
  protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
    if (log.isDebugEnabled()) {
      log.debug("Authentication exception", e);
    }
    this.setFailureAttribute(request, e);
    if (!loginFailureAutoRedirect) {
      // 返回json数据格式告知前端跳转目标页面
      HttpServletResponse res = (HttpServletResponse) response;
      ResultWrap.failure(803, "用户名或密码错误,请核对后无误后重新提交!", null).writeToResponse(res);
    }
    return true;
  }
  /**
   * 针对不同的人员登陆成功后有不同的跳转页面而设计
   */
  public interface LoginSuccessPageFetch {
    default String successUrl(AuthenticationToken token, Subject subject) {
      return StringUtils.EMPTY;
    }
  }
}
复制代码

ResultWrap.java

import com.example.awesomespring.util.JsonUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Objects;
/**
 * @author zouwei
 * @className ResultWrap
 * @date: 2022/8/2 下午2:02
 * @description:
 */
@Data
@AllArgsConstructor
public class ResultWrap<T, M> {
  //  方便前端判断当前请求处理结果是否正常
  private int code;
  //  业务处理结果
  private T data;
  //  产生错误的情况下,提示用户信息
  private String message;
  //  产生错误情况下的异常堆栈,提示开发人员
  private String error;
  //  发生错误的时候,返回的附加信息
  private M metaInfo;
  /**
   * 成功带处理结果
   *
   * @param data
   * @param <T>
   * @return
   */
  public static <T> ResultWrap success(T data) {
    return new ResultWrap(HttpStatus.OK.value(), data, StringUtils.EMPTY, StringUtils.EMPTY, null);
  }
  /**
   * 成功不带处理结果
   *
   * @return
   */
  public static ResultWrap success() {
    return success(HttpStatus.OK.name());
  }
  /**
   * 失败
   *
   * @param code
   * @param message
   * @param error
   * @return
   */
  public static <M> ResultWrap failure(int code, String message, String error, M metaInfo) {
    return new ResultWrap(code, null, message, error, metaInfo);
  }
  /**
   * 失败
   *
   * @param code
   * @param message
   * @param error
   * @param metaInfo
   * @param <M>
   * @return
   */
  public static <M> ResultWrap failure(int code, String message, Exception error, M metaInfo) {
    return failure(code, message, error.getStackTrace().toString(), metaInfo);
  }
  /**
   * 失败
   *
   * @param code
   * @param message
   * @param error
   * @return
   */
  public static ResultWrap failure(int code, String message, Exception error) {
    String errorMessage = StringUtils.EMPTY;
    if (Objects.nonNull(error)) {
      errorMessage = error.getStackTrace().toString();
    }
    return failure(code, message, errorMessage, null);
  }
  /**
   * 失败
   *
   * @param code
   * @param message
   * @param metaInfo
   * @param <M>
   * @return
   */
  public static <M> ResultWrap failure(int code, String message, M metaInfo) {
    return failure(code, message, StringUtils.EMPTY, metaInfo);
  }
  private static final String APPLICATION_JSON_VALUE = "application/json;charset=UTF-8";
  /**
   * 把结果写入响应中
   *
   * @param response
   */
  public void writeToResponse(HttpServletResponse response) {
    int code = this.getCode();
    if (Objects.isNull(HttpStatus.resolve(code))) {
      response.setStatus(HttpStatus.OK.value());
    } else {
      response.setStatus(code);
    }
    response.setContentType(APPLICATION_JSON_VALUE);
    try (PrintWriter writer = response.getWriter()) {
      writer.write(JsonUtil.obj2String(this));
      writer.flush();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}
复制代码

JsonUtil.java

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.util.Objects;
/**
 * @author zouwei
 * @className JsonUtil
 * @date: 2022/8/2 下午3:08
 * @description:
 */
@Slf4j
public final class JsonUtil {
  /** 防止使用者直接new JsonUtil() */
  private JsonUtil() {}
  private static ObjectMapper objectMapper = new ObjectMapper();
  static {
    // 对象所有字段全部列入序列化
    objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
    /** 所有日期全部格式化成时间戳 因为即使指定了DateFormat,也不一定能满足所有的格式化情况,所以统一为时间戳,让使用者按需转换 */
    objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
    /** 忽略空Bean转json的错误 假设只是new方式创建对象,并且没有对里面的属性赋值,也要保证序列化的时候不报错 */
    objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    /** 忽略反序列化中json字符串中存在,但java对象中不存在的字段 */
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
  }
  /**
   * 对象转换成json字符串
   *
   * @param obj
   * @param <T>
   * @return
   */
  public static <T> String obj2String(T obj) {
    return obj2String(obj, null);
  }
  /**
   * 对象转换成json字符串
   *
   * @param obj
   * @param <T>
   * @return
   */
  public static <T> String obj2String(T obj, String defaultValue) {
    if (Objects.isNull(obj)) {
      return defaultValue;
    }
    try {
      return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
    } catch (Exception e) {
      log.warn("Parse object to String error", e);
      // 即使序列化出错,也要保证程序走下去
      return null;
    }
  }
  /**
   * 对象转json字符串(带美化效果)
   *
   * @param obj
   * @param <T>
   * @return
   */
  public static <T> String obj2StringPretty(T obj) {
    if (Objects.isNull(obj)) {
      return null;
    }
    try {
      return obj instanceof String
          ? (String) obj
          : objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
    } catch (Exception e) {
      log.warn("Parse object to String error", e);
      // 即使序列化出错,也要保证程序走下去
      return null;
    }
  }
  /**
   * json字符串转简单对象
   *
   * @param <T>
   * @param json
   * @param clazz
   * @return
   */
  public static <T> T string2Obj(String json, Class<T> clazz) {
    if (StringUtils.isEmpty(json) || Objects.isNull(clazz)) {
      return null;
    }
    try {
      return clazz.equals(String.class) ? (T) json : objectMapper.readValue(json, clazz);
    } catch (Exception e) {
      log.warn("Parse String to Object error", e);
      // 即使序列化出错,也要保证程序走下去
      return null;
    }
  }
  /**
   * json字符串转复杂对象
   *
   * @param json
   * @param typeReference 例如:new TypeReference<List<User>>(){}
   * @param <T> 例如:List<User>
   * @return
   */
  public static <T> T string2Obj(String json, TypeReference<T> typeReference) {
    if (StringUtils.isEmpty(json) || Objects.isNull(typeReference)) {
      return null;
    }
    try {
      return (T)
          (typeReference.getType().equals(String.class)
              ? (T) json
              : objectMapper.readValue(json, typeReference));
    } catch (Exception e) {
      log.warn("Parse String to Object error", e);
      // 即使序列化出错,也要保证程序走下去
      return null;
    }
  }
  /**
   * json字符串转复杂对象
   *
   * @param json
   * @param collectionClass 例如:List.class
   * @param elementClasses 例如:User.class
   * @param <T> 例如:List<User>
   * @return
   */
  public static <T> T string2Obj(
      String json, Class<?> collectionClass, Class<?>... elementClasses) {
    if (StringUtils.isEmpty(json)
        || Objects.isNull(collectionClass)
        || Objects.isNull(elementClasses)) {
      return null;
    }
    JavaType javaType =
        objectMapper
            .getTypeFactory()
            .constructParametricType(collectionClass, elementClasses);
    try {
      return objectMapper.readValue(json, javaType);
    } catch (Exception e) {
      log.warn("Parse String to Object error", e);
      // 即使序列化出错,也要保证程序走下去
      return null;
    }
  }
}
复制代码

至此,在shiro中如何实现更灵活的登陆控制就编写完毕了。后面会陆续讲解我在使用shiro时遇到的其他问题,以及相应的解决方案。


相关文章
|
2月前
|
并行计算 Java 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
234 0
|
2月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
243 2
|
8天前
|
NoSQL Java Redis
Spring Boot 自动配置机制:从原理到自定义
Spring Boot 的自动配置机制通过 `spring.factories` 文件和 `@EnableAutoConfiguration` 注解,根据类路径中的依赖和条件注解自动配置所需的 Bean,大大简化了开发过程。本文深入探讨了自动配置的原理、条件化配置、自定义自动配置以及实际应用案例,帮助开发者更好地理解和利用这一强大特性。
52 14
|
2月前
|
安全 Java 数据库
shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)
这篇文章是关于Apache Shiro权限管理框架的详细学习指南,涵盖了Shiro的基本概念、认证与授权流程,并通过Spring Boot测试模块演示了Shiro在单应用环境下的使用,包括与IniRealm、JdbcRealm的集成以及自定义Realm的实现。
50 3
shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)
|
29天前
|
消息中间件 监控 Java
您是否已集成 Spring Boot 与 ActiveMQ?
您是否已集成 Spring Boot 与 ActiveMQ?
53 0
|
29天前
|
安全 Java 应用服务中间件
如何将Spring Boot应用程序运行到自定义端口
如何将Spring Boot应用程序运行到自定义端口
46 0
|
2月前
|
Java API Spring
springboot学习六:Spring Boot2.x 过滤器基础入门&实战项目场景实现
这篇文章是关于Spring Boot 2.x中过滤器的基础知识和实战项目应用的教程。
40 0
springboot学习六:Spring Boot2.x 过滤器基础入门&实战项目场景实现
|
2月前
|
NoSQL Java Redis
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
这篇文章介绍了如何使用Spring Boot整合Apache Shiro框架进行后端开发,包括认证和授权流程,并使用Redis存储Token以及MD5加密用户密码。
42 0
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
|
1月前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
48 0
|
2月前
|
前端开发 Java 数据库
springBoot:template engine&自定义一个mvc&后端给前端传数据&增删改查 (三)
本文介绍了如何自定义一个 MVC 框架,包括后端向前端传递数据、前后端代理配置、实现增删改查功能以及分页查询。详细展示了代码示例,从配置文件到控制器、服务层和数据访问层的实现,帮助开发者快速理解和应用。