spring security +MySQL + BCryptPasswordEncoder 单向加密验证 + 权限拦截 --- 心得

1.前言

前面学习了 security的登录与登出 , 但是用户信息 是 application 配置 或内存直接注入进去的 ,不具有实用性,实际上的使用还需要权限管理,有些 访问接口需要某些权限才可以使用

于是多了个权限管理的问题

2.环境

spring boot 2.1.6.RELEASE

mysql 5.5.28*win64

jdk 1.8.0_221

3.操作

(1)准备一张MySQL表

CREATE TABLE `t_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键,自递增',
  `username` varchar(20) DEFAULT NULL COMMENT '用户名',
  `psw` varchar(140) DEFAULT NULL COMMENT '密码',
  `nickname` varchar(50) DEFAULT NULL COMMENT '别名',
  `role` varchar(100) DEFAULT NULL COMMENT '权限名',
  `setTime` datetime DEFAULT NULL COMMENT '注册时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;

 

 

 

 (2)目录结构

(3)pom.xml

 

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>security-5500</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security-5500</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <!-- 设置项目编码格式-->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--spring security 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--访问静态资源-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- MySQL 依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <!--            <scope>runtime</scope>-->
            <version>5.1.30</version>
        </dependency>
        <!--MySQL 数据源 依赖包-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>

        <!--        mybatis依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
        <!-- mybatis的逆向工程依赖包-->
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- SCryptPasswordEncoder 加密才需要使用-->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
            <version>1.64</version>
        </dependency>
        <!--java工具包-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
View Code

 

(4)配置mybatis 与 dao层接口【具体操作这里不演示,可看我的其他随笔有具体讲解】

 

 

 (5)配置前端页面

index.html

<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
你好 ,世界 ,2333
<p>点击 <a th:href="@{/home}">我</a> 去home.html页面</p>

</body>
</html>
View Code

 

home.html

<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <meta charset="UTF-8">
    <title>security首页</title>
</head>
<body>
<h1>Welcome!你好,世界</h1>

<p>Click <a th:href="@{/hai}">here</a> to see a greeting.</p>
</body>
</html>
View Code

 

hai.html

<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <meta charset="UTF-8">
    <title>hai文件</title>
</head>
<body>
    你好呀世界,成功登录进来了
<br>
<hr>
用户名:<span th:text="${username}"></span>
<hr>
<!--  登出 路径是在security 拦截规则 那 设置的   ,当然也可以使用自己写的 ,必须post方式才可以访问,因为默认开启了CSRF -->
    <form th:action="@{/mylogout}" method="post">
        <button class="btn btn-danger" style="margin-top: 20px">退出登录</button>
    </form>
</body>
</html>
View Code

 

kk.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>kk</title>
</head>
<body>
<img src="img/xx.png" alt="">
</body>
</html>
View Code

 

login.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>Spring Security自定义</title>
</head>
<body>
<div th:if="${param.error}">
    Invalid username and password.
</div>
<div th:if="${param.logout}">
    You have been logged out.
</div>
<form th:action="@{/login}" method="post">
    <div><label> User Name : <input type="text" name="username"/> </label></div>
    <div><label> Password: <input type="password" name="password"/> </label></div>
    <div><input type="submit" value="Sign In"/></div>
</form>
<br>
lalallalalal啊是德国海
</body>
</html>
View Code

 

(6)配置controller 虚拟路径 【访问接口】

package com.example.security5500.controller;


import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import java.security.Principal;

@Controller
public class MVCController {

    @RequestMapping("/home")
    public String home() {
        return "home";
    }


    @RequestMapping("/login")
    public String login(){
        return "login";
    }


    @RequestMapping("/hai")
    public String hai(@AuthenticationPrincipal Principal principal, Model model) {
        //获取登录用户名信息 ,如果没有登录  principal.getName() 会报异常,因此弄个异常抛出
        String  s= "r";
        try {
            if (principal.getName() !=null){
                s = principal.getName();
            }
        }catch (Exception e){
            System.out.println("principal.getName()出异常");
        }

        model.addAttribute("username", s);
        return "hai";
    }

    @RequestMapping({"/", "/index"})
    public String index() {
        return "index";
    }

    @RequestMapping("kk")
    public String kk() {
        return "kk";
    }


    //获取用户权限
    @RequestMapping({"/info"})
    @ResponseBody
    public Object info(@AuthenticationPrincipal Principal principal) {
        return principal;
    }
    /*
    {"authorities":[{"authority":"admin"},{"authority":"user"}],
    "details":{"remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"1F57B8E39C5D1DB1F875D57D533DB982"},
    "authenticated":true,"principal":{"password":null,"username":"xi","authorities":[{"authority":"admin"},
    {"authority":"user"}],"accountNonExpired":true,"accountNonLocked":true,
    "credentialsNonExpired":true,"enabled":true},"credentials":null,"name":"xi"}

     */


}
View Code

 

package com.example.security5500.controller;


import com.example.security5500.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.security.Principal;
import java.util.Map;

@Controller
@RequestMapping("/admin")
public class UserController {

    @Autowired
    private UserService userService;

//    //登出操作
//    @RequestMapping({"/lo"})
//    public String logout(HttpServletRequest request, HttpServletResponse response) {
//        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
//        if (auth != null) {//清除认证
//            new SecurityContextLogoutHandler().logout(request, response, auth);
//        }
//        //重定向到指定页面
//        return "redirect:/login";
//    }


    //添加用户
    @RequestMapping({"/addUser"})
    @ResponseBody
    public Map<String,Object> addUser(String username , String psw ) {
        return userService.addUser(username,psw);
    }


}
View Code

 

 (7)service层实现类

package com.example.security5500.service.serviceImpl;

import com.example.security5500.dao.TUserMapper;
import com.example.security5500.entitis.tables.TUser;
import com.example.security5500.service.UserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;


@Service
public class UserServiceImpl implements UserService {

    @Resource
    private TUserMapper tUserMapper;
    
    //根据用户名获取用户信息
    @Override
    public TUser getByUsername(String useranme) {
        return tUserMapper.selectByUsername(useranme);
    }

    //添加新用户
    @Override
    public Map<String,Object> addUser(String username, String psw) {
        Map<String,Object> map = new HashMap<>();
        if (StringUtils.isBlank(username) || StringUtils.isBlank(psw))
        {
            map.put("data","参数不可空");
            return map;
        }

        ////根据用户名获取用户信息
        TUser u = tUserMapper.selectByUsername(username);
        if (u!= null){
            map.put("data","用户名已经存在");
            return map;
        }
        //
        TUser tUser = new TUser();
        tUser.setUsername(username);
        //
        //BCryptPasswordEncoder 单向加密
        tUser.setPsw((new BCryptPasswordEncoder()).encode(psw));
        //
        tUser.setNickname("别名-昵称");
        tUser.setRole("user");
        tUser.setSettime(new Date());
        int len = tUserMapper.insertSelective(tUser);
        if (len!=1){
            map.put("data","失败");
        }else {
            map.put("data","成功");
        }
        return map;
    }
}
View Code

 

(8)启动类

package com.example.security5500;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@SpringBootApplication
//设置mapper接口包位置
@MapperScan(basePackages = "com.example.security5500.dao")
public class Security5500Application {

    public static void main(String[] args) {
        SpringApplication.run(Security5500Application.class, args);
    }


}
View Code

 

 

(9)security配置类 ,继承了 WebSecurityConfigurerAdapter  ,重写了父类方法 ,可对访问路径自定义设置拦截规则

 

package com.example.security5500.securityConfig;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

//这个加不加无所谓
//@Configuration
//开启security自定义配置
@EnableWebSecurity
//开启 Controller层的访问方法权限,与注解@PreAuthorize("hasRole('admin')")配合,但是 经测试,无法使用,前端访问指定接口报错403 ,
//@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //实例自定义登录校验接口 【内部有 数据库查询】
    @Autowired
    private DbUserDetailsService dbUserDetailsService;

    //拦截规则设置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //允许基于使用HttpServletRequest限制访问
                .authorizeRequests()
                //设置不拦截页面,可直接通过,路径访问 "/", "/index", "/home" 则不拦截,
                
                .antMatchers("/", "/index", "/home", "/hhk/**")
                //是允许所有的意思
                .permitAll()
                //访问 /hai 需要admin权限 ,无权限则提示 403
                .antMatchers("/hai").hasAuthority("admin")
                //访问 /kk 需要admin或user权限 ,无权限则提示 403
                .antMatchers("/kk").hasAnyAuthority("admin","user")
                //路径/admin/**所有的请求都需要admin权限 ,无权限则提示 403
                .antMatchers("/admin/**").hasAuthority("admin")
                //其他页面都要拦截,【需要在最后设置这个】
                .anyRequest().authenticated()
                .and()
                //设置自定义登录页面
                .formLogin()
                //指定自定义登录页面的访问虚拟路径
                .loginPage("/login")
                .permitAll()
                .and()
//        添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效
//        来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success”
                .logout()
//                //指定的登出操作的虚拟路径,需要以post方式请求这个 http://localhost:5500/mylogout 才可以登出 ,也可以直接清除用户认证信息达到登出目的
                .logoutUrl("/mylogout")
                //登出成功后访问的地址
                .logoutSuccessUrl("/home");
    }


    /**
     * 添加 UserDetailsService, 实现自定义登录校验,数据库查询
     */
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        //注入用户信息,每次登录都会来这查询一次信息,因此不建议每次都向mysql查询,应该使用redis
        //密码加密
        builder.userDetailsService(dbUserDetailsService).passwordEncoder(passwordEncoder());
    }

    /**
     * BCryptPasswordEncoder相关知识:
     * 用户表的密码通常使用MD5等不可逆算法加密后存储,为防止彩虹表破解更会先使用一个特定的字符串(如域名)加密,然后再使用一个随机的salt(盐值)加密。
     * 特定字符串是程序代码中固定的,salt是每个密码单独随机,一般给用户表加一个字段单独存储,比较麻烦。
     * BCrypt算法将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题。
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


//    /**
//     * 选择加密方式 ,密码不加密的时候选择 NoOpPasswordEncoder,不可缺少,否则报错
//     * java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
//     */
//    @Bean
//    public static PasswordEncoder passwordEncoder() {
//        return NoOpPasswordEncoder.getInstance();
//    }


}
View Code

 

 

(10)实现自定义登录校验,实现了根据用户名去数据库查询用户信息,集齐参数用户名、加密后的密码、权限 , 

然后使用 new org.springframework.security.core.userdetails.User(tUser.getUsername(), tUser.getPsw(), simpleGrantedAuthorities); 注册登录用户 ,

然后内部会自动对比密码 进行校验 【使用 BCryptPasswordEncoder 单项加密】

 

package com.example.security5500.securityConfig;


import com.example.security5500.entitis.tables.TUser;
import com.example.security5500.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class DbUserDetailsService implements UserDetailsService {

   @Autowired
   private UserService userService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名查询用户信息
        TUser tUser = userService.getByUsername(username);
        if (tUser == null){
            throw new UsernameNotFoundException("用户不存在!");
        }
        //权限设置
//        List<GrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
        List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
        String role = tUser.getRole();
        //分割权限名称,如 user,admin
        String[] roles = role.split(",");
        System.out.println("添加权限");
        for (String r :roles){
            System.out.println(r);
            //添加权限
            simpleGrantedAuthorities.add(new SimpleGrantedAuthority(r));
        }

//        simpleGrantedAuthorities.add(new SimpleGrantedAuthority("USER"));
        /**
         * 创建一个用于认证的用户对象并返回,包括:用户名,密码,角色
         */
        //输入参数
        return new org.springframework.security.core.userdetails.User(tUser.getUsername(), tUser.getPsw(), simpleGrantedAuthorities);
    }

}
View Code

 

 

(11)application.properties

spring.application.name=security-5500
# 应用服务web访问端口
server.port=5500
#配置security登录账户密和密码  ,不配置则默认账户是user,密码是随机生成的字符串,打印在启动栏中
#spring.security.user.name=11
#spring.security.user.password=22
#
##
##
##
## Enable template caching.
#spring.thymeleaf.cache=true
## Check that the templates location exists.
#spring.thymeleaf.check-template-location=true
## Content-Type value.
##spring.thymeleaf.content-type=text/html
## Enable MVC Thymeleaf view resolution.
#spring.thymeleaf.enabled=true
## Template encoding.
#spring.thymeleaf.encoding=utf-8
## Comma-separated list of view names that should be excluded from resolution.
#spring.thymeleaf.excluded-view-names=
## Template mode to be applied to templates. See also StandardTemplateModeHandlers.
#spring.thymeleaf.mode=HTML5
## Prefix that gets prepended to view names when building a URL.
##设置html文件位置
#spring.thymeleaf.prefix=classpath:/templates/
## Suffix that gets appended to view names when building a URL.
#spring.thymeleaf.suffix=.html  spring.thymeleaf.template-resolver-order=
# Order of the template resolver in the chain. spring.thymeleaf.view-names= # Comma-separated list of view names that can be resolved.
#
#
#设置mybatis
#mybatis设置
#mybatis配置文件所在路径
mybatis.config-location=classpath:mybatis/config/mybatisConfig.xml
#所有Entity别名类所在包
mybatis.type-aliases-package=com.example.security5500.entitis.tables
#mapper映射xml文件[也可以放在 resources 里面]
#不论放在哪里,都必须使用classpath: 否则找不到 ,报错 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found):
mybatis.mapper-locations= classpath:mybatis/mapper/**/*.xml


#mysql配置
# 当前数据源操作类型
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# mysql驱动包
spring.datasource.driver-class-name=org.gjt.mm.mysql.Driver
# 数据库名称
spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=utf-8
# 数据库账户名
spring.datasource.username=root
# 数据库密码
spring.datasource.password=mysql
#
#
# 数据库连接池的最小维持连接数
spring.datasource.dbcp2.min-idle=5
# 初始化连接数
spring.datasource.dbcp2.initial-size=5
# 最大连接数
spring.datasource.dbcp2.max-total=5
# 等待连接获取的最大超时时间
spring.datasource.dbcp2.max-wait-millis=200
#
# 指明是否在从池中取出连接前进行检验,如果检验失败, 则从池中去除连接并尝试取出另一个,
#注意: 设置为true后如果要生效,validationQuery参数必须设置为非空字符串
spring.datasource.druid.test-on-borrow=false
#
# 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除.
#注意: 设置为true后如果要生效,validationQuery参数必须设置为非空字符串
spring.datasource.druid.test-while-idle=true
#
# 指明是否在归还到池中前进行检验,注意: 设置为true后如果要生效,
#validationQuery参数必须设置为非空字符串
spring.datasource.druid.test-on-return=false
#
# SQL查询,用来验证从连接池取出的连接,在将连接返回给调用者之前.
#如果指定,则查询必须是一个SQL SELECT并且必须返回至少一行记录
spring.datasource.druid.validation-query=select 1
View Code

 

 

 4.测试

 (1)启动 默认进入 index.html

 

 

 点击 “我” ,进入 home.html

 

 

点击 “here”  ,进入 hai.html ,但是因为设置了拦截,需要登录才可以访问 ,因此进入了自定义的登录页面

 

 

用一个只有 user权限的账户  

username = cen 

password = 11

登录后显示 403

 

 因为我将访问 hai.html的权限设为需要 admin 才可以访问 ,因此拒绝操作

 

 

 换一个有admin权限的账户

 

username = xi

password = 11

访问网址http://localhost:5500/login

再次登录

 

 

 

 

 这是对一个终端访问接口的权限拦截

 

那么,需要将某一路径的请求都给拦截怎么办?难道一个一个写?

不,可以拦截上一层的虚拟路径

 

 

 security的的配置写法

 

 

(2)一个拦截路径可以设置多个权限,只要有任意一个权限都可以访问

 

 网址访问 http://localhost:5500/kk  ,【无权限仍然提示403】

 

 

posted @ 2020-06-02 23:15  岑惜  阅读(964)  评论(0编辑  收藏  举报