一、简介
假如对该篇文章不了解,请移步上一篇文章: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
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |