Spring security 动态权限管理(基于数据库)

一、简介

如果对该篇文章不了解,请移步上一篇文章:spring security 中的授权使用-CSDN博客

当我们配置的 URL 拦截规则请求 URL 所需要的权限都是通过代码来配置的,这样就比较死板,如果想要调整访问某一个 URL 所需要的权限,就需要修改代码。动态管理权限规则就是我们将UR 拦截规则和访问 URI 所需要的权限都保存在数据库中,这样,在不修改源代码的情况下,只需要修改数据库中的数据,就可以对权限进行调整。

二、库表设计

里面涉及到 用户 ,角色 ,权限 ,用户角色关系表,角色菜单表共计五张表,用户是用来认证使用的;其中是一些建表语句,如果角色复杂可以将用户这会用内存实现;

2.1 权限表

sql 复制代码
-- 菜单表
CREATE TABLE `t_authority_menu`(
  id int(11) NOT NULL AUTO_INCREMENT primary key ,
  pattern_url varchar(128) DEFAULT ''
 )ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- 添加数据
BEGIN;
  insert into t_authority_menu values (1,'/admin/**');
  insert into t_authority_menu values (2,'/user/**');
  insert into t_authority_menu values (3,'/guest/**');

commit ;

2.2 角色表

sql 复制代码
-- 角色表
CREATE TABLE `t_authority_role`(
  id int(11) NOT NULL AUTO_INCREMENT primary key ,
  role_name varchar(128) DEFAULT '' comment '角色标识',
  role_desc varchar(128) DEFAULT '' comment '角色描述'
)ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- 添加数据
BEGIN;
insert into t_authority_role values (1,'ROLE_ADMIN','系统管理员');
insert into t_authority_role values (2,'ROLE_USER','普通用户');
insert into t_authority_role values (3,'ROLE_GUEST','游客');
commit ;

2.3 角色权限关联表

sql 复制代码
-- 角色菜单关系表 primary key (m_id,r_id)
CREATE TABLE `t_authority_role_menu`(
  id int(11) NOT NULL AUTO_INCREMENT primary key,
  m_id int(11) DEFAULT NULL comment '菜单id',
  r_id int(11) DEFAULT NULL comment '角色id'
)ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 创建唯一索引
CREATE UNIQUE INDEX i_authority_role_menu1 ON t_authority_role_menu(m_id, r_id);

-- 添加数据
BEGIN;
insert into t_authority_role_menu(m_id, r_id) values (1,1);
insert into t_authority_role_menu(m_id, r_id) values (2,1);
insert into t_authority_role_menu(m_id, r_id) values (2,2);
insert into t_authority_role_menu(m_id, r_id) values (3,3);
insert into t_authority_role_menu(m_id, r_id) values (3,2);
commit ;

2.4 用户表

sql 复制代码
-- 用户表
CREATE TABLE t_authority_user (
  id int(11)  NOT NULL AUTO_INCREMENT primary key,
  user_name   varchar(32) DEFAULT '' comment '用户名',
  password    varchar(32) DEFAULT '' comment '密码',
  enabled     tinyint(1) DEFAULT 1 comment '是否启用  1启用',
  locked      tinyint(1) DEFAULT 1 comment '是否锁定  1 未锁定'
 ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

BEGIN;
insert into t_authority_user values (1,'admin','{noop}123456','1','1');
insert into t_authority_user values (2,'user','{noop}123456','1','1');
insert into t_authority_user values (3,'guest','{noop}123456','1','1');
commit ;

2.5 用户角色关联表

sql 复制代码
-- 角色用户关系表
CREATE TABLE `t_authority_role_user`(
  id int(11) NOT NULL AUTO_INCREMENT primary key ,
  u_id int(11) DEFAULT 0 comment '用户id',
  r_id int(11) DEFAULT 0 comment '角色id'
)ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- 创建唯一索引
CREATE UNIQUE INDEX i_authority_role_user1 ON t_authority_role_user(u_id, r_id);

-- 添加数据
BEGIN;
insert into t_authority_role_user(u_id, r_id) values (1,1);
insert into t_authority_role_user(u_id, r_id) values (1,2);
insert into t_authority_role_user(u_id, r_id) values (2,2);
insert into t_authority_role_user(u_id, r_id) values (3,3);
commit ;

三、用户角色权限相关

我们设计 用户 《=》角色 《=》 权限,一个用户拥有多个角色,一个角色对应多个权限

3.1 相关实体类

java 复制代码
// 用户
@Data
public class AuthorityUser implements Serializable {
    private Integer id;

    /**
     * 用户名
     */
    private String userName;

    /**
     * 密码
     */
    private String password;

    /**
     * 是否启用  1启用
     */
    private Boolean enabled;

    /**
     * 是否锁定  1 未锁定
     */
    private Boolean locked;

    private static final long serialVersionUID = 1L;
}


// 角色
@Data
public class AuthorityRole implements Serializable {
    private Integer id;

    /**
     * 角色标识
     */
    private String roleName;

    /**
     * 角色描述
     */
    private String roleDesc;

    private static final long serialVersionUID = 1L;
}

// 菜单
@Data
public class AuthorityMenu implements Serializable {
    private Integer id;

    private String patternUrl;

    /**
     *  一个菜单对应多个角色
     */
    private List<AuthorityRole> roles;

    private static final long serialVersionUID = 1L;
}

3.2 相关dao 接口

java 复制代码
// 用户dao
public interface AuthorityUserDao {


    /**
     *  根据用户名获取用户信息  认证使用
     * @param id
     * @return
     */
    AuthorityUser selectByusername(String id);


    int deleteByPrimaryKey(Integer id);

    int insert(AuthorityUser record);

    int insertSelective(AuthorityUser record);

    AuthorityUser selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(AuthorityUser record);

    int updateByPrimaryKey(AuthorityUser record);
}

// 角色
public interface AuthorityRoleDao {

    /**
     *  根据用户编码获取角色列表
     * @param userId
     * @return
     */
    List<AuthorityRole> selectRoleListByUserId(int userId);

    int deleteByPrimaryKey(Integer id);

    int insert(AuthorityRole record);

    int insertSelective(AuthorityRole record);

    AuthorityRole selectByPrimaryKey(Integer id);

    int updateByPrimaryKeySelective(AuthorityRole record);

    int updateByPrimaryKey(AuthorityRole record);
}
// 权限
public interface AuthorityMenuDao {

    AuthorityMenu selectByPrimaryKey(Integer id);

    /**
     *  获取所有的菜单权限
     * @return
     */
    List<AuthorityMenu> getAllMenu();

}

3.3 dao对应的xml mybatis

XML 复制代码
// 用户
<?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.fashion.mapper.mysql.AuthorityUserDao">
  <resultMap id="BaseResultMap" type="com.fashion.model.AuthorityUser">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="user_name" jdbcType="VARCHAR" property="userName" />
    <result column="password" jdbcType="VARCHAR" property="password" />
    <result column="enabled" jdbcType="BOOLEAN" property="enabled" />
    <result column="locked" jdbcType="BOOLEAN" property="locked" />
  </resultMap>
  <sql id="Base_Column_List">
    id, user_name, `password`, enabled, locked
  </sql>
  <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select 
    <include refid="Base_Column_List" />
    from t_authority_user
    where id = #{id,jdbcType=INTEGER}
  </select>

  <select id="selectByusername" parameterType="java.lang.String" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from t_authority_user
    where user_name = #{userName}
  </select>


  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
    delete from t_authority_user
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.fashion.model.AuthorityUser" useGeneratedKeys="true">
    insert into t_authority_user (user_name, `password`, enabled, 
      locked)
    values (#{userName,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR}, #{enabled,jdbcType=BOOLEAN}, 
      #{locked,jdbcType=BOOLEAN})
  </insert>
  <insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.fashion.model.AuthorityUser" useGeneratedKeys="true">
    insert into t_authority_user
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="userName != null">
        user_name,
      </if>
      <if test="password != null">
        `password`,
      </if>
      <if test="enabled != null">
        enabled,
      </if>
      <if test="locked != null">
        locked,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="userName != null">
        #{userName,jdbcType=VARCHAR},
      </if>
      <if test="password != null">
        #{password,jdbcType=VARCHAR},
      </if>
      <if test="enabled != null">
        #{enabled,jdbcType=BOOLEAN},
      </if>
      <if test="locked != null">
        #{locked,jdbcType=BOOLEAN},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.fashion.model.AuthorityUser">
    update t_authority_user
    <set>
      <if test="userName != null">
        user_name = #{userName,jdbcType=VARCHAR},
      </if>
      <if test="password != null">
        `password` = #{password,jdbcType=VARCHAR},
      </if>
      <if test="enabled != null">
        enabled = #{enabled,jdbcType=BOOLEAN},
      </if>
      <if test="locked != null">
        locked = #{locked,jdbcType=BOOLEAN},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.fashion.model.AuthorityUser">
    update t_authority_user
    set user_name = #{userName,jdbcType=VARCHAR},
      `password` = #{password,jdbcType=VARCHAR},
      enabled = #{enabled,jdbcType=BOOLEAN},
      locked = #{locked,jdbcType=BOOLEAN}
    where id = #{id,jdbcType=INTEGER}
  </update>
</mapper>


// 角色
<?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.fashion.mapper.mysql.AuthorityRoleDao">
  <resultMap id="BaseResultMap" type="com.fashion.model.AuthorityRole">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="role_name" jdbcType="VARCHAR" property="roleName" />
    <result column="role_desc" jdbcType="VARCHAR" property="roleDesc" />
  </resultMap>
  <sql id="Base_Column_List">
    id, role_name, role_desc
  </sql>
  <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select 
    <include refid="Base_Column_List" />
    from t_authority_role
    where id = #{id,jdbcType=INTEGER}
  </select>

  <select id="selectRoleListByUserId" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select a.role_name,a.role_desc from
     t_authority_role a
     left join t_authority_role_user b on a.id = b.r_id
      where b.u_id = #{userId}
  </select>

  <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
    delete from t_authority_role
    where id = #{id,jdbcType=INTEGER}
  </delete>
  <insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.fashion.model.AuthorityRole" useGeneratedKeys="true">
    insert into t_authority_role (role_name, role_desc)
    values (#{roleName,jdbcType=VARCHAR}, #{roleDesc,jdbcType=VARCHAR})
  </insert>
  <insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.fashion.model.AuthorityRole" useGeneratedKeys="true">
    insert into t_authority_role
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="roleName != null">
        role_name,
      </if>
      <if test="roleDesc != null">
        role_desc,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="roleName != null">
        #{roleName,jdbcType=VARCHAR},
      </if>
      <if test="roleDesc != null">
        #{roleDesc,jdbcType=VARCHAR},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.fashion.model.AuthorityRole">
    update t_authority_role
    <set>
      <if test="roleName != null">
        role_name = #{roleName,jdbcType=VARCHAR},
      </if>
      <if test="roleDesc != null">
        role_desc = #{roleDesc,jdbcType=VARCHAR},
      </if>
    </set>
    where id = #{id,jdbcType=INTEGER}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.fashion.model.AuthorityRole">
    update t_authority_role
    set role_name = #{roleName,jdbcType=VARCHAR},
      role_desc = #{roleDesc,jdbcType=VARCHAR}
    where id = #{id,jdbcType=INTEGER}
  </update>
</mapper>

// 菜单
<?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.fashion.mapper.mysql.AuthorityMenuDao">
  <resultMap id="BaseResultMap" type="com.fashion.model.AuthorityMenu">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="pattern_url" jdbcType="VARCHAR" property="patternUrl" />
  </resultMap>
  <sql id="Base_Column_List">
    id, pattern_url
  </sql>


  <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select 
    <include refid="Base_Column_List" />
    from t_authority_menu
    where id = #{id,jdbcType=INTEGER}
  </select>


  <resultMap id="MenuResultMap" type="com.fashion.model.AuthorityMenu">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="pattern_url" jdbcType="VARCHAR" property="patternUrl" />
    <collection property="roles" ofType="com.fashion.model.AuthorityRole">
      <id column="rId" property="id"/>
      <result column="role_name" jdbcType="VARCHAR" property="roleName" />
      <result column="role_desc" jdbcType="VARCHAR" property="roleDesc" />
    </collection>
 </resultMap>


  <select id="getAllMenu"  resultMap="MenuResultMap">
   select a.*,c.id rId,c.role_name,c.role_desc
    from t_authority_menu a
    left join t_authority_role_menu b on a.id = b.m_id
    left join t_authority_role c on c.id = b.r_id
  </select>
</mapper>

四、自定义UserDetailsService

用来实现自定义登录逻辑,查询数据库用户,如果不懂请看前面系列教程

java 复制代码
@Service
public class UserServiceDetailImpl implements UserDetailsService {


    @Autowired
    private AuthorityUserDao authorityUserDao;

    @Autowired
    private AuthorityRoleDao authorityRoleDao;



    /**
     *  认证
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1、 根据用户名获取信息
        AuthorityUser authorityUser = authorityUserDao.selectByusername(username);

        if (null == authorityUser) {
            throw new UsernameNotFoundException("用户不存在!");
        }

        // 2、获取用户对应的角色
        List<AuthorityRole> roles = authorityRoleDao.selectRoleListByUserId(authorityUser.getId());

        UserDetailInf userDetailInf = new UserDetailInf(authorityUser, roles);

        return userDetailInf;
    }
}

4.1 UserDetails 自定义

用来保存用户登录成功后,SpringSecurityHolder中的认证信息,里面有用户权限信息

java 复制代码
public class UserDetailInf implements UserDetails {

    private AuthorityUser user;

    private List<AuthorityRole> roles;

    public UserDetailInf(AuthorityUser user, List<AuthorityRole> roles) {
        this.user = user;
        this.roles = roles;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream()
                .map(r -> new SimpleGrantedAuthority(r.getRoleName()))
                .collect(ArrayList::new, List::add,List::addAll);
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getPassword();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

4.2 自定义数据源扫码mapper

java 复制代码
@Configuration
@MapperScan(basePackages = MysqlDsConfiguration.PACKAGE, sqlSessionFactoryRef = "mysqlSqlSessionFactory" )
@Slf4j
public class MysqlDsConfiguration {


    static final String PACKAGE = "com.fashion.mapper.mysql";
    static final String MAPPER_LOCATION = "classpath:mybatis/mapper/mysql/*.xml";


    /**
     *  配置数据源
     * @return
     */
    @Primary
    @Bean
    public DataSource mysqlDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUsername("root");
        dataSource.setPassword("12345");
        return dataSource;
    }

    @Primary
    @Bean
    public SqlSessionFactory mysqlSqlSessionFactory(@Autowired DataSource mysqlDataSource){
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        try {
            sessionFactory.setDataSource(mysqlDataSource);
            sessionFactory.setConfigLocation(new ClassPathResource("/mybatis/mybatis-config.xml"));
            sessionFactory.setMapperLocations(
                    new PathMatchingResourcePatternResolver().getResources(MysqlDsConfiguration.MAPPER_LOCATION));
            return sessionFactory.getObject();
        } catch (Exception e) {
            log.error("mysql数据源初始化失败",e);
        }
        return null;
    }

}

五、定义 MetadataSource 权限信息

实现 FilterInvocationSecurity ,用来将权限对应的角色信息加载进去,该类用于我们重写getAttributes ,将我们菜单对应的角色权限查询出来,实现动态授权

java 复制代码
@Component
public class UrlAntPatchMetadataSource implements FilterInvocationSecurityMetadataSource {


    @Autowired
    private AuthorityMenuDao authorityMenuDao;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();


    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        // 当前obj 实际上是一个url
        String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
        // 获取所有的url 对应的角色集合
        List<AuthorityMenu> allAuthorityMenus = authorityMenuDao.getAllMenu();

        String[] roles = allAuthorityMenus.stream()
                .filter(menu -> antPathMatcher.match(menu.getPatternUrl(),requestURI))
                .flatMap(authorityMenu -> authorityMenu.getRoles().stream())
                .map(AuthorityRole::getRoleName)
                .toArray(String[]::new);

        if (null != roles && roles.length > 0) {
            return SecurityConfig.createList(roles);
        }

//        for (AuthorityMenu menu : allAuthorityMenus) {
//            if (antPathMatcher.match(menu.getPatternUrl(),requestURI)) {
//                String[] roles = menu.getRoles().stream().map(r -> r.getRoleName()).toArray(String[]::new);
//                return SecurityConfig.createList(roles);
//            }
//        }
        return null;
    }

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

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

六、SecurityConfiguration 配置

1、设置全局的自定义数据源

2、设置权限过滤规则,将自定义的FilterInvocationSecurityMetadataSource注入

java 复制代码
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {


    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private UrlAntPatchMetadataSource urlAntPatchMetadataSource;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 1、获取工厂对象
        ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
        // 2、设置自定义url 匹配规则
        http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
             .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                 @Override
                 public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                     object.setSecurityMetadataSource(urlAntPatchMetadataSource);
                     // 如果没有权限是否拒绝
                     object.setRejectPublicInvocations(true);

                     return object;
                 }
             });

        http.authorizeRequests()
                .anyRequest().authenticated()
                .and().formLogin()//开启表单登录
                .and().csrf().disable();
    }
}

七、测试controller

用户对应的角色分析

admin 用户拥有 ROLE_ADMIN/ROLE_USER 的角色

user 用户拥有 ROLE_USER 的角色

guest 用户拥有 ROLE_GUEST 的角色

角色对应的权限分析

ROLE_ADMIN角色拥有 /admin/** 以及 /user/** 以下权限

ROLE_USER 角色拥有 /user/** 以及/guest/** 以下权限

ROLE_GUEST用户拥有 /guest/** 路径以下权限

java 复制代码
@RestController
public class HelloController {


    /**
     *
     * @return
     */
    @RequestMapping("/admin/hello")
    public String admin() {
        return "hello admin";
    }


    @RequestMapping("/user/hello")
    public String user() {
        return "hello user";
    }

    @RequestMapping("/guest/hello")
    public String guest() {
        return "hello guest";
    }

    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }

}

7.1 admin 登录测试效果

使用 admin 用户登录,我们访问接口测试权限,经分析,admin 拥有下面所有的权限,对应controller中的三个方法是都可以访问的;

7.2 user登录测试效果

使用 user用户登录,分析得到;user用户只能访问 /user/hello 或者 /guest/hello 接口

7.3 guest 用户登录测试效果

使用 guest用户登录,分析得到;guest用户只能访问 /guest/hello 接口

八、问题总结

一、AntPathMatcher中的mather 方法,里面千万不能写反了,第一个是我们通配库里面定义的,第二个参数为请求的url,两个的顺序不能对调;

antPathMatcher.match(menu.getPatternUrl(),requestURI)

二、我们每一次授权都需要走一次数据库,性能问题;

解决方案:

1、设置一个hutool中的timecache 定时清除里面对应权限信息,设置10分钟,这样我们10分钟才跟数据源有一次交互;问题就是集群,可能每一台里面都需要放一次,优点是效率更高

2、我们将权限信息存到redis中,这种方案更好,如果是集群也不影响;缺点就是需要一个redis的依赖;

源码跟文档我都上传了,有需求的小伙伴自行下载,下载链接:

https://download.csdn.net/download/qq_36260963/89733771

相关推荐
isNotNullX3 分钟前
数据处理的流程是什么?如何进行数据预处理?
数据库·数据资产管理·数据处理·企业数字化
TAEHENGV6 分钟前
基本设置模块 Cordova 与 OpenHarmony 混合开发实战
android·java·数据库
Leo18710 分钟前
MySQL 回表(Back to Table)详解
数据库·mysql
遇见火星14 分钟前
MySQL 8.0复制架构主从自动切换脚本
mysql·adb·架构·mysql8.0·mysql主从
不知江月待何人..14 分钟前
MySQL服务无法启动问题
数据库·mysql
廋到被风吹走20 分钟前
【数据库】【Oracle】SQL基础
数据库·sql·oracle
milanleon25 分钟前
使用Spring Security进行登录认证
java·前端·spring
曹牧40 分钟前
Oracle统计每日发生次数
数据库·oracle
范纹杉想快点毕业1 小时前
返璞归真还是拥抱现代?——嵌入式研发中的“裸机开发”与RTOS全景解析
c语言·数据库·mongodb·设计模式·nosql
辗转.9531 小时前
MongoDB入门指南:从零开始掌握NoSQL数据库
数据库·mongodb·nosql