基於MyBatis的Spring Boot Security實戰

一 pom

<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>org.fkit</groupId>
     <artifactId>securitymybatistest</artifactId>
     <version>0.0.1-SNAPSHOT</version>
     <packaging>jar</packaging>
     <name>securitymybatistest</name>
     <url>http://maven.apache.org</url>
     <!-- spring-boot-starter-parent是Spring Boot的核心啓動器,  包含了自動配置、日誌和YAML等大量默認的配置,大大簡化了我們的開發。
           引入之後相關的starter引入就不需要添加version配置, spring  boot會自動選擇最合適的版本進行添加。 -->
     <parent>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-parent</artifactId>
           <version>2.0.0.RELEASE</version>
           <relativePath />
     </parent>
     <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-boot-starter-web模塊依賴 -->
           <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
           </dependency>
           <!-- 添加spring-boot-starter-thymeleaf模塊依賴 -->
           <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
           </dependency>
           <!-- 添加spring-boot-starter-security 依賴 -->
           <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
           </dependency>
           <!-- 添加mysql數據庫驅動 -->
           <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
           </dependency>
           <!-- 添加MyBatis依賴 -->
           <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>1.3.1</version>
           </dependency>
           <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <scope>test</scope>
           </dependency>
     </dependencies>
</project>

二 啓動類

package org.fkit.securitymybatistest;


import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


// @SpringBootApplication指定這是一個 spring boot的應用程序.
@SpringBootApplication
// 掃描數據訪問層接口的包名。
@MapperScan("org.fkit.securitymybatistest.mapper")
public class App {
    public static void main(String[] args) {
        // SpringApplication 用於從main方法啓動Spring應用的類。
        SpringApplication.run(App.class, args);
    }
}

三 控制器

package org.fkit.securitytest.controller;


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


import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;


@Controller
public class AppController {


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


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


    @RequestMapping("/home")
    public String homePage(Model model) {
        model.addAttribute("user", getUsername());
        model.addAttribute("role", getAuthority());
        return "home";
    }


    @RequestMapping(value = "/admin")
    public String adminPage(Model model) {
        model.addAttribute("user", getUsername());
        model.addAttribute("role", getAuthority());
        return "admin";
    }


    @RequestMapping(value = "/dba")
    public String dbaPage(Model model) {
        model.addAttribute("user", getUsername());
        model.addAttribute("role", getAuthority());
        return "dba";
    }


    @RequestMapping(value = "/accessDenied")
    public String accessDeniedPage(Model model) {
        model.addAttribute("user", getUsername());
        model.addAttribute("role", getAuthority());
        return "accessDenied";
    }


    @RequestMapping(value = "/logout")
    public String logoutPage(HttpServletRequest request, HttpServletResponse response) {
        // Authentication是一個接口,表示用戶認證信息
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        // 如果用戶認知信息不爲空,註銷
        if (auth != null) {
            new SecurityContextLogoutHandler().logout(request, response, auth);
        }
        // 重定向到login頁面
        return "redirect:/login?logout";
    }


    /**
     * 獲得當前用戶名稱
     */
    private String getUsername() {
        // 從SecurityContex中獲得Authentication對象代表當前用戶的信息
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        System.out.println("username = " + username);
        return username;
    }


    /**
     * 獲得當前用戶權限
     */
    private String getAuthority() {
        // 獲得Authentication對象,表示用戶認證信息。
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        List<String> roles = new ArrayList<String>();
        // 將角色名稱添加到List集合
        for (GrantedAuthority a : authentication.getAuthorities()) {
            roles.add(a.getAuthority());
        }
        System.out.println("role = " + roles);
        return roles.toString();
    }


}

四 數據訪問接口

package org.fkit.securitymybatistest.mapper;

import java.util.List;


import org.apache.ibatis.annotations.Many;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.mapping.FetchType;
import org.fkit.securitymybatistest.pojo.FKRole;
import org.fkit.securitymybatistest.pojo.FKUser;


public interface UserMapper {
    
    // 根據loginName查詢用戶信息,同時關聯查詢出用戶的權限
    @Select("select * from tb_user where login_name = #{loginName}")
     @Results({  
            @Result(id=true,column="id",property="id"),  
            @Result(column="login_name",property="loginName"),  
            @Result(column="password",property="password"),  
            @Result(column="username",property="username"),  
            @Result(column="id",property="roles",
            many=@Many(select="findRoleByUser",
            fetchType=FetchType.EAGER))  
         })  
    FKUser findByLoginName(String loginName);
    
    // 根據用戶id關聯查詢用戶的所有權限
    @Select(" SELECT id,authority FROM tb_role r,tb_user_role ur "
            + " WHERE r.id = ur.role_id AND user_id = #{id}")
    List<FKRole> findRoleByUser(Long id);

}

五 創建自定義服務類

package org.fkit.securitymybatistest.service;


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


import org.fkit.securitymybatistest.mapper.UserMapper;
import org.fkit.securitymybatistest.pojo.FKRole;
import org.fkit.securitymybatistest.pojo.FKUser;
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.User;
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;


/**
* 需要實現UserDetailsService接口
* 因爲在Spring Security中我們配置相關參數需要UserDetailsService類型的數據
* */
@Service
public class UserService implements UserDetailsService{


    // 注入持久層接口UserMapper
    @Autowired
    UserMapper userMapper;
    
    // 實現接口中的loadUserByUsername方法,通過該方法查詢到對應的用戶
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 調用持久層接口findByLoginName方法查找用戶,此處的傳進來的參數實際是loginName
        FKUser fkUser = userMapper.findByLoginName(username);
//        System.out.println("user = " + fkUser);
        if (fkUser == null) {
            throw new UsernameNotFoundException("用戶名不存在");
        }
        // 創建List集合,用來保存用戶權限,GrantedAuthority對象代表賦予給當前用戶的權限
        List<GrantedAuthority> authorities = new ArrayList<>();
        // 獲得當前用戶權限集合
        List<FKRole> roles = fkUser.getRoles();
        for (FKRole role : roles) {
            // 將關聯對象Role的authority屬性保存爲用戶的認證權限
            authorities.add(new SimpleGrantedAuthority(role.getAuthority()));
        }
        // 此處返回的是org.springframework.security.core.userdetails.User類,該類是Spring Security內部的實現
        return new User(fkUser.getUsername(), fkUser.getPassword(), authorities);
    }
}

六 認證處理類

1 AppAuthenticationSuccessHandler

package org.fkit.securitytest.security;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import  org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
@Component
public class AppAuthenticationSuccessHandler extends  SimpleUrlAuthenticationSuccessHandler {
     // Spring Security 通過RedirectStrategy對象負責所有重定向事務
     private RedirectStrategy redirectStrategy = new  DefaultRedirectStrategy();
     /*
      * 重寫handle方法,方法中通過RedirectStrategy對象重定向到指定的url
      */
     @Override
     protected void handle(HttpServletRequest request,  HttpServletResponse response, Authentication authentication)
                throws IOException {
           // 通過determineTargetUrl方法返回需要跳轉的url
           String targetUrl =  determineTargetUrl(authentication);
           // 重定向請求到指定的url
           redirectStrategy.sendRedirect(request, response,  targetUrl);
     }
     /*
      * 從Authentication對象中提取角色提取當前登錄用戶的角色,並根據其角色返回適當的URL。
      */
     protected String determineTargetUrl(Authentication  authentication) {
           String url = "";
           // 獲取當前登錄用戶的角色權限集合
           Collection<? extends GrantedAuthority> authorities =  authentication.getAuthorities();
           List<String> roles = new ArrayList<String>();
           // 將角色名稱添加到List集合
           for (GrantedAuthority a : authorities) {
                roles.add(a.getAuthority());
           }
           // 判斷不同角色跳轉到不同的url
           if (isAdmin(roles)) {
                url = "/admin";
           } else if (isUser(roles)) {
                url = "/home";
           } else {
                url = "/accessDenied";
           }
           System.out.println("url = " + url);
           return url;
     }
     private boolean isUser(List<String> roles) {
           if (roles.contains("ROLE_USER")) {
                return true;
           }
           return false;
     }
     private boolean isAdmin(List<String> roles) {
           if (roles.contains("ROLE_ADMIN")) {
                return true;
           }
           return false;
     }
     public void setRedirectStrategy(RedirectStrategy  redirectStrategy) {
           this.redirectStrategy = redirectStrategy;
     }
     protected RedirectStrategy getRedirectStrategy() {
           return redirectStrategy;
     }
}

2 AppSecurityConfigurer

package org.fkit.securityjpatest.security;


import org.fkit.securityjpatest.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


/**
* 自定義Spring Security認證處理類的時候 我們需要繼承自WebSecurityConfigurerAdapter來完成,相關配置重寫對應
* 方法即可。
*/
@Configuration
public class AppSecurityConfigurer extends WebSecurityConfigurerAdapter {


    // 依賴注入用戶服務類
    @Autowired
    private UserService userService;


    // 依賴注入加密接口
    @Autowired
    private PasswordEncoder passwordEncoder;


    // 依賴注入用戶認證接口
    @Autowired
    private AuthenticationProvider authenticationProvider;


    // 依賴注入認證處理成功類,驗證用戶成功後處理不同用戶跳轉到不同的頁面
    @Autowired
    AppAuthenticationSuccessHandler appAuthenticationSuccessHandler;


    /*
     * BCryptPasswordEncoder是Spring Security提供的PasswordEncoder接口是實現類
     * 用來創建密碼的加密程序,避免明文存儲密碼到數據庫
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    // DaoAuthenticationProvider是Spring Security提供AuthenticationProvider的實現
    @Bean
    public AuthenticationProvider authenticationProvider() {
        // 創建DaoAuthenticationProvider對象
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        // 不要隱藏"用戶未找到"的異常
        provider.setHideUserNotFoundExceptions(false);
        // 通過重寫configure方法添加自定義的認證方式。
        provider.setUserDetailsService(userService);
        // 設置密碼加密程序認證
        provider.setPasswordEncoder(passwordEncoder);
        return provider;
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        System.out.println("AppSecurityConfigurer configure auth......");
        // 設置認證方式。
        auth.authenticationProvider(authenticationProvider);


    }


    /**
     * 設置了登錄頁面,而且登錄頁面任何人都可以訪問,然後設置了登錄失敗地址,也設置了註銷請求,註銷請求也是任何人都可以訪問的。
     * permitAll表示該請求任何人都可以訪問,.anyRequest().authenticated(),表示其他的請求都必須要有權限認證。
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("AppSecurityConfigurer configure http......");
        http.authorizeRequests()
                // spring-security 5.0 之後需要過濾靜態資源
                .antMatchers("/login", "/css/**", "/js/**", "/img/*").permitAll().antMatchers("/", "/home")
                .hasRole("USER").antMatchers("/admin/**").hasAnyRole("ADMIN", "DBA").anyRequest().authenticated().and()
                .formLogin().loginPage("/login").successHandler(appAuthenticationSuccessHandler)
                .usernameParameter("loginName").passwordParameter("password").and().logout().permitAll().and()
                .exceptionHandling().accessDeniedPage("/accessDenied");
    }


}

七 視圖

1 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-springsecurity4">
<head>
<title>Spring Boot Security示例</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}" />
<link rel="stylesheet" th:href="@{css/app.css}" />
<link rel="stylesheet" th:href="@{css/bootstrap-theme.min.css}"  />
<link rel="stylesheet" type="text/css"
     href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.2.0/css/font-awesome.css" />
<script type="text/javascript"  th:src="@{js/jquery-1.11.0.min.js}"></script>
<script type="text/javascript"  th:src="@{js/bootstrap.min.js}"></script>
<script type="text/javascript">
     $(function() {
           $("#loginBtn").click(function() {
                var loginName = $("#loginName");
                var password = $("#password");
                var msg = "";
                if (loginName.val() == "") {
                     msg = "登錄名稱不能爲空!";
                     loginName.focus();
                } else if (password.val() == "") {
                     msg = "密碼不能爲空!";
                     password.focus();
                }
                if (msg != "") {
                     alert(msg);
                     return false;
                }
                $("#loginForm").submit();
           });
     });
</script>
</head>
<body>
     <div class="panel panel-primary">
           <div class="panel-heading">
                <h3 class="panel-title">簡單Spring Boot  Security示例</h3>
           </div>
     </div>
     <div id="mainWrapper">
           <div class="login-container">
                <div class="login-card">
                     <div class="login-form">
                           <!-- 表單提交到login -->
                           <form id="loginForm"  th:action="@{/login}" method="post"
                                class="form-horizontal">
                                <!-- 用戶名或密碼錯誤提示 -->
                                <div th:if="${param.error !=  null}">
                                     <div class="alert  alert-danger">
                                           <p>
                                                <font  color="red">用戶名或密碼錯誤!</font>
                                           </p>
                                     </div>
                                </div>
                                <!-- 註銷提示 -->
                                <div th:if="${param.logout !=  null}">
                                     <div class="alert  alert-success">
                                           <p>
                                                <font  color="red">用戶已註銷成功!</font>
                                           </p>
                                     </div>
                                </div>
                                <div class="input-group  input-sm">
                                     <label  class="input-group-addon"><i class="fa fa-user"></i></label>
                                     <input type="text"  class="form-control" id="loginName"
                                           name="loginName"  placeholder="請輸入用戶名" />
                                </div>
                                <div class="input-group  input-sm">
                                     <label  class="input-group-addon"><i class="fa fa-lock"></i></label>
                                     <input type="password"  class="form-control" id="password"
                                           name="password"  placeholder="請輸入密碼" />
                                </div>
                                <div class="form-actions">
                                     <input id="loginBtn"  type="button"
                                           class="btn btn-block  btn-primary btn-default" value="登錄" />
                                </div>
                           </form>
                     </div>
                </div>
           </div>
     </div>
</body>
</html>

2 home.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"></meta>
<title>home頁面</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}" />
<link rel="stylesheet" th:href="@{css/bootstrap-theme.min.css}"  />
<script type="text/javascript"  th:src="@{js/jquery-1.11.0.min.js}"></script>
<script type="text/javascript"  th:src="@{js/bootstrap.min.js}"></script>
</head>
<body>
     <div class="panel panel-primary">
           <div class="panel-heading">
                <h3 class="panel-title">Home頁面</h3>
           </div>
     </div>
     <h3>
           歡迎[<font color="red"><span th:text="${user}">用戶名</span></font>]訪問Home頁面!
           您的權限是<font color="red"><span th:text="${role}">權限</span></font><br />
           <br /> <a href="admin">訪問admin頁面</a><br />
           <br /> <a href="logout">安全退出</a>
     </h3>
</body>
</html>

3 admin.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"></meta>
<title>admin頁面</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}" />
<link rel="stylesheet" th:href="@{css/bootstrap-theme.min.css}"  />
<script type="text/javascript"  th:src="@{js/jquery-1.11.0.min.js}"></script>
<script type="text/javascript"  th:src="@{js/bootstrap.min.js}"></script>
</head>
<body>
     <div class="panel panel-primary">
           <div class="panel-heading">
                <h3 class="panel-title">Admin頁面</h3>
           </div>
     </div>
     <h3>
           歡迎[<font color="red"><span th:text="${user}">用戶名</span></font>]訪問Admin頁面!
           您的權限是<font color="red"><span th:text="${role}">權限</span></font><br />
           <br /> <a href="dba">訪問dba頁面</a><br />
           <br /> <a href="logout">安全退出</a>
     </h3>
</body>
</html>

4 dba.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"></meta>
<title>dba頁面</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}" />
<link rel="stylesheet" th:href="@{css/bootstrap-theme.min.css}"  />
<script type="text/javascript"  th:src="@{js/jquery-1.11.0.min.js}"></script>
<script type="text/javascript"  th:src="@{js/bootstrap.min.js}"></script>
</head>
<body>
     <div class="panel panel-primary">
           <div class="panel-heading">
                <h3 class="panel-title">DBA頁面</h3>
           </div>
     </div>
     <h3>
           歡迎[<font color="red"><span th:text="${user}">用戶名</span></font>]訪問訪問DBA頁面!
           您的權限是<font color="red"><span th:text="${role}">權限</span></font><br />
           <br /> <a href="logout">安全退出</a>
     </h3>
</body>
</html>

5 accessDenied.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"></meta>
<title>訪問拒絕頁面</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}" />
<link rel="stylesheet" th:href="@{css/bootstrap-theme.min.css}"  />
<script type="text/javascript"  th:src="@{js/jquery-1.11.0.min.js}"></script>
<script type="text/javascript"  th:src="@{js/bootstrap.min.js}"></script>
</head>
<body>
     <div class="panel panel-primary">
           <div class="panel-heading">
                <h3 class="panel-title">AccessDenied頁面</h3>
           </div>
     </div>
     <h3>
           <font color="red"><span th:text="${user}">用戶名</span></font>,
           您沒有權限訪問頁面! 您的權限是<font color="red"><span  th:text="${role}">權限</span></font><br />
           <br /> <a href="logout">安全退出</a>
     </h3>
</body>
</html>

八 創建持久化類

1 FKRole

package org.fkit.securitymybatistest.pojo;
import java.io.Serializable;
public class FKRole implements Serializable {
     private static final long serialVersionUID = 1L;
     
     private Long id;
    private String authority;
    
     public FKRole() {
           super();
           // TODO Auto-generated constructor stub
     }
     public Long getId() {
           return id;
     }
     public void setId(Long id) {
           this.id = id;
     }
     
     public String getAuthority() {
           return authority;
     }
     public void setAuthority(String authority) {
           this.authority = authority;
     }
     @Override
     public String toString() {
           return "FKRole [id=" + id + ", authority=" +  authority + "]";
     }
}

2 FKUser

package org.fkit.securitymybatistest.pojo;


import java.io.Serializable;
import java.util.List;


public class FKUser implements Serializable{
    
    private static final long serialVersionUID = 1L;
    
    private Long id;
    private String loginName;
    private String username;
    private String password;


    private List<FKRole> roles;


    public Long getId() {
        return id;
    }


    public void setId(Long id) {
        this.id = id;
    }


    public String getLoginName() {
        return loginName;
    }


    public void setLoginName(String loginName) {
        this.loginName = loginName;
    }


    public String getUsername() {
        return username;
    }


    public void setUsername(String username) {
        this.username = username;
    }


    public String getPassword() {
        return password;
    }


    public void setPassword(String password) {
        this.password = password;
    }


    public List<FKRole> getRoles() {
        return roles;
    }


    public void setRoles(List<FKRole> roles) {
        this.roles = roles;
    }


    @Override
    public String toString() {
        return "FKUser [id=" + id + ", loginName=" + loginName + ", username=" + username + ", password=" + password
                + ", roles=" + roles + "]";
    }
}

九 創建數據庫Springboot

相關數據表如下:

十 配置文件

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springboot
spring.datasource.username=root
spring.datasource.password=
logging.level.org.springframework.security=info
logging.level.org.fkit.securitymybatistest.mapper.UserMapper=debug
spring.thymeleaf.cache=false

十一 測試

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章