首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >SSM综合案例之动态权限实战教程

SSM综合案例之动态权限实战教程

作者头像
张哥编程
发布2024-12-13 14:23:14
发布2024-12-13 14:23:14
1860
举报
文章被收录于专栏:云计算linux云计算linux

一、课程目标

代码语言:javascript
复制
1. 【了解】动态权限配置
2. 【掌握】AOP日志管理

二、动态权限

2.1 将公开权限设置为无需认证即可访问

代码语言:javascript
复制
<!-- 配置不拦截的资源 -->
    <security:http pattern="/login.jsp" security="none"/>
    <security:http pattern="/failer.jsp" security="none"/>
    <security:http pattern="/css/**" security="none"/>
    <security:http pattern="/img/**" security="none"/>
    <security:http pattern="/layui/**" security="none"/>

2.2 配置具体的规则

代码语言:javascript
复制
<!--
        配置具体的规则
        auto-config="true"  不用自己编写登录的页面,框架提供默认登录页面
        use-expressions="true"  是否使用SPEL表达式(否则只能使用USER_角色的形式配置)
    -->
    <security:http auto-config="true" use-expressions="true">
        <!-- 同源策略 如果页面使用iframe需要配置 否则不能使用 -->
        <security:headers>
            <security:frame-options disabled="true"/>
        </security:headers>

        <!-- 定义跳转的具体的页面 -->
        <security:form-login
                login-page="/login.jsp"
                login-processing-url="/login"
                default-target-url="/index.jsp"
                authentication-failure-url="/failer.jsp"
                authentication-success-forward-url="/users/name"
        />
        <security:intercept-url pattern="/**"   access="isAuthenticated()"/>
        <!-- 配置具体的拦截的规则 pattern="请求路径的规则" access="isAuthenticated()" 
        <!-- 权限框架本质是过滤器链 会依次进行权限验证 如果验证通过继续执行  
        该配置 配置的是 除以上不拦截的资源外 所有url请求必须拥有认证的权限(登录后才能访问)
-->
        <!-- 关闭跨域请求 -->
        <security:csrf disabled="true"/>
        <!-- 退出 -->
        <security:logout invalidate-session="true" logout-url="/logout" logout-success-url="/login.jsp" />
    </security:http>

2.3 设置权限数据

2.4 自定义相关过滤器与决策器

自定义决策管理器

权限框架本身是由多个不同功能的过滤器组成的,不同的过滤器负责不同的功能例如认证过滤、静态资源过滤、等 授权过滤也是一样,决策器就是同于判断当前请求的url当前账号是否拥有权限

代码语言:javascript
复制
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Iterator;
@Service
//决策管理器
public class MyAccessDecisionManager implements AccessDecisionManager {

    // decide 方法是判定是否拥有权限的决策方法,
    //authentication 是释CustomUserService中循环添加到 GrantedAuthority 对象中的权限信息集合.
    //object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
    //configAttributes 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

        if(null== configAttributes || configAttributes.size() <=0) {
            return;
        }
        ConfigAttribute c;
        String needRole;
        for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
            c = iter.next();
            needRole = c.getAttribute();
            for(GrantedAuthority ga : authentication.getAuthorities()) {//authentication 为在注释1 中循环添加到 GrantedAuthority 对象中的权限信息集合
                if(needRole.trim().equals(ga.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("no right");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}
自定义权限加载器
代码语言:javascript
复制
import com.yunhe.javabean.Permission;
import com.yunhe.mapper.PermissionMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.*;

@Service
//授权管理器
//用于当前项目要动态配置的权限信息
//从数据库中取出所有的权限信息 进行配置
//这样当客户请求对应权限时进行权限验证(因为不可能将所有的url都过滤)
public class MyInvocationSecurityMetadataSourceService  implements
        FilterInvocationSecurityMetadataSource {

    @Autowired
    //注入权限查询的dao层
    private PermissionMapper permissionMapper;

    private HashMap<String, Collection<ConfigAttribute>> map =null;

    /**
     * 加载权限表中所有权限
     */
    public void loadResourceDefine(){
        map = new HashMap<>();
        Collection<ConfigAttribute> array;
        ConfigAttribute cfg;
        //动态查询当前数据库中所有的权限
        List<Permission> permissions = permissionMapper.selectAll();
        for(Permission permission : permissions) {
            array = new ArrayList<>();
            cfg = new SecurityConfig(permission.getPermissionName());
            //此处只添加了权限的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
            array.add(cfg);
            //用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value,
            //实际加载存储的结构为 url->[name1,name2....]
            //url是进行请求url拦截使用的  name是进行权限验证使用的
            //也就是说在用户进行授权时 实际加载的是权限名称

            map.put(permission.getUrl(), array);
        }

    }

    //此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        if(map ==null) loadResourceDefine();
        //object 中包含用户请求的request 信息
        HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        AntPathRequestMatcher matcher;
        String resUrl;
        for(Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {
            resUrl = iter.next();
            matcher = new AntPathRequestMatcher(resUrl);
            if(matcher.matches(request)) {
                return map.get(resUrl);
            }
        }
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}
自定义权限拦截器
代码语言:javascript
复制
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Service;

import javax.servlet.*;
import java.io.IOException;

@Service
//自定义权限拦截器
//FilterSecurityInterceptor是权限框架中用于处理权限验证的过滤器
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {


    //使用自己定义权限加载器
    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    //使用自定义的决策管理器
    @Autowired
    public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }


    public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
//执行下一个拦截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }

    @Override
    public void destroy() {

    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
}

2.5 将自定义权限拦截器配置入权限框架中

在security:http标签中添加

代码语言:javascript
复制
<!-- 配置自定义拦截器 将定义拦截器配置到当前权限框架拦截器链中的指定位置
           将其配置在本身的权限认证过滤器之后执行
          -->
<security:custom-filter ref="myFilterSecurityInterceptor" after="FILTER_SECURITY_INTERCEPTOR"></security:custom-filter>

2.6 配置授权页面

在配置后使用账号登录会出现没有权限403代码页面

如果用户登录进行操作时,直接报403错误用户体检并不好,我们可以制作一个比较好看的页面,告诉用户您用户不足,请联系管理员!

在web.xml中配置

代码语言:javascript
复制
<error-page>
    <error-code>403</error-code>
    <location>/failer.jsp</location>
  </error-page>

2.7 授权

在书写权限加载器时,我们发现,加载器加载url是用于http请求的过滤匹配,而实际进行权限验证使用的是权限名称,为了方便书写,我们在登录认证时查询权限信息进行授权操作

修改PermissionMapper

代码语言:javascript
复制
//根据用户id查询权限数据
    public List<Permission> selectByUid(int uid);
代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.yunhe.mapper.PermissionMapper">
    <select id="selectByUid" resultType="com.yunhe.javabean.Permission">
        select p.*
        from users u,role r,permission p,users_role ur,role_permission rp
        where u.id=ur.userId and r.id=ur.roleId and r.id=rp.roleId and p.id =rp.permissionId and u.id=#{uid}
    </select>
</mapper>

修改PermissionService

代码语言:javascript
复制
//根据uid查询权限数据
    public List<Permission> findByUid(int uid);
代码语言:javascript
复制
@Override
    public List<Permission> findByUid(int uid) {
        return permissionMapper.selectByUid(uid);
    }

修改userService

将我们之前书写写死的角色认证与权限认证查询数据库的形式添加

代码语言:javascript
复制
@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询数据返回实体类对应数据
        Users users = userMapper.selectByUserName(username);
        if (users != null) {
            //创建springSecurity主体中存储的账号对象并返回(账号,密码,权限列表)
            UserDetails userDetails = new User(users.getUsername(), users.getPassword(), getAuthority(users.getId()));
            return userDetails;
        }
        return null;
    }

    @Autowired
    PermissionService permissionService;
    //获取权限列表
    public List<GrantedAuthority> getAuthority(int uid) {
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        //查询指定用户权限列表
        List<Permission> permissionList = permissionService.findByUid(uid);
        for (Permission p:permissionList ) {
            GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(p.getPermissionName());
            grantedAuthorities.add(grantedAuthority);
        }
        return grantedAuthorities;
    }

三、AOP日志管理

3.1 数据库与表结构

日志表信息描述sysLog

序号

字段名称

字段类型

字段描述

1

id

VARCHAR(32)

主键 无意义uuid

2

vistTime

TIMESTAMP

访问时间

3

username

VARCHAR(32)

操作用户

4

ip

VARCHAR(50)

访问ip

5

url

VARCHAR(50)

访问资源url

6

executionTime

INT

执行时长

7

className

VARCHAR(50)

访问方法所在类

8

methodName

VARCHAR(50)

访问方法

9

args

VARCHAR(50)

访问方法参数列表

sql语句
代码语言:javascript
复制
CREATE TABLE `syslog` (
  `id` int(70) NOT NULL AUTO_INCREMENT,
  `visitTime` datetime DEFAULT NULL COMMENT '访问时间',
  `username` varchar(50) DEFAULT NULL COMMENT '操作者用户名',
  `ip` varchar(40) DEFAULT NULL COMMENT '访问ip',
  `url` varchar(40) DEFAULT NULL COMMENT '访问资源url',
  `executionTime` int(11) DEFAULT NULL COMMENT '执行时长',
  `className` varchar(255) DEFAULT NULL COMMENT '访问方法类名',
  `methodName` varchar(255) DEFAULT NULL COMMENT '访问方法名',
  `args` varchar(255) DEFAULT NULL COMMENT '访问方法参数',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=181 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
创建实体类
代码语言:javascript
复制
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysLog {
    private int id;
    private Date visitTime;
    private String visitTimeStr;
    private String username;
    private String ip;
    private String url;
    private Long executionTime;
    private String className;
    private String methodName;
    private String args;

    public String getVisitTimeStr() {
        // 对日期格式化
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
        if (null != visitTime) {
            visitTimeStr = dateFormat.format(visitTime);
        }
        return visitTimeStr;
    }
}

3.2 AOP日志处理

导入坐标
代码语言:javascript
复制
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>
修改applicationContext-tx.xml
代码语言:javascript
复制
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
       xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
   http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
   http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
   http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
   http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">


    <!-- 定义事务管理器 -->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!--开启事务注解-->
    <tx:annotation-driven/>

</beans>
修改springmvc-config.xml
代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/mvc
      http://www.springframework.org/schema/mvc/spring-mvc.xsd
       http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
      http://www.springframework.org/schema/security
      http://www.springframework.org/schema/security/spring-security.xsd">

    <security:global-method-security jsr250-annotations="enabled" secured-annotations="enabled"  pre-post-annotations="enabled"/>


    <!-- 视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/pages/"/>
        <property name="suffix" value=".jsp"/>
        <property name="contentType" value="text/html;charset=UTF-8"/>
    </bean>

    <!-- 开启注解驱动 -->
    <mvc:annotation-driven/>

    <!-- 开启注解扫描包-->
    <context:component-scan base-package="cn.yanqi.ssm.web" />

    <!--开启AOP注解支持,开启AspectJ 注解自动代理机制,扫描含有@Aspect的bean-->
    <aop:aspectj-autoproxy/>

</beans>
编写SysLogMapper
代码语言:javascript
复制
public interface SysLogMapper {
    @Insert("insert into syslog (visitTime,username,ip,url,executionTime,classname,methodName,args) values(#{visitTime},#{username},#{ip},#{url},#{executionTime},#{className},#{methodName},#{args})")
    public int insert(SysLog sysLog);
}
编写SysLogService
代码语言:javascript
复制
public interface SysLogService {

    public int add(SysLog syslog);

}
代码语言:javascript
复制
@Service("sysLogService")
public class SysLogServiceImpl implements SysLogService {

    @Autowired
    SysLogMapper sysLogMapper;
    @Override
    public int add(SysLog syslog) {
        return sysLogMapper.insert(syslog);
    }
}
创建切面类

在aop层创建AOP类

代码语言:javascript
复制
@Component
@Aspect
public class LogAop {
    //创建一个访问日志的切面类,交给spring管理

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private SysLogService sysLogService;

    private Date visitTime; //开始时间

    //前置通知  主要是获取开始时间,执行的类是哪一个,执行的是哪一个方法  JoinPoint程序执行过程中明确的点
    @Before("execution(* com.yunhe.controller.*.*(..))")
    public void doBefore(JoinPoint jp) throws NoSuchMethodException {
        visitTime = new Date();//当前时间就是开始访问的时间
    }

    //后置通知
    @After("execution(* com.yunhe.controller.*.*(..))")
    public void doAfter(JoinPoint jp) throws Exception {
        long time = new Date().getTime() - visitTime.getTime(); //获取访问的时长
        String className = jp.getTarget().getClass().getSimpleName(); //具体要访问的类
        String methodName = jp.getSignature().getName(); //获取访问的方法的名称
        Object[] args = jp.getArgs();
        String url = request.getRequestURI();
        String ip = request.getRemoteAddr();
        ip = ip.equals("0:0:0:0:0:0:0:1") ? "127.0.0.1" : ip;
        //获取当前操作的用户
        SecurityContext context = SecurityContextHolder.getContext();//从上下文中获了当前登录的用户
        User user = (User) context.getAuthentication().getPrincipal();
        String username = user.getUsername();

        //将日志相关信息封装到SysLog对象
        SysLog sysLog = new SysLog();
        sysLog.setExecutionTime(time); //执行时长
        sysLog.setIp(ip);
        sysLog.setClassName(className);
        sysLog.setMethodName(methodName);
        sysLog.setArgs(Arrays.toString(args));
        sysLog.setUrl(url);
        sysLog.setUsername(username);
        sysLog.setVisitTime(visitTime);

        //调用Service完成操作
        int add = sysLogService.add(sysLog);
    }
}
注意事项

1、要想在AOP类中使用request类获取用户主机的IP地址,我们需要在web.xml配置request的监听器

代码语言:javascript
复制
<listener>
   <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>

2、aop动态代理注解扫描书写在springmvc中

3、aop切面类存放在sop包中,如果书写在controoler可能出现问题

测试

3.3 查询所有日志

编写SysLogMapper
代码语言:javascript
复制
//根据用户名称与url模糊查询
    public List<SysLog> selectByUsernameAndUrl(@Param("username") String username, @Param("url")String url);
代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yunhe.mapper.SysLogMapper">

<select id="selectByUsernameAndUrl" resultType="com.yunhe.javabean.SysLog">
    select * from syslog
    <where>
        <if test="username!='' and username!=null ">
            and username like '%' #{username} '%'
        </if>

        <if test="url!='' and url!=null ">
            and url like '%' #{url} '%'
        </if>
    </where>
    order by id desc
</select>

</mapper>
编写SysLogService
代码语言:javascript
复制
public List<SysLog> findAll(String username,String url);
代码语言:javascript
复制
@Override
    public List<SysLog> findAll(String username, String url) {
        return sysLogMapper.selectByUsernameAndUrl(username,url);
    }
编写SysLogController
代码语言:javascript
复制
@Controller
@RequestMapping("/syslog")
public class SysLogController {
    @Autowired
    SysLogService sysLogService;

    @RequestMapping("/findAll")
    public String findAll(HttpServletRequest request, @RequestParam(value = "username",required = false,defaultValue = "") String username,@RequestParam(value = "url",required = false,defaultValue = "") String url,@RequestParam(value = "page",required = false,defaultValue = "1") int page, @RequestParam(value = "limit",required = false,defaultValue = "5")int limit){
        PageHelper.startPage(page,limit);
        List<SysLog> all = sysLogService.findAll(username, url);
        PageInfo<SysLog> pageInfo=new PageInfo<>(all);

        request.setAttribute("pageInfo",pageInfo);
        request.setAttribute("username",username);
        request.setAttribute("url",url);
        return "/syslog/syslog-list";
    }
}
测试
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-10-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、课程目标
  • 二、动态权限
    • 2.1 将公开权限设置为无需认证即可访问
    • 2.2 配置具体的规则
    • 2.3 设置权限数据
    • 2.4 自定义相关过滤器与决策器
      • 自定义决策管理器
      • 自定义权限加载器
      • 自定义权限拦截器
    • 2.5 将自定义权限拦截器配置入权限框架中
    • 2.6 配置授权页面
    • 2.7 授权
  • 三、AOP日志管理
    • 3.1 数据库与表结构
      • 日志表信息描述sysLog
      • sql语句
      • 创建实体类
    • 3.2 AOP日志处理
      • 导入坐标
      • 修改applicationContext-tx.xml
      • 修改springmvc-config.xml
      • 编写SysLogMapper
      • 编写SysLogService
      • 创建切面类
      • 注意事项
      • 测试
    • 3.3 查询所有日志
      • 编写SysLogMapper
      • 编写SysLogService
      • 编写SysLogController
      • 测试
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档