当前位置:实例文章 » 其他实例» [文章]No5.由内存存储改为数据库存储和redis存储(主要是授权服务端的用户信息、客户端信息、客户授权信息;资源端是没有存储的,每次解析token拿到用户信息);

No5.由内存存储改为数据库存储和redis存储(主要是授权服务端的用户信息、客户端信息、客户授权信息;资源端是没有存储的,每次解析token拿到用户信息);

发布人:清晨敲代码 发布时间:2022-12-09 10:07 阅读次数:5

代码地址与接口看总目录:【学习笔记】记录冷冷-pig项目的学习过程,大概包括Authorization Server、springcloud、Mybatis Plus~~~_清晨敲代码的博客-CSDN博客

前几篇的写的授权端和资源端中,里面的用户登录信息、客户端信息、客户授权信息的存储一直是使用的Spring提供的基于内存的存储,也就是,这种存储方式是不会持久化信息的,是每次重启系统都会重新加载我们写好的信息。

现在将用户登录信息、客户端信息的存储改为持久化的MySQL数据库,将客户授权信息的存储修改为Redis。

我们使用mybatisplus操作mysql数据库,使用springboot的redis来操作Redis。

目录

A1.修改用户登录信息的持久化为MySQL

B1.依赖包

B2.核心类

C1.SysUser

C2.SysUserMapper

C3.SysUserServiceImpl

C4.PigUserDetailsServiceImpl

B3.配置类

C1.application.xml

C2.AuthorizationServerConfiguration

C3.WebSecurityConfiguration

A2.修改客户信息的持久化为MySQL

B1.依赖包

B2.核心类

C1.SysOauthClientDetails

C2.SysOauthClientDetailsMapper

C3.SysOauthClientDetailsServiceImpl

C4.PigRegisteredClientRepository

B3.配置类

C1.RegisteredClientConfiguration

A3.修改客户授权信息的持久化为Redis

B1.依赖包

B2.核心类

C1.PigRedisOAuth2AuthorizationServiceImpl

B3.配置类

C1.RedisTemplateConfiguration

C2.RegisteredClientConfiguration

C3.resources/META-INF/spring.factories(pig-common-core和pig-auth)

A4.给用户登录信息和客户信息添加cache缓存

B1.用户登录信息添加cache缓存

C1.PigUserDetailsServiceImpl

B2.客户信息添加cache缓存

C1.PigRegisteredClientRepository


A1.修改用户登录信息的持久化为MySQL

思考一下,这个用户登录信息持久化,只有pigDaoAuthenticationProvider类会涉及到,他需要调用this.userDetailsService.loadUserByUsername()方法来获取用户信息,userDetailsService现在是用的基于内存的InMemoryUserDetailsManager。

我们只需要自定义一个实现userDetailsService接口的类,让他从数据库中拿到数据就可以了!

B1.依赖包

?由于不想直接在auth模块添加数据库相关的,就创建了一个loginUser模块,我看PIg里面是通过微服务调用的umps模块,我们这里就不那样做的,直接给auth依赖loginUser模块调用吧

其中要想使用mybatisplus调用mysql,需要导入下面两个包(mybatis-plus-boot-starter就依赖了jdbc)

        <!--orm 相关-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <!--连接数据库相关-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

B2.核心类

C1.SysUser

和平时添加的表关联实体一样,只是对于主键需要加个注解,这样方便使用mybatisplus提供的通过主键操作的方法。

对于删除标识可以选择添加@TableLogic注解,添加之后会变成逻辑删除,只修改状态而不是真正的删除,并且使用自带的查询是,会自定查询未删除的。


@Data
@EqualsAndHashCode(callSuper = true)
public class SysUser extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /**
     * 主键ID
     */
    @TableId(value = "user_id", type = IdType.ASSIGN_ID)
    private Long userId;

。。。

    /**
     * 0-正常,1-删除
     */
    //表示逻辑删除注解
    @TableLogic(value="0",delval="1")
    private String delFlag;

}

C2.SysUserMapper

mapper接口继承 BaseMapper 类后,就无需编写 mapper.xml 文件,可以直接使用定义好的CRUD功能。

对于注册获取用户来说,只需要获取用户信息就可以,就不用再加mapper.xml文件了

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {

}

C3.SysUserServiceImpl

业务接口和业务实现类需要分别继承?IService 、ServiceImpl,这样就可以使用mybatisplus提供的操作方法。

我们目前只需要业务类的 getOne(Wrapper)方法即可。


public interface SysUserService  extends IService<SysUser> {


}


@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

}

C4.PigUserDetailsServiceImpl

这个类是专门处理用户登录时,获取用户信息的,需要实现 UserDetailsService 接口。

在这个类里面使用SysUserServiceImpl拿到用户信息~

这里需要注意,拿到持久化的用户信息后,需要给密码加一个密码器类型:{bcrypt},因为我们在PigDaoAuthenticationProvider里面用的是 DelegatingPasswordEncoder 类型的密码管理器,他需要根据头部密码标识来判断用哪个密码管理器解码。(同时注意,这里加的密码标识和保存密码时的密码标识需要一致哦~)

@Service
@RequiredArgsConstructor
public class PigUserDetailsServiceImpl implements UserDetailsService {

    private final SysUserService sysUserService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getUsername,username));

        //这里需要给密码加入密码器类型:{bcrypt},因为我们用的是 DelegatingPasswordEncoder ,需要根据头部判断用哪个密码管理器
        PigUser userDetails = new PigUser(sysUser.getUsername(), SecurityConstants.BCRYPT + sysUser.getPassword(),new HashSet(Collections.singleton(new SimpleGrantedAuthority("write"))));

        return userDetails;
    }
}

B3.配置类

C1.application.xml

# freemark配置
spring:

  # 缓存类型使用redis
  cache:
    type: redis
  redis:
    host: pig-redis
    port: 6379

  # 数据源
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: username
    password: password
    url: jdbc:mysql://pig-mysql:3306/pig?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true



C2.AuthorizationServerConfiguration

注入一个UserDetailsService类型的bean,这里就会拿到我们自定义的PigUserDetailsServiceImpl类型的对象。

然后将原来的userDetailsService()方法创建Bean去掉,并且修改PigDaoAuthenticationProvider 对象的userDetailsService对象。

/**
 * 认证服务器配置
 */
@RequiredArgsConstructor
@Configuration
public class AuthorizationServerConfiguration {

    private final UserDetailsService userDetailsService;

    /**
     * @Description: 注入授权模式实现提供方
     * 1. 密码模式 </br>
     * 2. 短信登录 (暂无)</br>
     * @param http
     * @Return: void
     */
    public void addCustomOAuth2GrantAuthenticationProvider(HttpSecurity http){
        。。。
        //new一个自定义用户认证提供方,(类似于DaoAuthenticationProvider)需要注入用户管理业务userDetailsService,当前用的是InMemoryUserDetailsManager,生产模式最好不要使用
        PigDaoAuthenticationProvider pigDaoAuthenticationProvider = new PigDaoAuthenticationProvider();
        pigDaoAuthenticationProvider.setUserDetailsService(userDetailsService);
        。。。
    }

//    @Bean
//    UserDetailsService userDetailsService(){
//
//        UserDetails userDetails = User.builder()
//                .username("qc")
//                .password("123")
//                .passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()::encode)
//                .roles("read","write")
//                .build();
//        //new一个用户管理业务,注入一盒用户信息
//        return new InMemoryUserDetailsManager(userDetails);
//    }

}

C3.WebSecurityConfiguration

注入一个UserDetailsService类型的bean,这里就会拿到我们自定义的PigUserDetailsServiceImpl类型的对象。

修改PigDaoAuthenticationProvider 对象的userDetailsService对象。

@RequiredArgsConstructor
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration {

    private final UserDetailsService userDetailsService;

    /**
     * @Description: spring security 默认的安全策略
     * @param http
     * @Return: org.springframework.security.web.SecurityFilterChain
     */
    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        。。。
        PigDaoAuthenticationProvider pigDaoAuthenticationProvider = new PigDaoAuthenticationProvider();
        pigDaoAuthenticationProvider.setUserDetailsService(userDetailsService);
        。。。
    }

}

A2.修改客户信息的持久化为MySQL

思考一下,这个客户端信息(注意和客户端认证信息不一样哦)持久化,只有RegisteredClientRepository类会涉及到,现在是用的基于内存的InMemoryRegisteredClientRepository。

我们只需要自定义一个实现RegisteredClientRepository接口的类,让他从数据库中拿到数据就可以了!

B1.依赖包

和A1.一样的依赖包

B2.核心类

C1.SysOauthClientDetails

@Data
@EqualsAndHashCode(callSuper = true)
public class SysOauthClientDetails extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /**
     * 客户端ID
     */
    @TableId(value = "client_id", type = IdType.INPUT)
    private String clientId;

    /**
     * 客户端密钥
     */
    private String clientSecret;

    /**
     * 作用域
     */
    private String scope;

    /**
     * 授权方式(A,B,C)
     */
    private String authorizedGrantTypes;

    /**
     * 回调地址
     */
    private String webServerRedirectUri;

    /**
     * 权限
     */
    private String authorities;

    /**
     * 请求令牌有效时间
     */
    private Integer accessTokenValidity;

    /**
     * 刷新令牌有效时间
     */
    private Integer refreshTokenValidity;

    /**
     * 扩展信息
     */
    private String additionalInformation;

    /**
     * 是否自动放行,也就是是否打开授权确认页面
     */
    private String autoapprove;
}

C2.SysOauthClientDetailsMapper

@Mapper
public interface SysOauthClientDetailsMapper extends BaseMapper<SysOauthClientDetails> {
}

C3.SysOauthClientDetailsServiceImpl


public interface SysOauthClientDetailsService extends IService<SysOauthClientDetails> {
}


@Service
public class SysOauthClientDetailsServiceImpl extends ServiceImpl<SysOauthClientDetailsMapper, SysOauthClientDetails>
        implements SysOauthClientDetailsService {
}

C4.PigRegisteredClientRepository

先从数据库中拿到客户信息,然后构建成RegisteredClient对象并返回。

@Service
@RequiredArgsConstructor
public class PigRegisteredClientRepository implements RegisteredClientRepository {
    /**
     * 刷新令牌有效期默认 30 天
     */
    private final static int refreshTokenValiditySeconds = 60 * 60 * 24 * 30;

    /**
     * 请求令牌有效期默认 12 小时
     */
    private final static int accessTokenValiditySeconds = 60 * 60 * 12;


    private final SysOauthClientDetailsService sysOauthClientDetailsService;

    @Override
    public void save(RegisteredClient registeredClient) {
        throw new UnsupportedOperationException();
    }

    @Override
    public RegisteredClient findById(String s) {
        throw new UnsupportedOperationException();
    }

    @Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null")
    @Override
    public RegisteredClient findByClientId(String clientId) {

        SysOauthClientDetails clientDetails = sysOauthClientDetailsService.getOne(Wrappers.<SysOauthClientDetails>query().lambda().eq(SysOauthClientDetails::getClientId,clientId));

        RegisteredClient.Builder builder = RegisteredClient.withId(clientDetails.getClientId())
                .clientId(clientDetails.getClientId())
                .clientSecret(SecurityConstants.NOOP + clientDetails.getClientSecret())
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);

        // 授权模式
        Optional.ofNullable(clientDetails.getAuthorizedGrantTypes())
                .ifPresent(grants -> StringUtils.commaDelimitedListToSet(grants).forEach(s -> builder.authorizationGrantType(new AuthorizationGrantType(s))));

        // 回调地址
        Optional.ofNullable(clientDetails.getWebServerRedirectUri())
                .ifPresent(redirectUri -> StringUtils.commaDelimitedListToSet(redirectUri).forEach(r -> builder.redirectUri(r)));

        // scope 分割多个scope值
        Optional.ofNullable(clientDetails.getScope())
                .ifPresent(scopes -> StringUtils.commaDelimitedListToSet(scopes).forEach(s -> builder.scope(s)));

        return builder
                .tokenSettings(TokenSettings.builder()
                        .accessTokenFormat(OAuth2TokenFormat.REFERENCE)
                        .accessTokenTimeToLive(Duration.ofSeconds(Optional.ofNullable(clientDetails.getAccessTokenValidity()).orElse(accessTokenValiditySeconds)))
                        .refreshTokenTimeToLive(Duration.ofSeconds(Optional.ofNullable(clientDetails.getRefreshTokenValidity()).orElse(refreshTokenValiditySeconds)))
                        .build())
                .clientSettings(ClientSettings.builder()
                        .requireAuthorizationConsent(!BooleanUtil.toBoolean(clientDetails.getAutoapprove())).build())
                .build();
    }
}

B3.配置类

C1.RegisteredClientConfiguration

这个类里面配置客户信息的全部去掉,不再需要了。

@RequiredArgsConstructor
@Configuration
public class RegisteredClientConfiguration {
。。。
//    public RegisteredClientRepository registeredClientRepository() {。。。
//    }

//    private RegisteredClient createRegisteredClient_umps(final String id) {。。。


//    private RegisteredClient createRegisteredClient_client(final String id) {。。。
//    }
}

A3.修改客户授权信息的持久化为Redis

我们直接将redis配置添加到common-core里面。

涉及到客户授权信息的持久化操作是OAuth2AuthorizationService类,我们也实现这个类,然后在他里面调用RedisTemplate操作redis即可。

B1.依赖包

        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

B2.核心类

C1.PigRedisOAuth2AuthorizationServiceImpl

代码很好理解,主要是三个方法,一个save一个remove一个findbytoken。

注意findbytoken方法有一个参数是OAuth2TokenType,就是可以根据token和token的类型(code、access_token、refresh_token)来查询,那么我们保存时也要根据这个token类型保存,以防止查询错误。

进入save方法后根据OAuth2Authorization 里面的token类型来进行保存。只要OAuth2Authorization 携带的信息符合条件就会保存~

@RequiredArgsConstructor
public class PigRedisOAuth2AuthorizationServiceImpl  implements OAuth2AuthorizationService {
    private final static Long TIMEOUT = 10L;

    private static final String AUTHORIZATION = "token";

    private final RedisTemplate<String, Object> redisTemplate;

    @Override
    public void save(OAuth2Authorization authorization) {
        Assert.notNull(authorization, "authorization cannot be null");

        if (isState(authorization)) {
            String token = authorization.getAttribute("state");
            redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.STATE, token), authorization, TIMEOUT,
                    TimeUnit.MINUTES);
        }

        if (isCode(authorization)) {
            OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
                    .getToken(OAuth2AuthorizationCode.class);
            OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
            long between = ChronoUnit.MINUTES.between(authorizationCodeToken.getIssuedAt(),
                    authorizationCodeToken.getExpiresAt());
            redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()),
                    authorization, between, TimeUnit.MINUTES);
        }

        if (isRefreshToken(authorization)) {
            OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
            long between = ChronoUnit.SECONDS.between(refreshToken.getIssuedAt(), refreshToken.getExpiresAt());
            redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()),
                    authorization, between, TimeUnit.SECONDS);
        }


        if (isAccessToken(authorization)) {
            OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
            long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
            redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()),
                    authorization, between, TimeUnit.SECONDS);
        }
    }

    @Override
    public void remove(OAuth2Authorization authorization) {
        Assert.notNull(authorization, "authorization cannot be null");

        List<String> keys = new ArrayList<>();
        if (isState(authorization)) {
            String token = authorization.getAttribute("state");
            keys.add(buildKey(OAuth2ParameterNames.STATE, token));
        }

        if (isCode(authorization)) {
            OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
                    .getToken(OAuth2AuthorizationCode.class);
            OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
            keys.add(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()));
        }

        if (isRefreshToken(authorization)) {
            OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
            keys.add(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()));
        }

        if (isAccessToken(authorization)) {
            OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
            keys.add(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()));
        }
        redisTemplate.delete(keys);
    }

    @Override
    @Nullable
    public OAuth2Authorization findById(String id) {
        throw new UnsupportedOperationException();
    }

    @Override
    @Nullable
    public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
        Assert.hasText(token, "token cannot be empty");
        Assert.notNull(tokenType, "tokenType cannot be empty");
        return (OAuth2Authorization) redisTemplate.opsForValue().get(buildKey(tokenType.getValue(), token));
    }

    private String buildKey(String type, String id) {
        return String.format("%s::%s::%s", AUTHORIZATION, type, id);
    }

    private static boolean isState(OAuth2Authorization authorization) {
        return Objects.nonNull(authorization.getAttribute("state"));
    }

    private static boolean isCode(OAuth2Authorization authorization) {
        OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
                .getToken(OAuth2AuthorizationCode.class);
        return Objects.nonNull(authorizationCode);
    }

    private static boolean isRefreshToken(OAuth2Authorization authorization) {
        return Objects.nonNull(authorization.getRefreshToken());
    }

    private static boolean isAccessToken(OAuth2Authorization authorization) {
        return Objects.nonNull(authorization.getAccessToken());
    }

}

B3.配置类

C1.RedisTemplateConfiguration

创建一个RedisTemplate的bean。

这里有点小问题,系统序列化存储信息,拿取信息是正常的,但是在RedisDeskTopManager工具里看到的是乱码,还没找到解决办法o(╥﹏╥)o待解决

@EnableCaching
@AutoConfiguration
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisTemplateConfiguration {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(RedisSerializer.java());
        redisTemplate.setHashValueSerializer(RedisSerializer.java());
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }


    @Bean
    public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }

    @Bean
    public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
        return redisTemplate.opsForValue();
    }

    @Bean
    public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForList();
    }

    @Bean
    public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForSet();
    }

    @Bean
    public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForZSet();
    }

}

C2.RegisteredClientConfiguration

这个类里面配置客户授权信息的全部去掉,不再需要了。或者直接删除这个类吧,不再需要了

//    @Bean
//    public OAuth2AuthorizationService oAuth2AuthorizationService(){
//        //new一个授权管理业务,这里用的基于内存持久的
//        OAuth2AuthorizationService oAuth2AuthorizationService = new InMemoryOAuth2AuthorizationService();
//        return oAuth2AuthorizationService;
//    }
//

C3.resources/META-INF/spring.factories(pig-common-core和pig-auth)

在未直接import依赖时,同时这个类又不是启动类的同级及子类,就不会被扫描成bean,所以,我们需要手动import,可以使用@ComponentScans(""),也可以通过spring.factories引入。

--------------pig-auth

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.pig4cloud.pig.auth.service.impl.PigRedisOAuth2AuthorizationServiceImpl


--------------pig-common-core
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.pig4cloud.pig.common.core.config.RedisTemplateConfiguration

A4.给用户登录信息和客户信息添加cache缓存

有两种方式使用Cache缓存,一种是直接拿到CacheManager对象,然后拿到指定的Cache并调用get,set;一种是通过注解来调用;

B1.用户登录信息添加cache缓存

C1.PigUserDetailsServiceImpl

@Service
@RequiredArgsConstructor
public class PigUserDetailsServiceImpl implements UserDetailsService {

    private final SysUserService sysUserService;

    private final CacheManager cacheManager;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
        if (cache != null && cache.get(username) != null) {
            return (PigUser) cache.get(username).get();
        }

        SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getUsername,username));

        //这里需要给密码加入密码器类型:{bcrypt},因为我们用的是 DelegatingPasswordEncoder ,需要根据头部判断用哪个密码管理器
        PigUser userDetails = new PigUser(sysUser.getUsername(), SecurityConstants.BCRYPT + sysUser.getPassword(),new HashSet(Collections.singleton(new SimpleGrantedAuthority("write"))));

        if (cache != null) {
            cache.put(username, userDetails);
        }
        return userDetails;
    }
}

B2.客户信息添加cache缓存

C1.PigRegisteredClientRepository

    @Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null")
    @Override
    public RegisteredClient findByClientId(String clientId) {
        。。。
    }

相关标签:

免责声明

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱290110527@qq.com删除。

Top