当前位置:   article > 正文

Springboot3.0+Security6.0+jwt+jdk17登陆认证_springboot3.0+security6.0+redis+jdk17

springboot3.0+security6.0+redis+jdk17

集各位大佬的精华,弄出来的一套基于jdk17,Springboot3.0,SpringSecurity6.0,jwt的登陆及接口拦截体系。

1.先贴依赖:

  1. <parent>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-parent</artifactId>
  4. <version>3.0.0</version>
  5. <relativePath/>
  6. </parent>
  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-jdbc</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-security</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.springframework.boot</groupId>
  11. <artifactId>spring-boot-starter-web</artifactId>
  12. </dependency>
  13. <dependency>
  14. <groupId>org.springframework.boot</groupId>
  15. <artifactId>spring-boot-starter-test</artifactId>
  16. <scope>test</scope>
  17. </dependency>
  18. <dependency>
  19. <groupId>org.springframework.security</groupId>
  20. <artifactId>spring-security-test</artifactId>
  21. <scope>test</scope>
  22. </dependency>
  23. <!-- jwt -->
  24. <dependency>
  25. <groupId>com.nimbusds</groupId>
  26. <artifactId>nimbus-jose-jwt</artifactId>
  27. <version>9.31</version>
  28. </dependency>
  29. <dependency>
  30. <groupId>org.projectlombok</groupId>
  31. <artifactId>lombok</artifactId>
  32. <optional>true</optional>
  33. </dependency>
  34. <!-- mybatis-plus -->
  35. <dependency>
  36. <groupId>com.baomidou</groupId>
  37. <artifactId>mybatis-plus-boot-starter</artifactId>
  38. <version>3.5.3.1</version>
  39. </dependency>
  40. <dependency>
  41. <groupId>com.baomidou</groupId>
  42. <artifactId>mybatis-plus-generator</artifactId>
  43. <version>3.5.3.1</version>
  44. <exclusions>
  45. <exclusion>
  46. <artifactId>mybatis</artifactId>
  47. <groupId>org.mybatis</groupId>
  48. </exclusion>
  49. </exclusions>
  50. </dependency>
  51. <dependency>
  52. <groupId>log4j</groupId>
  53. <artifactId>log4j</artifactId>
  54. <version>1.2.17</version>
  55. </dependency>
  56. <dependency>
  57. <groupId>junit</groupId>
  58. <artifactId>junit</artifactId>
  59. <version>4.12</version>
  60. <scope>test</scope>
  61. </dependency>
  62. <dependency>
  63. <groupId>com.alibaba.fastjson2</groupId>
  64. <artifactId>fastjson2</artifactId>
  65. <version>2.0.26</version>
  66. </dependency>

2.jwtUtil  

  1. import com.alibaba.fastjson2.JSON;
  2. import com.nimbusds.jose.*;
  3. import com.nimbusds.jose.crypto.MACSigner;
  4. import com.nimbusds.jose.crypto.MACVerifier;
  5. import com.zy.common.ResponseEntity;
  6. import com.zy.entity.security.Claims;
  7. import java.text.ParseException;
  8. import java.util.Date;
  9. import java.util.UUID;
  10. /**
  11. * @author : zy
  12. * JWT工具类
  13. */
  14. public class JWTUtil {
  15. //密钥
  16. private static final String secret = "xpo1xgnl5ksinxkgu1nb6vcx3zaq1wsxvv";
  17. // 1000 * 60 * 60 * 24 * 1 一天
  18. //过期时间12h,单位毫秒
  19. private static final long EXPIRE = 1000 * 60 * 60 * 12;
  20. // 测试时为1min
  21. // private static final long EXPIRE = 1000 * 60 * 1;
  22. /**
  23. * 创建token
  24. *
  25. * @param claims 用户信息
  26. * @return 令牌
  27. */
  28. public static String createToken(Claims claims) {
  29. try {
  30. //对密钥进行签名
  31. JWSSigner jwsSigner = new MACSigner(secret);
  32. //准备JWS header
  33. JWSHeader jwsHeader = new JWSHeader
  34. .Builder(JWSAlgorithm.HS256)
  35. .type(JOSEObjectType.JWT)
  36. .build();
  37. //准备JWS payload
  38. claims.setJti(UUID.randomUUID().toString());
  39. claims.setIat(new Date().getTime());
  40. claims.setExp(new Date(System.currentTimeMillis() + EXPIRE).getTime());
  41. Payload payload = new Payload(JSON.toJSONString(claims));
  42. //封装JWS对象
  43. JWSObject jwsObject = new JWSObject(jwsHeader, payload);
  44. //签名
  45. jwsObject.sign(jwsSigner);
  46. return jwsObject.serialize();
  47. } catch (KeyLengthException e) {
  48. e.printStackTrace();
  49. } catch (JOSEException e) {
  50. e.printStackTrace();
  51. }
  52. return null;
  53. }
  54. /**
  55. * 验证并获取用户信息
  56. *
  57. * @param token 令牌
  58. * @return 解析后用户信息
  59. */
  60. public static ResponseEntity verifyToken(String token) {
  61. JWSObject jwsObject;
  62. ResponseEntity response = new ResponseEntity<>();
  63. try {
  64. jwsObject = JWSObject.parse(token);
  65. //HMAC验证器
  66. JWSVerifier jwsVerifier = new MACVerifier(secret);
  67. if (!jwsObject.verify(jwsVerifier)) {
  68. response.setCode(10008).setErrorMsg("token无效");
  69. return response;
  70. }
  71. String payload = jwsObject.getPayload().toString();
  72. Claims claims = JSON.parseObject(payload, Claims.class);
  73. if (claims.getExp() < new Date().getTime()) {
  74. response.setCode(10008).setErrorMsg("token无效");
  75. return response;
  76. }
  77. response.setCode(200).setData(claims).setMessage("解析成功");
  78. return response;
  79. } catch (ParseException | JOSEException e) {
  80. e.printStackTrace();
  81. }
  82. response.setCode(10008).setErrorMsg("token无效");
  83. return response;
  84. }
  85. }

3.jwtAuthenticationFilter

  1. import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
  2. import com.baomidou.mybatisplus.core.toolkit.StringUtils;
  3. import com.zy.common.ResponseEntity;
  4. import com.zy.config.security.JWTUtil;
  5. import com.zy.entity.security.Claims;
  6. import jakarta.servlet.FilterChain;
  7. import jakarta.servlet.ServletException;
  8. import jakarta.servlet.http.HttpServletRequest;
  9. import jakarta.servlet.http.HttpServletResponse;
  10. import org.springframework.stereotype.Component;
  11. import org.springframework.web.filter.OncePerRequestFilter;
  12. import java.io.IOException;
  13. import java.util.logging.Logger;
  14. /**
  15. * @author : zy
  16. *
  17. * 自定义jwt全局过滤器
  18. * 1.没有携带token放行
  19. * 2.携带token,将用户信息添加至security上下文中
  20. */
  21. @Component
  22. public class JWTAuthenticationFilter extends OncePerRequestFilter {
  23. private static final Logger logger = Logger.getLogger(JWTAuthenticationFilter.class.toString());
  24. @Override
  25. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  26. //获取当前请求的uri
  27. String uri = request.getRequestURI();
  28. logger.info("请求路径:" + uri);
  29. //判断是否是认证请求路径
  30. //是:直接放行
  31. if (uri.endsWith("/auth/login") || uri.endsWith("/auth/logout") || uri.startsWith("/swagger-ui")
  32. || uri.endsWith("doc.html") || uri.startsWith("/webjars/css") || uri.startsWith("/webjars/js")
  33. || uri.startsWith("/v3/api-docs") || uri.startsWith("/favicon.ico")
  34. || uri.startsWith("**/*.html") || uri.endsWith("/webjars/springfox-swagger-ui")
  35. || uri.startsWith("/swagger-resources")) {
  36. filterChain.doFilter(request, response);
  37. return;
  38. }
  39. //否:获取请求头中携带的token
  40. String authorization = request.getHeader("Authorization");
  41. logger.info("携带authorization:" + authorization);
  42. //判断是否携带token
  43. //否:抛出异常
  44. if (StringUtils.isBlank(authorization)) {
  45. logger.info("未查询到token");
  46. return;
  47. }
  48. String realToken = authorization.replace("Bearer ", "");
  49. //是:校验jwt有效性
  50. ResponseEntity responseE = JWTUtil.verifyToken(realToken);
  51. Claims data = (Claims) responseE.getData();
  52. if (ObjectUtils.isEmpty(data)) {
  53. logger.info("token失效");
  54. return;
  55. }
  56. // 验证token对象是否存在及验证token是否过期
  57. if (ObjectUtils.isEmpty(data)) {
  58. logger.info("token无效或者已经失效");
  59. return;
  60. }
  61. if (responseE.getCode() != 200) {
  62. logger.info("token无效");
  63. return;
  64. }
  65. filterChain.doFilter(request, response);
  66. }
  67. }

4.SecurityConfig

注:此处.requestMatchers("/**").permitAll()应该是把所有接口都放开,在jwtAuthenticationFilter中实现接口拦截,因为不写/**,其余接口直接访问不到,才疏学浅不知道为啥,大家可以试试,改正。

  1. import com.zy.config.security.filter.JWTAuthenticationFilter;
  2. import com.zy.entity.SysUser;
  3. import com.zy.entity.security.LoginUser;
  4. import com.zy.mapper.SysUserMapper;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.context.annotation.Configuration;
  8. import org.springframework.http.HttpMethod;
  9. import org.springframework.security.authentication.AuthenticationManager;
  10. import org.springframework.security.authentication.AuthenticationProvider;
  11. import org.springframework.security.authentication.LockedException;
  12. import org.springframework.security.authentication.ProviderManager;
  13. import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
  14. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  15. import org.springframework.security.config.http.SessionCreationPolicy;
  16. import org.springframework.security.core.userdetails.UserDetailsService;
  17. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  18. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  19. import org.springframework.security.crypto.password.PasswordEncoder;
  20. import org.springframework.security.web.SecurityFilterChain;
  21. import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
  22. /**
  23. * @author zy
  24. */
  25. @Configuration
  26. public class SecurityConfig {
  27. @Autowired
  28. private SysUserMapper sysUserMapper;
  29. @Autowired
  30. private JWTAuthenticationFilter jwtAuthenticationFilter;
  31. @Bean
  32. public PasswordEncoder passwordEncoder() {
  33. return new BCryptPasswordEncoder();
  34. }
  35. @Bean
  36. public UserDetailsService userDetailsService() {
  37. return username -> {
  38. SysUser user = sysUserMapper.selectByName(username);
  39. if (user == null) {
  40. throw new UsernameNotFoundException("用户不存在");
  41. }
  42. Integer status = user.getStatus();
  43. if (status != 0){
  44. throw new LockedException("用户已停用");
  45. }
  46. return new LoginUser(user);
  47. };
  48. }
  49. @Bean
  50. public AuthenticationProvider authenticationProvider() {
  51. DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
  52. authenticationProvider.setUserDetailsService(userDetailsService());
  53. authenticationProvider.setPasswordEncoder(passwordEncoder());
  54. authenticationProvider.setHideUserNotFoundExceptions(false);
  55. return authenticationProvider;
  56. }
  57. @Bean
  58. public AuthenticationManager authenticationManager() {
  59. return new ProviderManager(authenticationProvider());
  60. }
  61. @Bean
  62. public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  63. http
  64. .csrf().disable()
  65. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
  66. .and()
  67. .authenticationProvider(authenticationProvider())
  68. .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
  69. .authorizeHttpRequests()
  70. .requestMatchers(HttpMethod.OPTIONS).permitAll()
  71. .requestMatchers("/**").permitAll()
  72. .requestMatchers("/auth/login").permitAll()
  73. .requestMatchers("/auth/logout").permitAll()
  74. .requestMatchers("/swagger-ui.html").permitAll()
  75. .requestMatchers("/doc.html").permitAll()
  76. .requestMatchers("/webjars/springfox-swagger-ui/**").permitAll()
  77. .requestMatchers("/swagger-resources").permitAll()
  78. .requestMatchers("/v3/api-docs/**").permitAll()
  79. .requestMatchers("/favicon.ico").permitAll()
  80. .requestMatchers("/error").permitAll()
  81. .anyRequest()
  82. .authenticated();
  83. return http.build();
  84. }
  85. }

5.返回类型 ResponseEntity

  1. import io.swagger.v3.oas.annotations.media.Schema;
  2. import lombok.Data;
  3. import java.util.HashMap;
  4. /**
  5. * @author zy
  6. */
  7. @Schema(description = "返回响应数据")
  8. @Data
  9. public class ResponseEntity<T> {
  10. @Schema(description = "编码")
  11. private int code = 200;
  12. @Schema(description = "基本信息")
  13. private String message = "成功";
  14. @Schema(description = "错误信息")
  15. private String errorMsg = "";
  16. @Schema(description = "返回对象")
  17. private T data;
  18. /**
  19. * 成功状态码
  20. */
  21. public static final Integer SUCCESS = 200;
  22. /**
  23. * 失败状态码
  24. */
  25. public static final Integer ERROR = 500;
  26. private static HashMap<Integer, String> ERROR_CODE = new HashMap<Integer, String>() {
  27. {
  28. put(100, "暂无数据");
  29. put(200, "成功");
  30. put(300, "失败");
  31. put(500, "失败状态码");
  32. put(10000, "通用错误");
  33. ///用户类
  34. put(10001, "用户名或密码错误");
  35. put(10002, "登录状态已过期");
  36. put(10003, "注册用户已存在");
  37. put(10004, "账号已被锁定,请在一小时后重试");
  38. put(10005, "旧密码错误");
  39. put(10006, "用户名已存在");
  40. put(10007, "ip没有权限");
  41. put(10008, "token无效");
  42. put(10009, "token失效");
  43. ///操作权限类
  44. put(20001, "无操作权限");
  45. ///参数类
  46. put(30001, "非法参数");
  47. put(30002, "缺少必要参数");
  48. 数据操作类
  49. put(40001, "添加数据失败");
  50. put(40002, "更新数据失败");
  51. put(40003, "删除数据失败");
  52. put(40004, "添加数据失败,对象已经存在,建议修改或者删除");
  53. put(50001, "不存在的对象");
  54. put(99999, "无任何资源权限");
  55. put(990000, "系统错误");
  56. }
  57. };
  58. public ResponseEntity() {
  59. }
  60. public ResponseEntity(T date) {
  61. this.data = date;
  62. }
  63. public int getCode() {
  64. return code;
  65. }
  66. public ResponseEntity setCode(int code) {
  67. this.code = code;
  68. if (ERROR_CODE.containsKey(code)) {
  69. setMessage(ERROR_CODE.get(code));
  70. }
  71. return this;
  72. }
  73. public String getMessage() {
  74. return message;
  75. }
  76. public void setMessage(String message) {
  77. this.message = message;
  78. }
  79. public T getData() {
  80. return data;
  81. }
  82. public ResponseEntity setData(T data) {
  83. this.data = data;
  84. return this;
  85. }
  86. public static <T> ResponseEntity<T> def(Class<T> clazz) {
  87. return new ResponseEntity<>();
  88. }
  89. public ResponseEntity<T> ok() {
  90. setCode(200);
  91. return this;
  92. }
  93. public ResponseEntity<T> error(int code) {
  94. setCode(code);
  95. return this;
  96. }
  97. public ResponseEntity<T> message(String message) {
  98. setMessage(message);
  99. return this;
  100. }
  101. public ResponseEntity<T> data(T data) {
  102. setData(data);
  103. return this;
  104. }
  105. public ResponseEntity<T> back(int code, String message, T data) {
  106. setCode(code);
  107. setMessage(message);
  108. setData(data);
  109. return this;
  110. }
  111. public static <T> Boolean isError(ResponseEntity<T> r) {
  112. return !isSuccess(r);
  113. }
  114. public static <T> Boolean isSuccess(ResponseEntity<T> r) {
  115. return ResponseEntity.SUCCESS == r.getCode();
  116. }
  117. }

6.Claims

  1. import lombok.Data;
  2. import java.util.List;
  3. /**
  4. * jwt实体数据
  5. */
  6. @Data
  7. public class Claims {
  8. /**
  9. * 主题
  10. */
  11. private String sub;
  12. /**
  13. * 签发时间
  14. */
  15. private Long iat;
  16. /**
  17. * 过期时间
  18. */
  19. private Long exp;
  20. /**
  21. * JWT ID
  22. */
  23. private String jti;
  24. /**
  25. * 用户id
  26. */
  27. private String userId;
  28. /**
  29. * 用户名
  30. */
  31. private String username;
  32. /**
  33. * 用户状态(1:正常;0:禁用)
  34. */
  35. private String status;
  36. /**
  37. * 用户角色
  38. */
  39. private List<String> roles;
  40. /**
  41. * 权限列表
  42. */
  43. private List<String> permissions;
  44. public Claims(String sub, Long iat, Long exp, String jti, String userId, String username, String status, List<String> roles, List<String> permissions) {
  45. this.sub = sub;
  46. this.iat = iat;
  47. this.exp = exp;
  48. this.jti = jti;
  49. this.userId = userId;
  50. this.username = username;
  51. this.status = status;
  52. this.roles = roles;
  53. this.permissions = permissions;
  54. }
  55. public String getSub() {
  56. return sub;
  57. }
  58. public void setSub(String sub) {
  59. this.sub = sub;
  60. }
  61. public Long getIat() {
  62. return iat;
  63. }
  64. public void setIat(Long iat) {
  65. this.iat = iat;
  66. }
  67. public Long getExp() {
  68. return exp;
  69. }
  70. public void setExp(Long exp) {
  71. this.exp = exp;
  72. }
  73. public String getJti() {
  74. return jti;
  75. }
  76. public void setJti(String jti) {
  77. this.jti = jti;
  78. }
  79. public String getUserId() {
  80. return userId;
  81. }
  82. public void setUserId(String userId) {
  83. this.userId = userId;
  84. }
  85. public String getUsername() {
  86. return username;
  87. }
  88. public void setUsername(String username) {
  89. this.username = username;
  90. }
  91. public String getStatus() {
  92. return status;
  93. }
  94. public void setStatus(String status) {
  95. this.status = status;
  96. }
  97. public List<String> getRoles() {
  98. return roles;
  99. }
  100. public void setRoles(List<String> roles) {
  101. this.roles = roles;
  102. }
  103. public List<String> getPermissions() {
  104. return permissions;
  105. }
  106. public void setPermissions(List<String> permissions) {
  107. this.permissions = permissions;
  108. }
  109. public static ClaimsBuilder builder() {
  110. return new ClaimsBuilder();
  111. }
  112. public static final class ClaimsBuilder {
  113. //主题
  114. private String sub;
  115. //签发时间
  116. private Long iat;
  117. //过期时间
  118. private Long exp;
  119. //JWT ID
  120. private String jti;
  121. //用户id
  122. private String userId;
  123. //用户名
  124. private String username;
  125. private String status;
  126. //用户角色
  127. private List<String> roles;
  128. private List<String> permissions;
  129. private ClaimsBuilder() {
  130. }
  131. public ClaimsBuilder sub(String sub) {
  132. this.sub = sub;
  133. return this;
  134. }
  135. public ClaimsBuilder iat(Long iat) {
  136. this.iat = iat;
  137. return this;
  138. }
  139. public ClaimsBuilder exp(Long exp) {
  140. this.exp = exp;
  141. return this;
  142. }
  143. public ClaimsBuilder jti(String jti) {
  144. this.jti = jti;
  145. return this;
  146. }
  147. public ClaimsBuilder userId(String userId) {
  148. this.userId = userId;
  149. return this;
  150. }
  151. public ClaimsBuilder username(String username) {
  152. this.username = username;
  153. return this;
  154. }
  155. public ClaimsBuilder status(String status) {
  156. this.status = status;
  157. return this;
  158. }
  159. public ClaimsBuilder roles(List<String> roles) {
  160. this.roles = roles;
  161. return this;
  162. }
  163. public ClaimsBuilder permissions(List<String> permissions) {
  164. this.permissions = permissions;
  165. return this;
  166. }
  167. public Claims build() {
  168. return new Claims(
  169. this.sub,
  170. this.iat,
  171. this.exp,
  172. this.jti,
  173. this.userId,
  174. this.username,
  175. this.status,
  176. this.roles,
  177. this.permissions);
  178. }
  179. }
  180. }

7.LoginUser

  1. import com.zy.entity.SysUser;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. import org.springframework.security.core.GrantedAuthority;
  6. import org.springframework.security.core.userdetails.UserDetails;
  7. import java.util.ArrayList;
  8. import java.util.Collection;
  9. @Data
  10. @NoArgsConstructor
  11. @AllArgsConstructor
  12. public class LoginUser implements UserDetails {
  13. private SysUser user;
  14. @Override
  15. public Collection<? extends GrantedAuthority> getAuthorities() {
  16. return new ArrayList<>();
  17. }
  18. @Override
  19. public String getPassword() {
  20. return user.getPassword();
  21. }
  22. @Override
  23. public String getUsername() {
  24. return user.getUserName();
  25. }
  26. @Override
  27. public boolean isAccountNonExpired() {
  28. return true;
  29. }
  30. @Override
  31. public boolean isAccountNonLocked() {
  32. return true;
  33. }
  34. @Override
  35. public boolean isCredentialsNonExpired() {
  36. return true;
  37. }
  38. @Override
  39. public boolean isEnabled() {
  40. return true;
  41. }
  42. }

8.login登陆

  1. public ResponseEntity login(SysUser sysUser ) {
  2. ResponseEntity<Object> response = new ResponseEntity<>();
  3. // 获取用户密码
  4. String username = sysUser.getUserName();
  5. String password = sysUser.getPassword();
  6. // 用户认证
  7. UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
  8. Authentication authenticate = authenticationManagerBuilder.getObject().authenticate(usernamePasswordAuthenticationToken);
  9. // 认证成功后,返回用户信息及用户角色信息
  10. SysUser byNameUser = sysUserMapper.selectByName(username);
  11. LambdaQueryWrapper<SysUserRole> sysUserRoleLambdaQueryWrapper = new LambdaQueryWrapper<>();
  12. sysUserRoleLambdaQueryWrapper.eq(SysUserRole::getUserId,userId);
  13. List<SysUserRole> sysUserRoles = sysUserRoleMapper.selectList(sysUserRoleLambdaQueryWrapper);
  14. JSONObject data = new JSONObject();
  15. data.put("user",byNameUser);
  16. // jwt实体数据
  17. Claims claims = Claims.builder()
  18. .userId(userId)
  19. .username(byNameUser.getUserName())
  20. .roles(roles)
  21. .build();
  22. JWTUtil jwtUtil = new JWTUtil();
  23. String token = jwtUtil.createToken(claims);
  24. data.put("token",token);
  25. response.setCode(200).setData(data);
  26. return response;
  27. }

9.sysUser是用户实体类,相关的代码就不贴了,可以根据自己数据库需求进行编写。

有什么疑问或者教导请指正,谢谢。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/388663
推荐阅读
相关标签
  

闽ICP备14008679号