Spring Boot整合Shiro

分手后的思念是犯贱 2022-04-05 08:42 325阅读 0赞

文章目录

  • Spring Boot+Shiro
    • 1 简介
    • 2 环境搭建
      • 2.1 开发环境
      • 2.2 创建项目
      • 2.3 项目文件结构
      • 2.4 pom.xml
      • 2.5 application.yml
    • 3 数据库
      • 3.1 数据库设计
      • 3.2 数据库字段
    • 4 类
      • 4.1 po
      • 4.2 mapper
      • 4.3 config
      • 4.4 realm
      • 4.5 service
      • 4.6 controller
    • 5 页面
    • 6 测试运行
      • 6.1 普通用户登录
      • 6.2 会员用户登录

Spring Boot+Shiro

Shiro这个Java安全框架我一直都想学会怎么去使用,但每次依照着别人的博客尝试把它配置到自己的项目中,总是出现各种问题,导致一直没有成功。经过不懈努力,这一次终于成功了!从零搭建整个项目,并通过一个简单的用户登录功能来进行说明!

源码github地址:https://github.com/Rhine404/shirodemo

1 简介

推荐阅读官方文档去认识Shiro,虽然不是中文的,但绝对是比看各种博客里重复度高、零零散散的资料好。

官方的简介如下:

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.

Shiro是一款强大易用的Java安全框架,主要功能是进行认证、授权、加密、会话管理等。与Spring Security相比的话,Shiro是会更加轻量。

2 环境搭建

2.1 开发环境

  • IDEA
  • HTML/CSS/JavaScript + FreeMarker
  • JDK8 + Maven + Spring Boot + Shiro
  • druid + mybatis + Navicat + MySQL5.7

这里需要你有预备知识:

  • 熟练使用IDEA或eclipse
  • 了解Maven,知道如何配置settings.xml与maven仓库
  • 了解Spring Boot,并会使用它搭建Web项目
  • 了解FreeMarker/thymeleaf、yaml语法

虽然都是通过Maven构建项目,不过Spring Boot不再使用.xml文件对项目进行配置,所以Shiro的配置类ShiroConfig不太容易理解,对于刚开始接触Spring Boot的同学来说这是一个难点。

2.2 创建项目

这里使用IDEA通过Spring Initializr创建项目(当然也可以通过Maven自己添加所有依赖)。
选择依赖
我们下面要通过这个项目来搭建一个简单的用户系统,系统有两类用户——普通用户和VIP用户,通过shiro来完成登录、登出、权限验证功能,主要在于展示前后端的交互、后端与数据库的交互。

2.3 项目文件结构

项目结构
项目结构2

2.4 pom.xml

新建的项目还需要再手动增加三个依赖项:druid数据库连接池,shiro依赖,log4j依赖

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3. <modelVersion>4.0.0</modelVersion>
  4. <parent>
  5. <groupId>org.springframework.boot</groupId>
  6. <artifactId>spring-boot-starter-parent</artifactId>
  7. <version>2.1.1.RELEASE</version>
  8. <relativePath/> <!-- lookup parent from repository -->
  9. </parent>
  10. <groupId>com.rhine.blog</groupId>
  11. <artifactId>shirodemo</artifactId>
  12. <version>0.0.1-SNAPSHOT</version>
  13. <packaging>jar</packaging>
  14. <name>shirodemo</name>
  15. <description>Spring Boot with Shiro</description>
  16. <properties>
  17. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  18. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  19. <java.version>1.8</java.version>
  20. </properties>
  21. <dependencies>
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-starter-freemarker</artifactId>
  25. </dependency>
  26. <dependency>
  27. <groupId>org.springframework.boot</groupId>
  28. <artifactId>spring-boot-starter-jdbc</artifactId>
  29. </dependency>
  30. <dependency>
  31. <groupId>org.springframework.boot</groupId>
  32. <artifactId>spring-boot-starter-web</artifactId>
  33. </dependency>
  34. <dependency>
  35. <groupId>org.mybatis.spring.boot</groupId>
  36. <artifactId>mybatis-spring-boot-starter</artifactId>
  37. <version>1.3.2</version>
  38. </dependency>
  39. <dependency>
  40. <groupId>mysql</groupId>
  41. <artifactId>mysql-connector-java</artifactId>
  42. <scope>runtime</scope>
  43. </dependency>
  44. <dependency>
  45. <groupId>org.springframework.boot</groupId>
  46. <artifactId>spring-boot-starter-test</artifactId>
  47. <scope>test</scope>
  48. </dependency>
  49. <!-- 数据库连接池 -->
  50. <dependency>
  51. <groupId>com.alibaba</groupId>
  52. <artifactId>druid</artifactId>
  53. <version>1.1.10</version>
  54. </dependency>
  55. <!-- Shiro -->
  56. <dependency>
  57. <groupId>org.apache.shiro</groupId>
  58. <artifactId>shiro-spring</artifactId>
  59. <version>1.3.2</version>
  60. </dependency>
  61. <!-- log4j -->
  62. <dependency>
  63. <groupId>log4j</groupId>
  64. <artifactId>log4j</artifactId>
  65. <version>1.2.17</version>
  66. </dependency>
  67. </dependencies>
  68. <build>
  69. <plugins>
  70. <plugin>
  71. <groupId>org.springframework.boot</groupId>
  72. <artifactId>spring-boot-maven-plugin</artifactId>
  73. </plugin>
  74. </plugins>
  75. </build>
  76. </project>

2.5 application.yml

druid的配置会稍微多些,所以也附上了详细的注释,属性和值之间必须要有”:”号。

  1. spring:
  2. datasource:
  3. url: jdbc:mysql://localhost:3306/lastpass?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
  4. username: root
  5. password: abc123456
  6. driver-class-name: com.mysql.cj.jdbc.Driver
  7. type: com.alibaba.druid.pool.DruidDataSource
  8. # 初始化时建立物理连接连接的个数
  9. initialSize: 5
  10. # 最小连接池数量
  11. minIdle: 5
  12. # 最大连接池数量
  13. maxActive: 20
  14. # 获取连接时最大等待时间(ms),即60s
  15. maxWait: 60000
  16. # 1.Destroy线程会检测连接的间隔时间;2.testWhileIdle的判断依据
  17. timeBetweenEvictionRunsMillis: 60000
  18. # 最小生存时间ms
  19. minEvictableIdleTimeMillis: 600000
  20. maxEvictableIdleTimeMillis: 900000
  21. # 用来检测连接是否有效的sql
  22. validationQuery: SELECT 1 FROM DUAL
  23. # 申请连接时执行validationQuery检测连接是否有效,启用会降低性能
  24. testOnBorrow: false
  25. # 归还连接时执行validationQuery检测连接是否有效,启用会降低性能
  26. testOnReturn: false
  27. # 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,
  28. # 执行validationQuery检测连接是否有效,不会降低性能
  29. testWhileIdle: true
  30. # 是否缓存preparedStatement,mysql建议关闭
  31. poolPreparedStatements: false
  32. # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
  33. filters: stat,wall,log4j
  34. freemarker:
  35. suffix: .html
  36. charset: utf-8
  37. mvc:
  38. # 配置静态资源映射路径,/public、/resources路径失效
  39. static-path-pattern: /static/**
  40. mybatis:
  41. mapper-locations: classpath:mappers/*.xml
  42. # 虽然可以配置这项来进行pojo包扫描,但其实我更倾向于在mapper.xml写全类名
  43. type-aliases-package: com.rhine.blog.po

3 数据库

因为我们不仅仅要使用Shiro来进行认证,还要进行授权、加密等操作,所以我们要建立用户表以及权限管理表。

3.1 数据库设计

数据库有用户(user)、角色(role)、权限(permission)三个实体,除了实体表以外,为了实现表间用户与角色、角色与权限多对多的表间关系,所以产生了user_role、role_permission两张关系表。在下图中,使用红线将表的外键标记了出来,但为了方便并没有在表中创建外键,我们手动进行维护。
表间关系

3.2 数据库字段

再简单介绍下数据库字段,user表中name是用户名,password是密码;role表中name是角色名(如user、vip);permission表中,name是权限名(如会员中心),url是实际的权限字段(user:vip)

4 类

4.1 po

Userbean.java

  1. package com.rhine.blog.po;
  2. /** * 用户类 */
  3. public class UserBean implements Serializable {
  4. private String id;
  5. private String name;
  6. private String password;
  7. private Set<RoleBean> roles = new HashSet<>();
  8. // 省略setter、getter方法
  9. }

RoleBean.java

  1. package com.rhine.blog.po;
  2. /** * 角色类 */
  3. public class RoleBean implements Serializable {
  4. private String id;
  5. private String name;
  6. private Set<PermissionBean> permissions = new HashSet<>();
  7. // 省略setter、getter方法
  8. }

RoleBean.java

  1. package com.rhine.blog.po;
  2. /** * 权限类 */
  3. public class PermissionBean implements Serializable {
  4. private String id;
  5. private String name;
  6. private String url;
  7. // 省略setter、getter方法
  8. }

4.2 mapper

UserMapper.java

  1. package com.rhine.blog.mapper;
  2. public interface UserMapper {
  3. // 查询用户信息
  4. UserBean findByName(String name);
  5. // 查询用户信息、角色、权限
  6. UserBean findById(String id);
  7. }

UserMapper.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3. <mapper namespace="com.rhine.blog.mapper.UserMapper">
  4. <resultMap id="userMap" type="UserBean">
  5. <id property="id" column="id"/>
  6. <result property="name" column="name"/>
  7. <result property="password" column="password"/>
  8. <collection property="roles" ofType="RoleBean">
  9. <id property="id" column="roleId"/>
  10. <result property="name" column="roleName"/>
  11. <collection property="permissions" ofType="PermissionBean">
  12. <id property="id" column="permissionId"/>
  13. <result property="name" column="permissionName"/>
  14. <result property="url" column="permissionUrl"/>
  15. </collection>
  16. </collection>
  17. </resultMap>
  18. <select id="findByName" parameterType="String" resultType="UserBean">
  19. SELECT id, name, password
  20. FROM user
  21. WHERE NAME = #{name}
  22. </select>
  23. <select id="findById" parameterType="String" resultMap="userMap">
  24. SELECT user.id, user.name, user.password,
  25. role.id as roleId, role.name as roleName,
  26. permission.id as permissionId,
  27. permission.name as permissionName,
  28. permission.url as permissionUrl
  29. FROM user, user_role, role, role_permission, permission
  30. WHERE user.id = #{id}
  31. AND user.id = user_role.user_id
  32. AND user_role.role_id = role.id
  33. AND role.id = role_permission.role_id
  34. AND role_permission.permission_id = permission.id
  35. </select>
  36. </mapper>

因为在application.yml中配置了 type-aliases-package: com.rhine.blog.po,所以resultType可以不用写全类名。添加新的查询时,一定要区分清楚resultMap和resultType,否则出错不容易发现。

4.3 config

DruidConfig.java

  1. package com.rhine.blog.config;
  2. /** * Druid配置类 */
  3. @Configuration
  4. public class DruidConfig {
  5. @ConfigurationProperties(prefix = "spring.datasource")
  6. @Bean(destroyMethod="close", initMethod = "init")
  7. public DataSource druid(){
  8. return new DruidDataSource();
  9. }
  10. /** * 配置监控服务器 **/
  11. @Bean
  12. public ServletRegistrationBean statViewServlet(){
  13. ServletRegistrationBean bean = new ServletRegistrationBean(
  14. new StatViewServlet(), "/druid/*");
  15. Map<String,String> initParams = new HashMap<>();
  16. // druid后台管理员用户
  17. initParams.put("loginUsername","admin");
  18. initParams.put("loginPassword","123456");
  19. // 是否能够重置数据
  20. initParams.put("resetEnable", "false");
  21. bean.setInitParameters(initParams);
  22. return bean;
  23. }
  24. /** * 配置web监控的过滤器 **/
  25. @Bean
  26. public FilterRegistrationBean webStatFilter(){
  27. FilterRegistrationBean bean = new FilterRegistrationBean(
  28. new WebStatFilter());
  29. // 添加过滤规则
  30. bean.addUrlPatterns("/*");
  31. Map<String,String> initParams = new HashMap<>();
  32. // 忽略过滤格式
  33. initParams.put("exclusions","*.js,*.css,*.icon,*.png,*.jpg,/druid/*");
  34. bean.setInitParameters(initParams);
  35. return bean;
  36. }
  37. }

ShiroConfig.java

  1. package com.rhine.blog.config;
  2. /** * Shiro配置类 */
  3. @Configuration
  4. public class ShiroConfig {
  5. @Bean("hashedCredentialsMatcher")
  6. public HashedCredentialsMatcher hashedCredentialsMatcher() {
  7. HashedCredentialsMatcher credentialsMatcher =
  8. new HashedCredentialsMatcher();
  9. //指定加密方式为MD5
  10. credentialsMatcher.setHashAlgorithmName("MD5");
  11. //加密次数
  12. credentialsMatcher.setHashIterations(1024);
  13. credentialsMatcher.setStoredCredentialsHexEncoded(true);
  14. return credentialsMatcher;
  15. }
  16. @Bean("userRealm")
  17. public UserRealm userRealm(@Qualifier("hashedCredentialsMatcher")
  18. HashedCredentialsMatcher matcher) {
  19. UserRealm userRealm = new UserRealm();
  20. userRealm.setCredentialsMatcher(matcher);
  21. return userRealm;
  22. }
  23. @Bean
  24. public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager")
  25. DefaultWebSecurityManager securityManager) {
  26. ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
  27. // 设置 SecurityManager
  28. bean.setSecurityManager(securityManager);
  29. // 设置登录成功跳转Url
  30. bean.setSuccessUrl("/main");
  31. // 设置登录跳转Url
  32. bean.setLoginUrl("/toLogin");
  33. // 设置未授权提示Url
  34. bean.setUnauthorizedUrl("/error/unAuth");
  35. /** * anon:匿名用户可访问 * authc:认证用户可访问 * user:使用rememberMe可访问 * perms:对应权限可访问 * role:对应角色权限可访问 **/
  36. Map<String, String> filterMap = new LinkedHashMap<>();
  37. filterMap.put("/login","anon");
  38. filterMap.put("/user/index","authc");
  39. filterMap.put("/vip/index","roles[vip]");
  40. filterMap.put("/druid/**", "anon");
  41. filterMap.put("/static/**","anon");
  42. filterMap.put("/**","authc");
  43. filterMap.put("/logout", "logout");
  44. bean.setFilterChainDefinitionMap(filterMap);
  45. return bean;
  46. }
  47. /** * 注入 securityManager */
  48. @Bean(name="securityManager")
  49. public DefaultWebSecurityManager getDefaultWebSecurityManager(
  50. HashedCredentialsMatcher hashedCredentialsMatcher) {
  51. DefaultWebSecurityManager securityManager =
  52. new DefaultWebSecurityManager();
  53. // 关联realm.
  54. securityManager.setRealm(userRealm(hashedCredentialsMatcher));
  55. return securityManager;
  56. }
  57. }

4.4 realm

UserRealm.java

  1. package com.rhine.blog.realm;
  2. /** * 自定义Realm,实现授权与认证 */
  3. public class UserRealm extends AuthorizingRealm {
  4. @Autowired
  5. private UserService userService;
  6. /** * 用户授权 **/
  7. @Override
  8. protected AuthorizationInfo doGetAuthorizationInfo(
  9. PrincipalCollection principalCollection) {
  10. System.out.println("===执行授权===");
  11. Subject subject = SecurityUtils.getSubject();
  12. UserBean user = (UserBean)subject.getPrincipal();
  13. if(user != null){
  14. SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
  15. // 角色与权限字符串集合
  16. Collection<String> rolesCollection = new HashSet<>();
  17. Collection<String> premissionCollection = new HashSet<>();
  18. // 读取并赋值用户角色与权限
  19. Set<RoleBean> roles = user.getRole();
  20. for(RoleBean role : roles){
  21. rolesCollection.add(role.getName());
  22. Set<PermissionBean> permissions = role.getPermissions();
  23. for (PermissionBean permission : permissions){
  24. premissionCollection.add(permission.getUrl());
  25. }
  26. info.addStringPermissions(premissionCollection);
  27. }
  28. info.addRoles(rolesCollection);
  29. return info;
  30. }
  31. return null;
  32. }
  33. /** * 用户认证 **/
  34. @Override
  35. protected AuthenticationInfo doGetAuthenticationInfo(
  36. AuthenticationToken authenticationToken) throws AuthenticationException {
  37. System.out.println("===执行认证===");
  38. UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
  39. UserBean bean = userService.findByName(token.getUsername());
  40. if(bean == null){
  41. throw new UnknownAccountException();
  42. }
  43. ByteSource credentialsSalt = ByteSource.Util.bytes(bean.getName());
  44. return new SimpleAuthenticationInfo(bean, bean.getPassword(),
  45. credentialsSalt, getName());
  46. }
  47. // 模拟Shiro用户加密,假设用户密码为123456
  48. public static void main(String[] args){
  49. // 用户名
  50. String username = "rhine";
  51. // 用户密码
  52. String password = "123456";
  53. // 加密方式
  54. String hashAlgorithName = "MD5";
  55. // 加密次数
  56. int hashIterations = 1024;
  57. ByteSource credentialsSalt = ByteSource.Util.bytes(username);
  58. Object obj = new SimpleHash(hashAlgorithName, password,
  59. credentialsSalt, hashIterations);
  60. System.out.println(obj);
  61. }
  62. }

4.5 service

UserService.java

  1. package com.rhine.blog.service;
  2. /** * UserService抽象接口 */
  3. public interface UserService {
  4. UserBean findByName(String name);
  5. }

UserServiceImpl.java

  1. package com.rhine.blog.service.impl;
  2. /** * UserService实现类 */
  3. @Service
  4. public class UserServiceImpl implements UserService {
  5. @Autowired
  6. private UserMapper userMapper;
  7. @Override
  8. public UserBean findByName(String name) {
  9. // 查询用户是否存在
  10. UserBean bean = userMapper.findByName(name);
  11. if (bean != null) {
  12. // 查询用户信息、角色、权限
  13. bean = userMapper.findById(bean.getId());
  14. }
  15. return bean;
  16. }
  17. }

4.6 controller

MainController.java

  1. package com.rhine.blog.controller;
  2. /** * 用户登录、登出、错误页面跳转控制器 */
  3. @Controller
  4. public class MainController {
  5. @RequestMapping("/main")
  6. public String index(HttpServletRequest request, HttpServletResponse response){
  7. response.setHeader("root", request.getContextPath());
  8. return "index";
  9. }
  10. @RequestMapping("/toLogin")
  11. public String toLogin(HttpServletRequest request, HttpServletResponse response){
  12. response.setHeader("root", request.getContextPath());
  13. return "login";
  14. }
  15. @RequestMapping("/login")
  16. public String login(HttpServletRequest request, HttpServletResponse response){
  17. response.setHeader("root", request.getContextPath());
  18. String userName = request.getParameter("username");
  19. String password = request.getParameter("password");
  20. // 1.获取Subject
  21. Subject subject = SecurityUtils.getSubject();
  22. // 2.封装用户数据
  23. UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
  24. // 3.执行登录方法
  25. try{
  26. subject.login(token);
  27. return "redirect:/main";
  28. } catch (UnknownAccountException e){
  29. e.printStackTrace();
  30. request.setAttribute("msg","用户名不存在!");
  31. } catch (IncorrectCredentialsException e){
  32. request.setAttribute("msg","密码错误!");
  33. }
  34. return "login";
  35. }
  36. @RequestMapping("/logout")
  37. public String logout(){
  38. Subject subject = SecurityUtils.getSubject();
  39. if (subject != null) {
  40. subject.logout();
  41. }
  42. return "redirect:/main";
  43. }
  44. @RequestMapping("/error/unAuth")
  45. public String unAuth(){
  46. return "/error/unAuth";
  47. }
  48. }

UserController.java

  1. package com.rhine.blog.controller;
  2. /** * 用户页面跳转 */
  3. @Controller
  4. public class UserController {
  5. /** * 个人中心,需认证可访问 */
  6. @RequestMapping("/user/index")
  7. public String add(HttpServletRequest request){
  8. UserBean bean = (UserBean) SecurityUtils.getSubject().getPrincipal();
  9. request.setAttribute("userName", bean.getName());
  10. return "/user/index";
  11. }
  12. /** * 会员中心,需认证且角色为vip可访问 */
  13. @RequestMapping("/vip/index")
  14. public String update(){
  15. return "/vip/index";
  16. }
  17. }

5 页面

login.html——登录页面

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>登录</title>
  6. </head>
  7. <body>
  8. <h1>用户登录</h1>
  9. <hr>
  10. <form id="from" action="${root!}/login" method="post">
  11. <table>
  12. <tr>
  13. <td>用户名</td>
  14. <td>
  15. <input type="text" name="username" placeholder="请输入账户名"/>
  16. </td>
  17. </tr>
  18. <tr>
  19. <td>密码</td>
  20. <td>
  21. <input type="password" name="password" placeholder="请输入密码"/>
  22. </td>
  23. </tr>
  24. <tr>
  25. <td colspan="2">
  26. <font color="red">${msg!}</font>
  27. </td>
  28. </tr>
  29. <tr>
  30. <td colspan="2">
  31. <input type="submit" value="登录"/>
  32. <input type="reset" value="重置"/>
  33. </td>
  34. </tr>
  35. </table>
  36. </form>
  37. </body>
  38. </html>

index.html——首页

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>首页</title>
  5. </head>
  6. <body>
  7. <h1>首页</h1>
  8. <hr>
  9. <ul>
  10. <li><a href="user/index">个人中心</a></li>
  11. <li><a href="vip/index">会员中心</a></li>
  12. <li><a href="logout">退出登录</a></li>
  13. </ul>
  14. </body>
  15. </html>

/user/index.html——用户中心

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>用户中心</title>
  5. </head>
  6. <body>
  7. <h1>用户中心</h1>
  8. <hr>
  9. <h1>欢迎${userName!},这里是用户中心</h1>
  10. </body>
  11. </html>

/vip/index.html——会员中心

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>会员中心</title>
  5. </head>
  6. <body>
  7. <h1>会员中心</h1>
  8. <hr>
  9. <h1>欢迎来到<font color="red">会员中心</font></h1>
  10. </body>
  11. </html>

/error/unAuth.html——未授权提示页面

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>未授权提示</title>
  5. </head>
  6. <body>
  7. <h1>您还不是<font color="red">会员</font>,没有权限访问这个页面!</h1>
  8. </body>
  9. </html>

6 测试运行

6.1 普通用户登录

普通会员登录

6.2 会员用户登录

会员用户登录

发表评论

表情:
评论列表 (有 0 条评论,325人围观)

还没有评论,来说两句吧...

相关阅读