java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Security二次认证

Spring Security添加二次认证的项目实践

作者:格林希尔

在用户自动登录后,可以通过对密码进行二次校验进而确保用户的真实性,本文就来介绍一下Spring Security添加二次认证的项目实践,具有一定的参考价值,感兴趣的可以了解一下

一、 简介

1 Spring Security概述

Spring Security是一个基于Spring框架的安全框架,用于为Java应用程序提供身份验证和授权服务。

2 二次认证的必要性

传统的用户名和密码验证方式存在被破解的风险,因此在用户登录后需要进行二次认证,增强身份验证的安全性。

二、 Spring Security实现二次认证的方式

1 使用已有二次认证服务

1.1 集成Google Authenticator

Google Authenticator是一款基于TOTP算法的开源软件,用户可以将其安装在智能手机上,以便进行二次认证。Spring Security可以通过Google Authenticator进行二次认证。下面是一个简单的示例。

1.1.1 在pom.xml中添加依赖

<dependency>
    <groupId>com.warrenstrange</groupId>
    <artifactId>googleauth</artifactId>
    <version>1.0.0</version>
</dependency>

1.1.2 实现Google Authenticator

@Service
public class GoogleAuthenticatorService {

    private final GoogleAuthenticator gAuth = new GoogleAuthenticator();

    /** 获取密钥 **/
    public String createSecret() {
        final GoogleAuthenticatorKey gak = gAuth.createCredentials();
        return gak.getKey();
    }

    /** 验证身份 **/
    public boolean authorize(final String secret, final int otp) {
        return gAuth.authorize(secret, otp);
    }

    /** 获取二维码 **/
    public String getQR(final String secret, final String account) {
        final String format = "otpauth://totp/%s?secret=%s&issuer=%s";
        return String.format(format, account, secret, account);
    }

}

在上述代码中创建了一个GoogleAuthenticatorService类,用于实现Google Authenticator的相关功能。其中,createSecret()方法用于创建一个新的密钥,authorize()方法用于验证一个TOTP是否有效,getQR()方法用于生成一个TOTP的二维码。

1.1.3 在Spring Security中配置Google Authenticator

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private GoogleAuthenticatorService googleAuthenticatorService;

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                    .antMatchers("/user/**").authenticated()
                    .antMatchers("/admin/**").hasAnyRole("ADMIN")
                    .and()
                .formLogin()
                    .and()
                .addFilterBefore(buildGoogleAuthFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    private GoogleAuthFilter buildGoogleAuthFilter() throws Exception {
        final GoogleAuthFilter filter = new GoogleAuthFilter("/check-google-auth");
        filter.setSecretProvider((request, username) -> {
            final String secret = userService.getSecret(username); // 获取用户的密钥
            return secret != null ? secret : "";
        });
        filter.setGoogleAuthenticator(googleAuthenticatorService.getGoogleAuthenticator());
        return filter;
    }

}

在上述代码中创建了一个SecurityConfig类,并在其中定义了一个GoogleAuthFilter过滤器用于添加二次认证功能。在该过滤器中,我们通过setSecretProvider()方法获取用户的密钥,然后通过setGoogleAuthenticator()方法设置Google Authenticator的实例。

1.2 集成Authy

Authy是一款基于TOTP算法的二次认证服务提供商,可以为Spring Security提供二次认证服务。下面是一个简单的示例。

1.2.1 在pom.xml中添加依赖

<dependency>
    <groupId>com.authy</groupId>
    <artifactId>authy-client</artifactId>
    <version>1.2</version>
</dependency>

1.2.2 实现Authy

@Service
public class AuthyService {

    /** Authy API Key **/
    private final static String AUTHY_API_KEY = "your-authy-api-key";

    /** Authy客户端 **/
    private final AuthyApiClient authyApiClient = new AuthyApiClient(AUTHY_API_KEY);

    /** 注册用户 **/
    public User createUser(final String email, final String countryCode, final String phone) throws Exception {
        final Users users = authyApiClient.getUsers();
        final User user = users.createUser(email, phone, countryCode);
        return user;
    }

    /** 发送验证码 **/
    public void sendVerification(final String userId, final String via) throws Exception {
        final Tokens tokens = authyApiClient.getTokens();
        tokens.requestSms(Integer.valueOf(userId), via);
    }

    /** 验证验证码 **/
    public boolean verifyToken(final String userId, final int token) throws Exception {
        final Tokens tokens = authyApiClient.getTokens();
        final TokenVerification tokenVerification = tokens.verify(Integer.valueOf(userId), token);
        return tokenVerification.isOk();
    }

}

在上述代码中创建了一个AuthyService类,用于与Authy客户端进行交互。其中,createUser()方法用于注册一个新用户,sendVerification()方法用于向用户发送验证码,verifyToken()方法用于验证验证码是否有效。

1.2.3 在Spring Security中配置Authy

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthyService authyService;

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                    .antMatchers("/user/**").authenticated()
                    .antMatchers("/admin/**").hasAnyRole("ADMIN")
                    .and()
                .formLogin()
                    .and()
                .addFilterBefore(buildAuthyFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    private AuthyFilter buildAuthyFilter() throws Exception {
        final AuthyFilter filter = new AuthyFilter("/check-authy");
        filter.setAuthyService(authyService);
        return filter;
    }

}

在上述代码中,创建了一个SecurityConfig类,并在其中定义了一个AuthyFilter过滤器用于添加二次认证功能。在该过滤器中,我们通过setAuthyService()方法设置AuthyService的实例。

2 自己开发二次认证模块

2.1 实现二次认证功能

自己开发二次认证模块意味着我们需要实现自己的TOTP算法。下面是一个简单的例子。

@Service
public class TotpService {

    private final static int WINDOW_SIZE = 3;
    private final static int CODE_DIGITS = 6;
    private final static String HMAC_ALGORITHM = "HmacSHA1";

    private static final int[] DIGITS_POWER
            = // 0 1  2   3    4     5      6       7        8
            {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 };

    /** 获取密钥 **/
    public String createSecret() {
        final SecureRandom random = new SecureRandom();
        final byte[] bytes = new byte[64];
        random.nextBytes(bytes);
        return Base32Utils.encode(bytes);
    }

    /** 生成验证码 **/
    public int generateCode(final String secret) throws Exception {
        final long timeIndex = System.currentTimeMillis() / 30000L; // 30秒
        final byte[] keyBytes = Base32Utils.decode(secret);
        final byte[] data = new byte[8];
        for (int i = 7; i >= 0; i--) {
            data[i] = (byte) (timeIndex & 0xff);
            timeIndex >>= 8;
        }
        final SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_ALGORITHM);
        final Mac mac = Mac.getInstance(HMAC_ALGORITHM);
        mac.init(signingKey);
        final byte[] hmac = mac.doFinal(data);
        int offset = hmac[hmac.length - 1] & 0xf;
        int binCode = ((hmac[offset] & 0x7f) << 24)
                | ((hmac[offset + 1] & 0xff) << 16)
                | ((hmac[offset + 2] & 0xff) << 8)
                | (hmac[offset + 3] & 0xff);
        return binCode % DIGITS_POWER[CODE_DIGITS];
    }

    /** 验证身份 **/
    public boolean authorize(final String secret, final int otp, final int tolerance) throws Exception {
        final long timeIndex = System.currentTimeMillis() / 30000L; // 30秒
        final byte[] keyBytes = Base32Utils.decode(secret);
        for (int i = -tolerance; i <= tolerance; i++) {
            final long ti = timeIndex + i;
            final byte[] data = new byte[8];
            for (int j = 7; j >= 0; j--) {
                data[j] = (byte) (ti & 0xff);
                ti >>= 8;
            }
            final SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_ALGORITHM);
            final Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            mac.init(signingKey);
            final byte[] hmac = mac.doFinal(data);
            int offset = hmac[hmac.length - 1] & 0xf;
            int binCode = ((hmac[offset] & 0x7f) << 24)
                    | ((hmac[offset + 1] & 0xff) << 16)
                    | ((hmac[offset + 2] & 0xff) << 8)
                    | (hmac[offset + 3] & 0xff);
            if (binCode % DIGITS_POWER[CODE_DIGITS] == otp) {
                return true;
            }
        }
        return false;
    }

}

在上述代码中创建了一个TotpService类,用于实现二次认证功能。其中,createSecret()方法用于创建一个新的密钥,generateCode()方法用于生成一个TOTP验证码,authorize()方法用于验证TOTP是否有效。

2.2 在Spring Security中配置自己的二次认证模块

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private TotpService totpService;

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                    .antMatchers("/user/**").authenticated()
                    .antMatchers("/admin/**").hasAnyRole("ADMIN")
                    .and()
                .formLogin()
                    .and()
                .addFilterBefore(buildTotpFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    private TotpFilter buildTotpFilter() throws Exception {
        final TotpFilter filter = new TotpFilter("/check-totp");
        filter.setTotpService(totpService);
        return filter;
    }

}

在上述代码中创建了一个SecurityConfig类,并在其中定义了一个TotpFilter过滤器用于添加二次认证功能。在该过滤器中,我们通过setTotpService()方法设置TotpService的实例。

三、实现过程

1 集成Google Authenticator

1.1 安装并配置Google Authenticator

首先需要在服务器端安装和配置Google Authenticator。具体步骤可以按照以下操作进行:

1.2 在Spring Security中配置Google Authenticator

在将Google Authenticator与Spring Security集成时需要完成以下步骤:

1.3 测试Google Authenticator的集成

在进行Google Authenticator集成测试时需要完成以下步骤:

2 集成Authy

2.1 注册并配置Authy

首先需要在Authy网站上注册一个账户。然后需要创建一个新的应用程序,获取应用程序的Api Key和App Id,这两个值后面需要在代码中使用。

2.2 在Spring Security中配置Authy

在没有使用Google Authenticator身份验证的情况下,Authy可以实现二次身份验证。我们可以按照以下步骤来将Authy与Spring Security集成:

我们需要在Spring Security配置中注册自定义的过滤器:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(new AuthyTotpFilter("/authenticate/authy"), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                    .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                    .antMatchers("/admin/**").hasRole("ADMIN")
                    .anyRequest().authenticated()
                    .and()
                .formLogin().defaultSuccessUrl("/home")
                    .and()
                .logout().logoutSuccessUrl("/login?logout");
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user").password("password").roles("USER")
                .and()
                .withUser("admin").password("password").roles("ADMIN");
    }
}

在上述代码中通过addFilterBefore()方法注册了我们所实现的自定义AuthyTotp过滤器。这个过滤器将在Spring Security的UsernamePasswordAuthenticationFilter之前执行,用于验证用户输入的Authy TOTP码是否合法。

2.3 测试Authy的集成

在进行Authy集成测试时需要完成以下步骤:

3 自己开发二次认证模块

当系统需求与现有的二次认证方式不匹配时,我们可以自己开发二次认证模块。现在,我们打算实现一个基于手机短信的二次认证模块,并将其集成到一个基于Spring Security的Java Web应用程序中。

3.1 实现基于手机短信的二次认证

我们的基于手机短信的二次认证模块,需要完成以下任务:

我们使用Twilio API实现短信验证码的发送和验证过程。Twilio是一个使用简单的云通讯平台,可以方便地发送短信和语音信息。我们可以使用Twilio提供的REST API来发送和验证短信验证码,并在Java Web应用程序中进行封装。

首先在网站上注册账号获取API Key和API Secret,用于进行短信验证码的发送和验证。

其次在Java Web应用程序中需要使用Twilio提供的Java库,引入以下代码:

import com.twilio.Twilio;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;

public class TwilioSMSVerifier {
  private static final String TWILIO_ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
  private static final String TWILIO_AUTH_TOKEN = "your_auth_token";
  private static final String TWILIO_PHONE_NUMBER = "+1415XXXXXXX";

  public static void sendMessage(String toPhoneNumber, String message) {
    Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);
    Message twilioMessage = Message.creator(
            new PhoneNumber(toPhoneNumber),
            new PhoneNumber(TWILIO_PHONE_NUMBER),
            message
    ).create();
  }

  public static boolean verifyCode(String toPhoneNumber, String code) {
    // 进行验证码验证...
    return true;
  }
}

这里我们引入了Twilio库,并定义了发送短信和验证验证码的方法。请注意,这里的TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKEN需要替换为您自己的Twilio账户信息。TWILIO_PHONE_NUMBER是您的Twilio账户绑定的电话号码。

接下来实现sendMessage方法,调用Twilio API发送短信验证码:

public static void sendMessage(String toPhoneNumber, String message) {
    Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);
    Message twilioMessage = Message.creator(
            new PhoneNumber(toPhoneNumber),
            new PhoneNumber(TWILIO_PHONE_NUMBER),
            message
    ).create();

    System.out.println("Twilio message SID: " + twilioMessage.getSid());
}

这里使用Twilio提供的Message对象进行短信验证码的发送。我们使用PhoneNumber对象来表示电话号码,在Message构造函数中指定了tofrom电话号码,以及需发送的消息内容message。发送后,我们通过System.out输出Twilio消息的SID。

接下来实现verifyCode方法,验证用户提交的短信验证码:

public static boolean verifyCode(String toPhoneNumber, String code) {
    Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);

    List<Message> messages = Message.reader()
            .setTo(new PhoneNumber(toPhoneNumber))
            .read();

    for (Message message : messages) {
        String body = message.getBody();
        if (body.contains(code)) {
            return true;
        }
    }

    return false;
}

这里使用Twilio提供的Message.reader对象,查询该手机号码的短信信息。我们遍历查询结果,判断消息正文是否包含提交的验证码。如果存在,则返回true,否则返回false

3.2 在Spring Security中配置自己开发的二次认证模块

现在已经完成了基于手机短信的二次认证模块的开发。接下来,我们将其集成到基于Spring Security的Java Web应用程序中。为此,我们需要在Spring Security中添加一个新的认证提供程序,并将我们的验证码验证逻辑集成在其中。

我们可以通过继承AbstractUserDetailsAuthenticationProvider,实现自己的认证方式。我们的认证提供程序,需要完成以下任务:

下面是我们自己开发的二次认证提供程序的Java代码:

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

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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;

public class SMSAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
  private UserDetailsService userDetailsService;
  private int codeLength; // 验证码长度
  private int codeExpiration; // 验证码有效期

  public SMSAuthenticationProvider(UserDetailsService userDetailsService, int codeLength,
      int codeExpiration) {
    this.userDetailsService = userDetailsService;
    this.codeLength = codeLength;
    this.codeExpiration = codeExpiration;
  }

  @Override
  protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    String code = authentication.getCredentials().toString();
    SMSUserDetails smsUser = (SMSUserDetails) userDetails;
    if (!TwilioSMSVerifier.verifyCode(smsUser.getMobile(), code)) {
      throw new BadCredentialsException("Invalid verification code");
    }
  }

  @Override
  protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    String mobile = (String) authentication.getDetails();
    UserDetails loadedUser;
    try {
      loadedUser = userDetailsService.loadUserByUsername(username);
    } catch (UsernameNotFoundException notFound) {
      throw new BadCredentialsException("Account not found");
    }
    return new SMSUserDetails(loadedUser.getUsername(), loadedUser.getPassword(), mobile,
        loadedUser.getAuthorities());
  }

  @Override
  public boolean supports(Class<?> authentication) {
    return authentication.equals(UsernamePasswordAuthenticationToken.class);
  }

  public class SMSUserDetails implements UserDetails {
    private final String username;
    private final String password;
    private final String mobile;
    private final List<GrantedAuthority> authorities;

    public SMSUserDetails(String username, String password, String mobile, List<GrantedAuthority> authorities) {
      this.username = username;
      this.password = password;
      this.mobile = mobile;
      this.authorities = new ArrayList<>(authorities);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
      return authorities;
    }

    @Override
    public String getPassword() {
      return password;
    }

    @Override
    public String getUsername() {
      return username;
    }

    public String getMobile() {
      return mobile;
    }

    @Override
    public boolean isAccountNonExpired() {
      return true;
    }

    @Override
    public boolean isAccountNonLocked() {
      return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
      return true;
    }

    @Override
    public boolean isEnabled() {
      return true;
    }
  }
}

首先定义了一个描述用户详情的内部类SMSUserDetails,用于存储用户ID、密码、手机号码和权限等信息。在retrieveUser方法中,我们首先获取用户提交的手机号码,然后调用userDetailsService获取用户信息。最后,我们返回SMSUserDetails对象,封装用户详情。需要注意的是,SMSUserDetails必须实现UserDetails接口,以保证在之后的认证过程中能够正确地获取用户信息。

additionalAuthenticationChecks中,我们使用TwilioSMSVerifier对象验证用户提交的验证码。在这里,我们获取authentication.getCredentials(),即用户提交的验证码,进行验证。如果认证不通过,我们则抛出一个BadCredentialsException,表示认证失败。

最后在supports方法中返回支持的认证类型。这里,我们只支持UsernamePasswordAuthenticationToken类型的认证。如果使用其他类型的认证对象,Spring Security将会抛出异常。

3.3 测试自己开发的二次认证模块的集成

现在已经完成了基于手机短信的二次认证模块的开发,并将其集成到了基于Spring Security的Java Web应用程序中。接下来,我们将会对这个模块进行测试,验证其是否可以正常工作。

首先需要使用Twilio API向我们自己的手机号码发送一条测试短信。我们可以使用以下代码:

TwilioSMSVerifier.sendMessage("+8612345678901", "Your verification code is: 123456");

在发送短信后可以使用以下代码,在Spring Security中进行认证:

@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(HttpServletRequest request) {
  String username = request.getParameter("username");
  String password = request.getParameter("password");
  String mobile = request.getParameter("mobile");
  String code = request.getParameter("code");

  UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
      username, password, Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
  authRequest.setDetails(mobile);

  Authentication authentication = authManager.authenticate(authRequest);
  SecurityContextHolder.getContext().setAuthentication(authentication);

  return "redirect:/home";
}

这里首先获取用户提交的认证信息,包括用户名、密码、手机号码和短信验证码。接下来,我们创建一个UsernamePasswordAuthenticationToken对象,并设置其类型为ROLE_USER。我们将手机号码设置为该对象的详情信息。最后,我们使用authManager进行认证,并将认证结果保存在SecurityContextHolder中。

现在已经完成了自己开发的二次认证模块的集成测试。需要注意的是需要使用真实手机号码进行测试,以确保短信验证码可以正常发送和验证。如果您没有真实手机号码,可以注册一个Twilio账号,并使用Twilio提供的测试号码进行测试。

四、小结回顾

在本篇文章中详细介绍了二次认证的概念以及其优缺点。二次认证可以通过多种方式实现包括基于硬件令牌、短信验证码和移动应用等。不同的实现方式各有优缺点,需要按照实际情况进行选择。

其次使用Spring Security演示了如何在一个基于Web的应用程序中添加二次认证。通过使用Spring Security可以轻松地将一个二次认证模块与我们的应用程序集成。在这个过程中了解如何配置Spring Security以及如何使用Spring Security提供的安全特性。

最后还讨论了一些可能的问题以及解决方案。例如,提到了可能会遇到与移动设备兼容性和恶意攻击等问题。为了解决这些问题提供了一些方法和建议,例如使用较新的移动设备和实施防范措施等。

希望大家能够了解二次认证的主要概念以及如何使用Spring Security实现二次认证。同时也希望大家能够了解二次认证可能遇到的挑战,并能够选择适当的解决方案。

4.1 二次认证的优缺点

二次认证可以提供额外的安全保障,增强用户账号的安全性。通过二次认证,用户需要在登录时进行额外的验证,使得其他人无法直接登录到该账号中。尤其是在一些关键应用场景中,例如银行账户和电子商务平台,二次认证可以提供更加可靠的账号保护。

虽然二次认证可以提供更高的安全保障,但它也可能会影响系统的便捷性和易用性。尤其是在一些需要频繁登录的应用中,例如互联网社交应用和游戏应用,过多的验证步骤可能会导致用户反感。此外二次认证实现的复杂度也可能会造成不必要的负担和成本。

因此在实践中需要根据实际情况进行选择。我们可以在关键应用场景中使用二次认证,同时在其他应用场景中采用其他的验证方式,以提供更好的用户体验和系统安全性的平衡。

4.2 Spring Security添加二次认证的效果

通过使用Spring Security可以轻松地将二次认证模块集成到我们的应用程序中。可以使用Spring Security提供的二次认证过滤器,并按照要求配置我们的认证方式。在进行认证时可以使用Spring Security提供的许多安全特性,以保护我们的应用程序不受恶意攻击。

同时通过Spring Security还可以轻松地实现其他的安全特性,例如用户角色控制、会话管理和密码加密等。这样,我们可以构建一个更加健壮和安全的应用程序,并帮助用户获得更好的体验和信任感。

4.3 可能的问题及解决方案

在实践中能会遇到一些问题如移动设备兼容性、短信验证码实现和恶意攻击等。为了解决这些问题,我们可以采取一些措施例如:

总之通过有效地管理和保护我们的应用程序可以提供更高质量的用户体验和安全性。通过Spring Security,我们可以轻松地添加二次认证模块,并让我们的应用程序更加可靠和安全。

到此这篇关于Spring Security添加二次认证的项目实践的文章就介绍到这了,更多相关Spring Security二次认证内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文