好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

Spring Boot整合Spring Security简单实现登入登出从零搭建教程

前言

spring security是一个能够为基于spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在spring应用上下文中配置的bean,充分利用了spring ioc,di(控制反转inversion of control ,di:dependency injection 依赖注入)和aop(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

本文主要给大家介绍了关于spring boot 整合 spring security实现登入登出的相关内容,下面话不多说了,来一起看看详细的介绍吧

技术栈 : springboot + springsecurity + jpa + freemark ,完整项目地址 : https://github.com/ealenxie/spring-security-login

方法如下:

1 . 新建一个spring-security-login的maven项目 ,pom.xml添加基本依赖 :

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd" >

  <modelversion> 4.0 . 0 </modelversion>

 

  <groupid>com.wuxicloud</groupid>

  <artifactid>spring-security-login</artifactid>

  <version> 1.0 </version>

  <parent>

  <groupid>org.springframework.boot</groupid>

  <artifactid>spring-boot-starter-parent</artifactid>

  <version> 1.5 . 6 .release</version>

  </parent>

  <properties>

  <author>ealenxie</author>

  <description>springboot整合springsecurity实现简单登入登出</description>

  </properties>

 

  <dependencies>

  <dependency>

  <groupid>org.springframework.boot</groupid>

  <artifactid>spring-boot-starter</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-data-jpa</artifactid>

  </dependency>

  <dependency>

  <groupid>org.springframework.boot</groupid>

  <artifactid>spring-boot-starter-test</artifactid>

  </dependency>

  <dependency>

  <groupid>org.springframework.boot</groupid>

  <artifactid>spring-boot-starter-security</artifactid>

  </dependency>

  <dependency>

  <groupid>org.springframework.boot</groupid>

  <artifactid>spring-boot-starter-freemarker</artifactid>

  </dependency>

  <dependency>

  <groupid>org.springframework.boot</groupid>

  <artifactid>spring-boot-starter-aop</artifactid>

  </dependency>

  <!--alibaba-->

  <dependency>

  <groupid>com.alibaba</groupid>

  <artifactid>druid</artifactid>

  <version> 1.0 . 24 </version>

  </dependency>

  <dependency>

  <groupid>com.alibaba</groupid>

  <artifactid>fastjson</artifactid>

  <version> 1.2 . 31 </version>

  </dependency>

  <dependency>

  <groupid>mysql</groupid>

  <artifactid>mysql-connector-java</artifactid>

  <scope>runtime</scope>

  </dependency>

  </dependencies>

</project>

2 . 准备你的数据库,设计表结构,要用户使用登入登出,新建用户表。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

drop table if exists `user`;

create table `user` (

  `id` int ( 11 ) not null auto_increment,

  `user_uuid` varchar( 70 ) character set utf8 collate utf8_general_ci default null ,

  `username` varchar( 255 ) character set utf8 collate utf8_general_ci default null ,

  `password` varchar( 255 ) character set utf8 collate utf8_general_ci default null ,

  `email` varchar( 255 ) character set utf8 collate utf8_general_ci default null ,

  `telephone` varchar( 255 ) character set utf8 collate utf8_general_ci default null ,

  `role` int ( 10 ) default null ,

  `image` varchar( 255 ) character set utf8 collate utf8_general_ci default null ,

  `last_ip` varchar( 255 ) character set utf8 collate utf8_general_ci default null ,

  `last_time` varchar( 255 ) character set utf8 collate utf8_general_ci default null ,

  primary key (`id`) using btree

) engine = innodb auto_increment = 2 character set = utf8 collate = utf8_general_ci row_format = compact;

 

set foreign_key_checks = 1 ;

3 . 用户对象user.java :

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

import javax.persistence.*;

 

/**

  * created by ealenxie on 2018/7/5 15:17

  */

@entity

@table (name = "user" )

public class user {

  @id

  @generatedvalue (strategy = generationtype.auto)

  private integer id;

  private string user_uuid; //用户uuid

  private string username; //用户名

  private string password; //用户密码

  private string email; //用户邮箱

  private string telephone; //电话号码

  private string role; //用户角色

  private string image; //用户头像

  private string last_ip; //上次登录ip

  private string last_time; //上次登录时间

 

  public integer getid() {

  return id;

  }

 

  public string getrole() {

  return role;

  }

 

  public void setrole(string role) {

  this .role = role;

  }

 

  public string getimage() {

  return image;

  }

 

  public void setimage(string image) {

  this .image = image;

  }

 

  public void setid(integer id) {

  this .id = id;

  }

 

 

  public string getusername() {

  return username;

  }

 

  public void setusername(string username) {

  this .username = username;

  }

 

  public string getemail() {

  return email;

  }

 

  public void setemail(string email) {

  this .email = email;

  }

 

  public string gettelephone() {

  return telephone;

  }

 

  public void settelephone(string telephone) {

  this .telephone = telephone;

  }

 

  public string getpassword() {

  return password;

  }

 

  public void setpassword(string password) {

  this .password = password;

  }

 

  public string getuser_uuid() {

  return user_uuid;

  }

 

  public void setuser_uuid(string user_uuid) {

  this .user_uuid = user_uuid;

  }

 

  public string getlast_ip() {

  return last_ip;

  }

 

  public void setlast_ip(string last_ip) {

  this .last_ip = last_ip;

  }

 

  public string getlast_time() {

  return last_time;

  }

 

  public void setlast_time(string last_time) {

  this .last_time = last_time;

  }

 

  @override

  public string tostring() {

  return "user{" +

  "id=" + id +

  ", user_uuid='" + user_uuid + '\ '' +

  ", username='" + username + '\ '' +

  ", password='" + password + '\ '' +

  ", email='" + email + '\ '' +

  ", telephone='" + telephone + '\ '' +

  ", role='" + role + '\ '' +

  ", image='" + image + '\ '' +

  ", last_ip='" + last_ip + '\ '' +

  ", last_time='" + last_time + '\ '' +

  '}' ;

  }

}

 4 . application.yml配置一些基本属性

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

spring:

  resources:

  static -locations: classpath:/

  freemarker:

  template-loader-path: classpath:/templates/

  suffix: .html

  content-type: text/html

  charset: utf- 8

  datasource:

  url: jdbc:mysql: //localhost:3306/yourdatabase

  username: yourname

  password: yourpass

  driver- class -name: com.mysql.jdbc.driver

  type: com.alibaba.druid.pool.druiddatasource

server:

  port: 8083

  error:

  whitelabel:

  enabled: true

5 . 考虑我们应用的效率 , 可以配置数据源和线程池 :

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

package com.wuxicloud.config;

 

import com.alibaba.druid.pool.druiddatasource;

import com.alibaba.druid.pool.druiddatasourcefactory;

import com.alibaba.druid.support.http.statviewservlet;

import com.alibaba.druid.support.http.webstatfilter;

import org.springframework.beans.factory.annotation.autowired;

import org.springframework.boot.context.properties.configurationproperties;

import org.springframework.boot.web.servlet.filterregistrationbean;

import org.springframework.boot.web.servlet.servletregistrationbean;

import org.springframework.context.annotation.bean;

import org.springframework.context.annotation.configuration;

import org.springframework.core.env.*;

 

import javax.sql.datasource;

import java.util.hashmap;

import java.util.map;

import java.util.properties;

 

@configuration

public class druidconfig {

  private static final string db_prefix = "spring.datasource." ;

 

  @autowired

  private environment environment;

 

  @bean

  @configurationproperties (prefix = db_prefix)

  public datasource druiddatasource() {

  properties dbproperties = new properties();

  map<string, object> map = new hashmap<>();

  for (propertysource<?> propertysource : ((abstractenvironment) environment).getpropertysources()) {

  getpropertiesfromsource(propertysource, map);

  }

  dbproperties.putall(map);

  druiddatasource dds;

  try {

  dds = (druiddatasource) druiddatasourcefactory.createdatasource(dbproperties);

  dds.init();

  } catch (exception e) {

  throw new runtimeexception( "load datasource error, dbproperties is :" + dbproperties, e);

  }

  return dds;

  }

 

  private void getpropertiesfromsource(propertysource<?> propertysource, map<string, object> map) {

  if (propertysource instanceof mappropertysource) {

  for (string key : ((mappropertysource) propertysource).getpropertynames()) {

  if (key.startswith(db_prefix))

   map.put(key.replacefirst(db_prefix, "" ), propertysource.getproperty(key));

  else if (key.startswith(db_prefix))

   map.put(key.replacefirst(db_prefix, "" ), propertysource.getproperty(key));

  }

  }

 

  if (propertysource instanceof compositepropertysource) {

  for (propertysource<?> s : ((compositepropertysource) propertysource).getpropertysources()) {

  getpropertiesfromsource(s, map);

  }

  }

  }

 

  @bean

  public servletregistrationbean druidservlet() {

  return new servletregistrationbean( new statviewservlet(), "/druid/*" );

  }

 

  @bean

  public filterregistrationbean filterregistrationbean() {

  filterregistrationbean filterregistrationbean = new filterregistrationbean();

  filterregistrationbean.setfilter( new webstatfilter());

  filterregistrationbean.addurlpatterns( "/*" );

  filterregistrationbean.addinitparameter( "exclusions" , "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" );

  return filterregistrationbean;

  }

}

配置线程池 :

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

package com.wuxicloud.config;

 

import org.springframework.context.annotation.bean;

import org.springframework.context.annotation.configuration;

import org.springframework.scheduling.annotation.enableasync;

import org.springframework.scheduling.concurrent.threadpooltaskexecutor;

 

import java.util.concurrent.executor;

import java.util.concurrent.threadpoolexecutor;

 

@configuration

@enableasync

public class threadpoolconfig {

  @bean

  public executor getexecutor() {

  threadpooltaskexecutor executor = new threadpooltaskexecutor();

  executor.setcorepoolsize( 5 ); //线程池维护线程的最少数量

  executor.setmaxpoolsize( 30 ); //线程池维护线程的最大数量

  executor.setqueuecapacity( 8 ); //缓存队列

  executor.setrejectedexecutionhandler( new threadpoolexecutor.callerrunspolicy()); //对拒绝task的处理策略

  executor.setkeepaliveseconds( 60 ); //允许的空闲时间

  executor.initialize();

  return executor;

  }

}

6.用户需要根据用户名进行登录,访问数据库 :

?

1

2

3

4

5

6

7

8

9

10

11

import com.wuxicloud.model.user;

import org.springframework.data.jpa.repository.jparepository;

 

/**

  * created by ealenxie on 2018/7/11 14:23

  */

public interface userrepository extends jparepository<user, integer> {

 

  user findbyusername(string username);

 

}

7.构建真正用于springsecurity登录的安全用户(userdetails),我这里使用新建了一个pojo来实现 :

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

package com.wuxicloud.security;

 

import com.wuxicloud.model.user;

import org.springframework.security.core.grantedauthority;

import org.springframework.security.core.authority.simplegrantedauthority;

import org.springframework.security.core.userdetails.userdetails;

 

import java.util.arraylist;

import java.util.collection;

 

public class securityuser extends user implements userdetails {

  private static final long serialversionuid = 1l;

 

  public securityuser(user user) {

  if (user != null ) {

  this .setuser_uuid(user.getuser_uuid());

  this .setusername(user.getusername());

  this .setpassword(user.getpassword());

  this .setemail(user.getemail());

  this .settelephone(user.gettelephone());

  this .setrole(user.getrole());

  this .setimage(user.getimage());

  this .setlast_ip(user.getlast_ip());

  this .setlast_time(user.getlast_time());

  }

  }

 

  @override

  public collection<? extends grantedauthority> getauthorities() {

  collection<grantedauthority> authorities = new arraylist<>();

  string username = this .getusername();

  if (username != null ) {

  simplegrantedauthority authority = new simplegrantedauthority(username);

  authorities.add(authority);

  }

  return authorities;

  }

 

  @override

  public boolean isaccountnonexpired() {

  return true ;

  }

 

  @override

  public boolean isaccountnonlocked() {

  return true ;

  }

 

  @override

  public boolean iscredentialsnonexpired() {

  return true ;

  }

 

  @override

  public boolean isenabled() {

  return true ;

  }

}

8 . 核心配置,配置springsecurity访问策略,包括登录处理,登出处理,资源访问,密码基本加密。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

package com.wuxicloud.config;

 

import com.wuxicloud.dao.userrepository;

import com.wuxicloud.model.user;

import com.wuxicloud.security.securityuser;

import org.slf4j.logger;

import org.slf4j.loggerfactory;

import org.springframework.beans.factory.annotation.autowired;

import org.springframework.context.annotation.bean;

import org.springframework.context.annotation.configuration;

import org.springframework.security.config.annotation.authentication.builders.authenticationmanagerbuilder;

import org.springframework.security.config.annotation.web.builders.httpsecurity;

import org.springframework.security.config.annotation.web.configuration.enablewebsecurity;

import org.springframework.security.config.annotation.web.configuration.websecurityconfigureradapter;

import org.springframework.security.core.authentication;

import org.springframework.security.core.userdetails.userdetails;

import org.springframework.security.core.userdetails.userdetailsservice;

import org.springframework.security.core.userdetails.usernamenotfoundexception;

import org.springframework.security.crypto.bcrypt.bcryptpasswordencoder;

import org.springframework.security.web.authentication.savedrequestawareauthenticationsuccesshandler;

import org.springframework.security.web.authentication.logout.logoutsuccesshandler;

 

import javax.servlet.servletexception;

import javax.servlet.http.httpservletrequest;

import javax.servlet.http.httpservletresponse;

import java.io.ioexception;

 

/**

  * created by ealenxie on 2018/1/11.

  */

@configuration

@enablewebsecurity

public class websecurityconfig extends websecurityconfigureradapter {

  private static final logger logger = loggerfactory.getlogger(websecurityconfig. class );

 

  @override

  protected void configure(httpsecurity http) throws exception { //配置策略

  http.csrf().disable();

  http.authorizerequests().

  antmatchers( "/static/**" ).permitall().anyrequest().authenticated().

  and().formlogin().loginpage( "/login" ).permitall().successhandler(loginsuccesshandler()).

  and().logout().permitall().invalidatehttpsession( true ).

  deletecookies( "jsessionid" ).logoutsuccesshandler(logoutsuccesshandler()).

  and().sessionmanagement().maximumsessions( 10 ).expiredurl( "/login" );

  }

 

  @autowired

  public void configureglobal(authenticationmanagerbuilder auth) throws exception {

  auth.userdetailsservice(userdetailsservice()).passwordencoder(passwordencoder());

  auth.erasecredentials( false );

  }

 

  @bean

  public bcryptpasswordencoder passwordencoder() { //密码加密

  return new bcryptpasswordencoder( 4 );

  }

 

  @bean

  public logoutsuccesshandler logoutsuccesshandler() { //登出处理

  return new logoutsuccesshandler() {

  @override

  public void onlogoutsuccess(httpservletrequest httpservletrequest, httpservletresponse httpservletresponse, authentication authentication) throws ioexception, servletexception {

  try {

   securityuser user = (securityuser) authentication.getprincipal();

   logger.info( "user : " + user.getusername() + " logout success ! " );

  } catch (exception e) {

   logger.info( "logout exception , e : " + e.getmessage());

  }

  httpservletresponse.sendredirect( "/login" );

  }

  };

  }

 

  @bean

  public savedrequestawareauthenticationsuccesshandler loginsuccesshandler() { //登入处理

  return new savedrequestawareauthenticationsuccesshandler() {

  @override

  public void onauthenticationsuccess(httpservletrequest request, httpservletresponse response, authentication authentication) throws ioexception, servletexception {

  user userdetails = (user) authentication.getprincipal();

  logger.info( "user : " + userdetails.getusername() + " login success ! " );

  super .onauthenticationsuccess(request, response, authentication);

  }

  };

  }

  @bean

  public userdetailsservice userdetailsservice() { //用户登录实现

  return new userdetailsservice() {

  @autowired

  private userrepository userrepository;

 

  @override

  public userdetails loaduserbyusername(string s) throws usernamenotfoundexception {

  user user = userrepository.findbyusername(s);

  if (user == null ) throw new usernamenotfoundexception( "username " + s + " not found" );

  return new securityuser(user);

  }

  };

  }

}

9.至此,已经基本将配置搭建好了,从上面核心可以看出,配置的登录页的url 为/login,可以创建基本的controller来验证登录了。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

package com.wuxicloud.web;

 

import com.wuxicloud.model.user;

import org.springframework.security.core.authentication;

import org.springframework.security.core.context.securitycontext;

import org.springframework.security.core.context.securitycontextholder;

import org.springframework.security.core.userdetails.userdetails;

import org.springframework.stereotype.controller;

import org.springframework.web.bind.annotation.requestmapping;

import org.springframework.web.bind.annotation.requestmethod;

import org.springframework.web.context.request.requestcontextholder;

import org.springframework.web.context.request.servletrequestattributes;

 

import javax.servlet.http.httpservletrequest;

 

/**

  * created by ealenxie on 2018/1/11.

  */

@controller

public class logincontroller {

 

  @requestmapping (value = "/login" , method = requestmethod.get)

  public string login() {

  return "login" ;

  }

 

  @requestmapping ( "/" )

  public string root() {

  return "index" ;

  }

 

  public user getuser() { //为了session从获取用户信息,可以配置如下

  user user = new user();

  securitycontext ctx = securitycontextholder.getcontext();

  authentication auth = ctx.getauthentication();

  if (auth.getprincipal() instanceof userdetails) user = (user) auth.getprincipal();

  return user;

  }

 

  public httpservletrequest getrequest() {

  return ((servletrequestattributes) requestcontextholder.getrequestattributes()).getrequest();

  }

}

11 . springboot基本的启动类 application.class

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

package com.wuxicloud;

 

import org.springframework.boot.springapplication;

import org.springframework.boot.autoconfigure.springbootapplication;

 

/**

  * created by ealenxie on 2018/7/11 15:01

  */

@springbootapplication

public class application {

 

  public static void main(string[] args) {

  springapplication.run(application. class , args);

  }

}

11.根据freemark和controller里面可看出配置的视图为 /templates/index.html和/templates/index.login。所以创建基本的登录页面和登录成功页面。

login.html

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

<!doctype html>

<html lang= "en" >

<head>

  <meta charset= "utf-8" >

  <title>用户登录</title>

</head>

<body>

<form action= "/login" method= "post" >

  用户名 : <input type= "text" name= "username" />

  密码 : <input type= "password" name= "password" />

  <input type= "submit" value= "登录" >

</form>

</body>

</html>

注意 : 这里方法必须是post,因为get在controller被重写了,用户名的name属性必须是username,密码的name属性必须是password

index.html

?

1

2

3

4

5

6

7

8

9

10

11

12

<!doctype html>

<html lang= "en" >

<head>

  <meta charset= "utf-8" >

  <title>首页</title>

  <#assign user=session.spring_security_context.authentication.principal/>

</head>

<body>

欢迎你,${user.username}<br/>

<a href= "/logout" >注销</a>

</body>

</html>

注意 : 为了从session中获取到登录的用户信息,根据配置springsecurity的用户信息会放在session.spring_security_context.authentication.principal里面,根据freemarker模板引擎的特点,可以通过这种方式进行获取 : <#assign user=session.spring_security_context.authentication.principal/>

12 . 为了方便测试,我们在数据库中插入一条记录,注意,从websecurity.java配置可以知道密码会被加密,所以我们插入的用户密码应该是被加密的。

这里假如我们使用的密码为admin,则加密过后的字符串是 $2a$04$1oiua3yechbxqbji8jamyukznlwzwvfeqjkahnwaeqwnacjt6ukqu

 测试类如下 :

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

package com.wuxicloud.security;

 

import org.junit.test;

import org.springframework.security.crypto.bcrypt.bcryptpasswordencoder;

 

/**

  * created by ealenxie on 2018/7/11 15:13

  */

public class testencoder {

 

  @test

  public void encoder() {

  string password = "admin" ;

  bcryptpasswordencoder encoder = new bcryptpasswordencoder( 4 );

  string enpassword = encoder.encode(password);

  system.out.println(enpassword);

  }

}

测试登录,从上面的加密的密码我们插入一条数据到数据库中。

?

1

insert into `user` values ( 1 , 'd242ae49-4734-411e-8c8d-d2b09e87c3c8' , 'ealenxie' , '$2a$04$petexpgclkfdln4tyfxk0u8ryazmzdhlaswlx/xxm8hgqar1c892w' , 'sssss' , 'ssssssssss' , 1 , 'g' , '0:0:0:0:0:0:0:1' , '2018-07-11 11:26:27' );

13 . 启动项目进行测试 ,访问 localhost:8083

 

点击登录,登录失败会留在当前页面重新登录,成功则进入index.html

 登录如果成功,可以看到后台打印登录成功的日志 :

 页面进入index.html :

点击注销 ,则回重新跳转到login.html,后台也会打印登出成功的日志 :

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。

原文链接:https://www.cnblogs.com/ealenxie/p/9293768.html

查看更多关于Spring Boot整合Spring Security简单实现登入登出从零搭建教程的详细内容...

  阅读:51次