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

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_userwhere id = #{id,jdbcType=INTEGER}</select><select id="selectByusername" parameterType="java.lang.String" resultMap="BaseResultMap">select<include refid="Base_Column_List" />from t_authority_userwhere user_name = #{userName}</select><delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">delete from t_authority_userwhere 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_userset 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_rolewhere id = #{id,jdbcType=INTEGER}</select><select id="selectRoleListByUserId" parameterType="java.lang.Integer" resultMap="BaseResultMap">select a.role_name,a.role_desc fromt_authority_role aleft join t_authority_role_user b on a.id = b.r_idwhere b.u_id = #{userId}</select><delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">delete from t_authority_rolewhere 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_roleset 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_menuwhere 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_descfrom t_authority_menu aleft join t_authority_role_menu b on a.id = b.m_idleft join t_authority_role c on c.id = b.r_id</select>
</mapper>

四、自定义UserDetailsService

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

@Service
public class UserServiceDetailImpl implements UserDetailsService {@Autowiredprivate AuthorityUserDao authorityUserDao;@Autowiredprivate AuthorityRoleDao authorityRoleDao;/***  认证* @param username* @return* @throws UsernameNotFoundException*/@Overridepublic 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;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return roles.stream().map(r -> new SimpleGrantedAuthority(r.getRoleName())).collect(ArrayList::new, List::add,List::addAll);}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getPassword();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic 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@Beanpublic 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@Beanpublic 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 {@Autowiredprivate AuthorityMenuDao authorityMenuDao;private AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {// 当前obj 实际上是一个urlString 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;}@Overridepublic Collection<ConfigAttribute> getAllConfigAttributes() {return null;}@Overridepublic boolean supports(Class<?> clazz) {return FilterInvocation.class.isAssignableFrom(clazz);}
}

六、SecurityConfiguration 配置

1、设置全局的自定义数据源
2、设置权限过滤规则,将自定义的FilterInvocationSecurityMetadataSource注入

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate UrlAntPatchMetadataSource urlAntPatchMetadataSource;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService);}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 1、获取工厂对象ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);// 2、设置自定义url 匹配规则http.apply(new UrlAuthorizationConfigurer<>(applicationContext)).withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {@Overridepublic <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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 使用Python生成多种不同类型的Excel图表
  • 计算机毕业设计 毕业季一站式旅游服务定制平台的设计与实现 Java实战项目 附源码+文档+视频讲解
  • Ribbon (WPF)
  • 研1日记11
  • 302状态如何进行重定向
  • 深度估计智能化的应用
  • Prometheus优化指南:如何提升系统性能
  • windows@共享网络共享打印机@局域网内远程调用打印机打印
  • Kafka客户端核心参数详解
  • ceph简介
  • vue开发遇到的js判断问题
  • 【我的Android进阶之旅】解决CardView四个圆角有白边的问题
  • ubuntu20.04 Qt6引用dcmtk库实现dicom文件读取和字符集转换
  • 怎么把网站设置成HTTPS访问?
  • Android 应用使用theme处理全局焦点框
  • Angular 响应式表单 基础例子
  • CSS实用技巧
  • download使用浅析
  • Java 23种设计模式 之单例模式 7种实现方式
  • JavaScript DOM 10 - 滚动
  • Java程序员幽默爆笑锦集
  • open-falcon 开发笔记(一):从零开始搭建虚拟服务器和监测环境
  • Spring框架之我见(三)——IOC、AOP
  • webpack4 一点通
  • 分享一个自己写的基于canvas的原生js图片爆炸插件
  • 模型微调
  • 前端面试之闭包
  • 前端性能优化——回流与重绘
  • 实战|智能家居行业移动应用性能分析
  • 使用Tinker来调试Laravel应用程序的数据以及使用Tinker一些总结
  • 微信小程序--------语音识别(前端自己也能玩)
  • 与 ConTeXt MkIV 官方文档的接驳
  • Prometheus VS InfluxDB
  • ​ 轻量应用服务器:亚马逊云科技打造全球领先的云计算解决方案
  • ​HTTP与HTTPS:网络通信的安全卫士
  • ​queue --- 一个同步的队列类​
  • !! 2.对十份论文和报告中的关于OpenCV和Android NDK开发的总结
  • # 数据结构
  • #gStore-weekly | gStore最新版本1.0之三角形计数函数的使用
  • #Spring-boot高级
  • (2024,Vision-LSTM,ViL,xLSTM,ViT,ViM,双向扫描)xLSTM 作为通用视觉骨干
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (附源码)计算机毕业设计SSM疫情下的学生出入管理系统
  • (解决办法)ASP.NET导出Excel,打开时提示“您尝试打开文件'XXX.xls'的格式与文件扩展名指定文件不一致
  • (力扣题库)跳跃游戏II(c++)
  • (原創) 如何將struct塞進vector? (C/C++) (STL)
  • (转)ABI是什么
  • ***测试-HTTP方法
  • .gitignore
  • .net core webapi 部署iis_一键部署VS插件:让.NET开发者更幸福
  • .NET Core 项目指定SDK版本
  • .NET 设计模式—适配器模式(Adapter Pattern)
  • .net安装_还在用第三方安装.NET?Win10自带.NET3.5安装
  • .NET精简框架的“无法找到资源程序集”异常释疑
  • .NET序列化 serializable,反序列化