当前位置: 首页 > article >正文

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

一、简介

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

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

二、库表设计

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

2.1 权限表

-- 菜单表
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 角色表

-- 角色表
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 角色权限关联表

-- 角色菜单关系表 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 用户表

-- 用户表
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 用户角色关联表

-- 角色用户关系表
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 相关实体类

// 用户
@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 接口

// 用户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 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

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

@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中的认证信息,里面有用户权限信息

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

@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 ,将我们菜单对应的角色权限查询出来,实现动态授权

@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注入

@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/** 路径以下权限

@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


http://www.kler.cn/a/306021.html

相关文章:

  • 车-路-站-网”信息耦合的汽车有序充电
  • [Docker#8] 容器配置 | Mysql | Redis | C++ | 资源控制 | 命令对比
  • Oracle 11g rac 集群节点的修复过程
  • SAP_MM_SD_PP_FICO_视频课程几乎免费送
  • Java 网络编程(一)—— UDP数据报套接字编程
  • 使用@react-three/fiber,@mkkellogg/gaussian-splats-3d加载.splat,.ply,.ksplat文件
  • 多源BFS的模板以及练习题(多源BFS)
  • `character_set_server` 和 `collation_server`
  • nvm安装并配置全局缓存文件
  • 【webpack4系列】webpack初识与构建工具发展(一)
  • 【GO语言】Go语言详解与应用场景分析,与Java的对比及优缺点
  • CSP组T1怪物
  • 升级VMware
  • 视频监控摄像头国标GB28181配置参数逐条解析
  • UE5安卓项目打包安装
  • Rust 控制流
  • NarratoAI利用AI大模型,一键解说并剪辑视频
  • SQL优化(二)统计信息
  • linux手册翻译 addr2line
  • Grafana 汉化
  • 顺序栈讲解
  • C语言 | Leetcode C语言题解之第406题根据身高重建队列
  • ICPC网络赛 以及ACM训练总结
  • 计算架构模式之接口高可用
  • OpenAI发布o1,首个具备‘推理’能力的模型
  • JavaScript事件处理和常用对象