欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解SpringBoot+SpringSecurity+jwt整合及初體驗

 更新時間:2019年06月03日 11:16:09   作者:啤酒就辣條  
這篇文章主要介紹了詳解SpringBoot+SpringSecurity+jwt整合及初體驗,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
原來一直使用shiro做安全框架,配置起來相當(dāng)方便,正好有機會接觸下SpringSecurity,學(xué)習(xí)下這個。順道結(jié)合下jwt,把安全信息管理的問題扔給客戶端,

準(zhǔn)備

首先用的是SpringBoot,省去寫各種xml的時間。然后把依賴加入一下

<!--安全-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--jwt-->
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>

application.yml加上一點配置信息,后面會用

jwt:
 secret: secret
 expiration: 7200000
 token: Authorization

可能用到代碼,目錄結(jié)構(gòu)放出來一下

配置

SecurityConfig配置

首先是配置SecurityConfig,代碼如下

@Configuration
@EnableWebSecurity// 這個注解必須加,開啟Security
@EnableGlobalMethodSecurity(prePostEnabled = true)//保證post之前的注解可以使用
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

  @Autowired
  JwtUserDetailsService jwtUserDetailsService;

  @Autowired
  JwtAuthorizationTokenFilter authenticationTokenFilter;


  //先來這里認(rèn)證一下
  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoderBean());
  }

  //攔截在這配
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
        .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
        .and()
        .authorizeRequests()
        .antMatchers("/login").permitAll()
        .antMatchers("/haha").permitAll()
        .antMatchers("/sysUser/test").permitAll()
        .antMatchers(HttpMethod.OPTIONS, "/**").anonymous()
        .anyRequest().authenticated()    // 剩下所有的驗證都需要驗證
        .and()
        .csrf().disable()           // 禁用 Spring Security 自帶的跨域處理
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
      // 定制我們自己的 session 策略:調(diào)整為讓 Spring Security 不創(chuàng)建和使用 session

    http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

  }

  @Bean
  public PasswordEncoder passwordEncoderBean() {
    return new BCryptPasswordEncoder();
  }

  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }

}

ok,下面娓娓道來。首先我們這個配置類繼承了WebSecurityConfigurerAdapter,這里面有三個重要的方法需要我們重寫一下:

configure(HttpSecurity http):這個方法是我們配置攔截的地方,exceptionHandling().authenticationEntryPoint(),這里面主要配置如果沒有憑證,可以進(jìn)行一些操作,這個后面會看jwtAuthenticationEntryPoint這個里面的代碼。進(jìn)行下一項配置,為了區(qū)分必須加入.and()。authorizeRequests()這個后邊配置那些路徑有需要什么權(quán)限,比如我配置的那幾個url都是permitAll(),及不需要權(quán)限就可以訪問。值得一提的是antMatchers(HttpMethod.OPTIONS, "/**"),是為了方便后面寫前后端分離的時候前端過來的第一次驗證請求,這樣做,會減少這種請求的時間和資源使用。csrf().disable()是為了防止csdf攻擊的,至于什么是csdf攻擊,請自行百度。

另起一行,以示尊重。sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);因為我們要使用jwt托管安全信息,所以把Session禁止掉??聪耂essionCreationPolicy枚舉的幾個參數(shù):

public enum SessionCreationPolicy {
 ALWAYS,//總是會新建一個Session。
 NEVER,//不會新建HttpSession,但是如果有Session存在,就會使用它。
 IF_REQUIRED,//如果有要求的話,會新建一個Session。
 STATELESS;//這個是我們用的,不會新建,也不會使用一個HttpSession。

 private SessionCreationPolicy() {
 }
 }

http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);這行代碼主要是用于JWT驗證,后面再說。

configure(WebSecurity web):這個方法我代碼中沒有用,這個方法主要用于訪問一些靜態(tài)的東西控制。其中ignoring()方法可以讓訪問跳過filter驗證。configureGlobal(AuthenticationManagerBuilder auth):這個方法是主要進(jìn)行驗證的地方,其中jwtUserDetailsService代碼待會會看,passwordEncoder(passwordEncoderBean())是密碼的一種加密方式。

還有兩個注解:@EnableWebSecurity,這個注解必須加,開啟Security。
@EnableGlobalMethodSecurity(prePostEnabled = true),保證post之前的注解可以使用

以上,我們可以確定了哪些路徑訪問不需要任何權(quán)限了,至于哪些路徑需要什么權(quán)限接著往下看。

SecurityUserDetails

Security 中也有類似于shiro中主體的概念,就是在內(nèi)存中存了一個東西,方便程序判斷當(dāng)前請求的用戶有什么權(quán)限,需要實現(xiàn)UserDetails這個接口,所以我寫了這個類,并且繼承了我自己的類SysUser。

public enum SessionCreationPolicy {
 ALWAYS,//總是會新建一個Session。
 NEVER,//不會新建HttpSession,但是如果有Session存在,就會使用它。
 IF_REQUIRED,//如果有要求的話,會新建一個Session。
 STATELESS;//這個是我們用的,不會新建,也不會使用一個HttpSession。

 private SessionCreationPolicy() {
 }
 }

authorities就是我們的權(quán)限,構(gòu)造方法中我手動把密碼set進(jìn)去了,這不合適,包括權(quán)限我也是手動傳進(jìn)去的。這些東西都應(yīng)該從數(shù)據(jù)庫搜出來,我現(xiàn)在只是體驗一把Security,角色權(quán)限那一套都沒寫,所以說明一下就好了,這個構(gòu)造方法就是傳進(jìn)來一個標(biāo)志(我這里用的是username,或者應(yīng)該用userId什么的都可以),然后給你一個完整的主體信息,供其他地方使用。ok,next。

JwtUserDetailsService

SecurityConfig配置里面不是有個方法是做真正的認(rèn)證嘛,或者說從數(shù)據(jù)庫拿信息,具體那認(rèn)證信息的方法就是在這個方法里面。

@Service
public class JwtUserDetailsService implements UserDetailsService {

  @Override
  public UserDetails loadUserByUsername(String user) throws UsernameNotFoundException {
    System.out.println("JwtUserDetailsService:" + user);
    List<GrantedAuthority> authorityList = new ArrayList<>();
    authorityList.add(new SimpleGrantedAuthority("ROLE_USER"));
    return new SecurityUserDetails(user,authorityList);
  }

}

繼承了Security提供的UserDetailsService接口,實現(xiàn)loadUserByUsername這個方法,我們這里手動模擬從數(shù)據(jù)庫搜出來一個叫USER的權(quán)限,通過剛才的構(gòu)造方法,模擬生成當(dāng)前user的信息,供后面jwt Filter一大堆驗證。至于為什么USER權(quán)限要加上“ROLE_”前綴,待會會說。

ok,現(xiàn)在我們知道了怎么配置各種url是否需要權(quán)限才能訪問,也知道了哪里可以拿到我們的主體信息,那么繼續(xù)。

JwtAuthorizationTokenFilter

千呼萬喚始出來,JWT終于可以上場了。至于怎么生成這個token憑證,待會會說,現(xiàn)在假設(shè)前端已經(jīng)拿到了token憑證,要訪問某個接口了,看看怎么進(jìn)行jwt業(yè)務(wù)的攔截吧。

@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {

  private final UserDetailsService userDetailsService;
  private final JwtTokenUtil jwtTokenUtil;
  private final String tokenHeader;

  public JwtAuthorizationTokenFilter(@Qualifier("jwtUserDetailsService") UserDetailsService userDetailsService,
                    JwtTokenUtil jwtTokenUtil, @Value("${jwt.token}") String tokenHeader) {
    this.userDetailsService = userDetailsService;
    this.jwtTokenUtil = jwtTokenUtil;
    this.tokenHeader = tokenHeader;
  }

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
    final String requestHeader = request.getHeader(this.tokenHeader);
    String username = null;
    String authToken = null;
    if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
      authToken = requestHeader.substring(7);
      try {
        username = jwtTokenUtil.getUsernameFromToken(authToken);
      } catch (ExpiredJwtException e) {
      }
    }

    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

      UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

      if (jwtTokenUtil.validateToken(authToken, userDetails)) {
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);
      }

    }
    chain.doFilter(request, response);
  }
}

提前說一下,關(guān)于@Value注解參數(shù)開頭寫了。

doFilterInternal() 這個方法就是這個過濾器的精髓了。首先從header中獲取憑證authToken,從中挖掘出來我們的username,然后看看上下文中是否有我們以這個username為標(biāo)識的主體。沒有,ok,去new一個(如果對象也可以new就好了。。。)。然后就是驗證這個authToken 是否在有效期呢啊,驗證token是否對啊等等吧。其實我們剛剛把我們SecurityUserDetails這個對象叫做主體,到這里我才發(fā)現(xiàn)有點自做多情了,因為生成Security承認(rèn)的主體是通過UsernamePasswordAuthenticationToken類似與這種類去實現(xiàn)的,之前之所以叫SecurityUserDetails為主體,只是它存了一些關(guān)鍵信息。然后將主體信息————authentication,存入上下文環(huán)境,供后面使用。

我的很多工具類代碼都放到了jwtTokenUtil,下面貼一下代碼:

@Component
public class JwtTokenUtil implements Serializable {
  private static final long serialVersionUID = -3301605591108950415L;

  @Value("${jwt.secret}")
  private String secret;

  @Value("${jwt.expiration}")
  private Long expiration;

  @Value("${jwt.token}")
  private String tokenHeader;

  private Clock clock = DefaultClock.INSTANCE;

  public String generateToken(UserDetails userDetails) {
    Map<String, Object> claims = new HashMap<>();
    return doGenerateToken(claims, userDetails.getUsername());
  }

  private String doGenerateToken(Map<String, Object> claims, String subject) {
    final Date createdDate = clock.now();
    final Date expirationDate = calculateExpirationDate(createdDate);

    return Jwts.builder()
        .setClaims(claims)
        .setSubject(subject)
        .setIssuedAt(createdDate)
        .setExpiration(expirationDate)
        .signWith(SignatureAlgorithm.HS512, secret)
        .compact();
  }

  private Date calculateExpirationDate(Date createdDate) {
    return new Date(createdDate.getTime() + expiration);
  }

  public Boolean validateToken(String token, UserDetails userDetails) {
    SecurityUserDetails user = (SecurityUserDetails) userDetails;
    final String username = getUsernameFromToken(token);
    return (username.equals(user.getUsername())
        && !isTokenExpired(token)
    );
  }

  public String getUsernameFromToken(String token) {
    return getClaimFromToken(token, Claims::getSubject);
  }

  public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
    final Claims claims = getAllClaimsFromToken(token);
    return claimsResolver.apply(claims);
  }

  private Claims getAllClaimsFromToken(String token) {
    return Jwts.parser()
        .setSigningKey(secret)
        .parseClaimsJws(token)
        .getBody();
  }


  private Boolean isTokenExpired(String token) {
    final Date expiration = getExpirationDateFromToken(token);
    return expiration.before(clock.now());
  }

  public Date getExpirationDateFromToken(String token) {
    return getClaimFromToken(token, Claims::getExpiration);
  }

}

根據(jù)注釋你能猜個大概吧,就不再說了,有些東西是jwt方面的東西,今天就不再多說了。

JwtAuthenticationEntryPoint

前面還說了一個發(fā)現(xiàn)沒有憑證走一個方法,代碼也貼一下。

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
  @Override
  public void commence(HttpServletRequest request,
             HttpServletResponse response,
             AuthenticationException authException)
      throws IOException, ServletException {

    System.out.println("JwtAuthenticationEntryPoint:"+authException.getMessage());
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"沒有憑證");
  }
}

實現(xiàn)AuthenticationEntryPoint這個接口,發(fā)現(xiàn)沒有憑證,往response中放些東西。

run code

下面跑一下幾個接口,看看具體是怎么具體訪問某個方法的吧,還有前面一點懸念一并解決。

登錄

先登錄一下,看看怎么生成token扔給前端的吧。

@RestController
public class LoginController {

  @Autowired
  @Qualifier("jwtUserDetailsService")
  private UserDetailsService userDetailsService;

  @Autowired
  private JwtTokenUtil jwtTokenUtil;

  @PostMapping("/login")
  public String login(@RequestBody SysUser sysUser, HttpServletRequest request){
    final UserDetails userDetails = userDetailsService.loadUserByUsername(sysUser.getUsername());
    final String token = jwtTokenUtil.generateToken(userDetails);
    return token;
  }

  @PostMapping("haha")
  public String haha(){
    UserDetails userDetails = (UserDetails) org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    return "haha:"+userDetails.getUsername()+","+userDetails.getPassword();
  }
}

我們前面配置中已經(jīng)把login設(shè)置為隨便訪問了,這邊通過jwt生成一個token串,具體方法請看jwtTokenUtil.generateToken,已經(jīng)寫了。只要知道這里面存了username、加密規(guī)則、過期時間就好了。

然后跑下haha接口,發(fā)現(xiàn)沒問題,正常打印,說明主體也在上下文中了。

需要權(quán)限

然后我們訪問一個需要權(quán)限的接口吧。

@RestController
@RequestMapping("/sysUser")
public class SysUserController {

  @GetMapping(value = "/test")
  public String test() {
    return "Hello Spring Security";
  }

  @PreAuthorize("hasAnyRole('USER')")
  @PostMapping(value = "/testNeed")
  public String testNeed() {
    return "testNeed";
  }
}

訪問testNeed接口,看到?jīng)],@PreAuthorize("hasAnyRole('USER')")這個說明需要USER權(quán)限!我們在剛剛生成SecurityUserDetails這個的時候已經(jīng)模擬加入了USER權(quán)限了,所以可以訪問。現(xiàn)在說說為什么加權(quán)限的時候需要加入前綴“ROLE_”.看hasAnyRole源碼:

public final boolean hasAnyRole(String... roles) {
  return hasAnyAuthorityName(defaultRolePrefix, roles);
}

private boolean hasAnyAuthorityName(String prefix, String... roles) {
  Set<String> roleSet = getAuthoritySet();

  for (String role : roles) {
    String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
    if (roleSet.contains(defaultedRole)) {
      return true;
    }
  }

  return false;
}

private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
  if (role == null) {
    return role;
  }
  if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
    return role;
  }
  if (role.startsWith(defaultRolePrefix)) {
    return role;
  }
  return defaultRolePrefix + role;
}

關(guān)鍵是 defaultRolePrefix 看這個類最上面
private String defaultRolePrefix = "ROLE_";

人家源碼這么干的,咱們就這么寫唄,咱也不敢問。其實也有不需要前綴的方式,去看看SecurityExpressionRoot這個類吧,用的方法不一樣,也就是@PreAuthorize里面有另外一個參數(shù)。

一個重要的問題

先說結(jié)論:Security上下文環(huán)境(里面有主體)生命周期只限于一次請求。

我做了一個測試:

把SecurityConfig里面configure(HttpSecurity http)這個方法里面

http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

這行代碼注釋掉,不走那個jwt filter。就是不每次都添加上下上下文環(huán)境。

然后loginController改成

@RestController
public class LoginController {

  @Autowired
  @Qualifier("jwtUserDetailsService")
  private UserDetailsService userDetailsService;

  @Autowired
  private JwtTokenUtil jwtTokenUtil;

  @PostMapping("/login")
  public String login(@RequestBody SysUser sysUser, HttpServletRequest request){
    final UserDetails userDetails = userDetailsService.loadUserByUsername(sysUser.getUsername());
    final String token = jwtTokenUtil.generateToken(userDetails);
    //添加 start
    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    SecurityContextHolder.getContext().setAuthentication(authentication);
    //添加 end
    return token;
  }

  @PostMapping("haha")
  public String haha(){
    UserDetails userDetails = (UserDetails) org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    return "haha:"+userDetails.getUsername()+","+userDetails.getPassword();
  }
}

然后登陸,然后訪問/haha,崩了,發(fā)現(xiàn)userDetails里面沒數(shù)據(jù)。說明這會上下文環(huán)境中我們主體不存在。

為什么會這樣呢?

SecurityContextPersistenceFilter 一次請求,filter鏈結(jié)束之后 會清除掉Context里面的東西。所說以,主體數(shù)據(jù)生命周期是一次請求。

源碼如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    throws IOException, ServletException {
  ...假裝有一堆代碼...
  try {
  }
  finally {
    SecurityContext contextAfterChainExecution = SecurityContextHolder
        .getContext();
    // Crucial removal of SecurityContextHolder contents - do this before anything
    // else.
    SecurityContextHolder.clearContext();
    repo.saveContext(contextAfterChainExecution, holder.getRequest(),
        holder.getResponse());
    request.removeAttribute(FILTER_APPLIED);
  }
}

關(guān)鍵就是finally里面 SecurityContextHolder.clearContext(); 這句話。這才體現(xiàn)了那句,把維護(hù)信息的事扔給了客戶端,你不請求,我也不知道你有啥。

體驗小結(jié)

配置起來感覺還可以吧,使用jwt方式,生成token.由于上下文環(huán)境的生命周期是一次請求,所以在不請求的情況下,服務(wù)端不清楚用戶有那些權(quán)限,真正實現(xiàn)了客戶端維護(hù)安全信息,所以項目中也沒有登出接口,因為沒必要。即使前端退出了,你有token,依然可以通過postman請求接口(token沒有過期)。不同于shiro可以把信息維護(hù)在服務(wù)端,要是登出,clear主體信息,訪問接口就需要在登錄。不過Security這樣也有好處,可以實現(xiàn)單點登陸了,也方便做分布式。(只要你不同子系統(tǒng)中驗證那一套邏輯相同,或者在分布式的情況下有單獨的驗證系統(tǒng))。

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

  • SpringCloud?Feign請求頭刪除修改的操作代碼

    SpringCloud?Feign請求頭刪除修改的操作代碼

    這篇文章主要介紹了SpringCloud?Feign請求頭刪除修改,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-03-03
  • 不規(guī)范使用ThreadLocal導(dǎo)致bug分析解決

    不規(guī)范使用ThreadLocal導(dǎo)致bug分析解決

    這篇文章主要為大家介紹了不規(guī)范使用ThreadLocal導(dǎo)致bug分析解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-01-01
  • 基于Java Socket實現(xiàn)一個簡易在線聊天功能(一)

    基于Java Socket實現(xiàn)一個簡易在線聊天功能(一)

    這篇文章主要給大家介紹基于Java Socket實現(xiàn)一個簡易在線聊天功能(一),分為客戶端和服務(wù)端兩段代碼,非常具有參考價值,感興趣的朋友一起學(xué)習(xí)吧
    2016-05-05
  • SpringBoot整合WebSocket實現(xiàn)實時通信功能

    SpringBoot整合WebSocket實現(xiàn)實時通信功能

    在當(dāng)今互聯(lián)網(wǎng)時代,實時通信已經(jīng)成為了許多應(yīng)用程序的基本需求,而WebSocket作為一種全雙工通信協(xié)議,為開發(fā)者提供了一種簡單、高效的實時通信解決方案,本文將介紹如何使用SpringBoot框架來實現(xiàn)WebSocket的集成,快速搭建實時通信功能,感興趣的朋友可以參考下
    2023-11-11
  • SpringBoot項目設(shè)置斷點debug調(diào)試無效忽略web.xml問題的解決

    SpringBoot項目設(shè)置斷點debug調(diào)試無效忽略web.xml問題的解決

    這篇文章主要介紹了SpringBoot項目設(shè)置斷點debug調(diào)試無效忽略web.xml問題的解決,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-08-08
  • IDEA如何開啟并配置services窗口

    IDEA如何開啟并配置services窗口

    在使用IntelliJ IDEA時,可能會遇到Services窗口不自動彈出的情況,本文介紹了如何手動開啟Services窗口的簡單步驟,首先,通過點擊菜單欄中的“視圖”->“工具窗口”->“服務(wù)”,或使用快捷鍵Alt+F8(注意快捷鍵可能存在沖突)來打開Services窗口
    2024-10-10
  • java如何通過流讀取圖片做base64編碼

    java如何通過流讀取圖片做base64編碼

    這篇文章主要介紹了java如何通過流讀取圖片做base64編碼問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-11-11
  • 多線程計數(shù),怎么保持計數(shù)準(zhǔn)確的方法

    多線程計數(shù),怎么保持計數(shù)準(zhǔn)確的方法

    這篇文章主要介紹了多線程計數(shù)的方法,有需要的朋友可以參考一下
    2014-01-01
  • springboot實現(xiàn)跨域的五種方式總結(jié)

    springboot實現(xiàn)跨域的五種方式總結(jié)

    在Spring Boot中實現(xiàn)跨域,可以采用全局跨域和局部跨域兩種方式,下面這篇文章主要給大家介紹了關(guān)于springboot實現(xiàn)跨域的五種方式,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2024-01-01
  • 詳解在SpringBoot應(yīng)用中獲取應(yīng)用上下文方法

    詳解在SpringBoot應(yīng)用中獲取應(yīng)用上下文方法

    本篇文章主要介紹了詳解在SpringBoot應(yīng)用中獲取應(yīng)用上下文方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下。
    2017-04-04

最新評論