SpringBoot中整合Shiro實現(xiàn)權(quán)限管理的示例代碼
之前在 SSM 項目中使用過 shiro,發(fā)現(xiàn) shiro 的權(quán)限管理做的真不錯,但是在 SSM 項目中的配置太繁雜了,于是這次在 SpringBoot 中使用了 shiro,下面一起看看吧
一、簡介
Apache Shiro是一個強(qiáng)大且易用的Java安全框架,執(zhí)行身份驗證、授權(quán)、密碼和會話管理。使用Shiro的易于理解的API,您可以快速、輕松地獲得任何應(yīng)用程序,從最小的移動應(yīng)用程序到最大的網(wǎng)絡(luò)和企業(yè)應(yīng)用程序。
三個核心組件:
1、Subject
即“當(dāng)前操作用戶”。但是,在 Shiro 中,Subject 這一概念并不僅僅指人,也可以是第三方進(jìn)程、后臺帳戶(Daemon Account)或其他類似事物。它僅僅意味著“當(dāng)前跟軟件交互的東西”。Subject 代表了當(dāng)前用戶的安全操作,SecurityManager 則管理所有用戶的安全操作。
2、SecurityManager
它是Shiro 框架的核心,典型的 Facade 模式,Shiro 通過 SecurityManager 來管理內(nèi)部組件實例,并通過它來提供安全管理的各種服務(wù)。
3、Realm
Realm 充當(dāng)了 Shiro 與應(yīng)用安全數(shù)據(jù)間的“橋梁”或者“連接器”。也就是說,當(dāng)對用戶執(zhí)行認(rèn)證(登錄)和授權(quán)(訪問控制)驗證時,Shiro 會從應(yīng)用配置的 Realm 中查找用戶及其權(quán)限信息。從這個意義上講,Realm 實質(zhì)上是一個安全相關(guān)的 DAO:它封裝了數(shù)據(jù)源的連接細(xì)節(jié),并在需要時將相關(guān)數(shù)據(jù)提供給 Shiro。當(dāng)配置 Shiro 時,你必須至少指定一個 Realm,用于認(rèn)證和(或)授權(quán)。配置多個 Realm 是可以的,但是至少需要一個。Shiro 內(nèi)置了可以連接大量安全數(shù)據(jù)源(又名目錄)的 Realm,如 LDAP、關(guān)系數(shù)據(jù)庫(JDBC)、類似 INI 的文本配置資源以及屬性文件等。如果缺省的 Realm 不能滿足需求,你還可以插入代表自定義數(shù)據(jù)源的自己的 Realm 實現(xiàn)。
二、整合 shiro
1、引入 maven 依賴
<!-- web支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- thymeleaf 模板引擎 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- Shiro 權(quán)限管理 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.4</version> </dependency> <!-- 為了能夠在 html 中使用 shiro 的標(biāo)簽引入 --> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
我使用的 SpringBoot 版本是 2.3.1,其它依賴自己看著引入吧
2、創(chuàng)建 shiro 配置文件
關(guān)于 shiro 的配置信息,我們都放在 ShiroConfig.java 文件中
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* shiro配置類
*/
@Configuration
public class ShiroConfig {
/**
* 注入這個是是為了在thymeleaf中使用shiro的自定義tag。
*/
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
/**
* 地址過濾器
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 設(shè)置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 設(shè)置登錄url
shiroFilterFactoryBean.setLoginUrl("/login");
// 設(shè)置主頁url
shiroFilterFactoryBean.setSuccessUrl("/");
// 設(shè)置未授權(quán)的url
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 開放登錄接口
filterChainDefinitionMap.put("/doLogin", "anon");
// 開放靜態(tài)資源文件
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/layui/**", "anon");
// 其余url全部攔截,必須放在最后
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 自定義安全管理策略
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
/**
設(shè)置自定義的relam
*/
securityManager.setRealm(loginRelam());
return securityManager;
}
/**
* 登錄驗證
*/
@Bean
public LoginRelam loginRelam() {
return new LoginRelam();
}
/**
* 以下是為了能夠使用@RequiresPermission()等標(biāo)簽
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
}
上面開放靜態(tài)資源文件,其它博客說的是 **filterChainDefinitionMap.put("/static/**", "anon");** ,但我發(fā)現(xiàn),我們在 html 文件中引入靜態(tài)文件時,請求路徑根本沒有經(jīng)過 static,thymeleaf 自動默認(rèn)配置 **static/** 下面就是靜態(tài)資源文件,所以,我們開放靜態(tài)資源文件需要指定響應(yīng)的目錄路徑
2、登錄驗證管理
關(guān)于登錄驗證的一些邏輯,以及賦權(quán)等操作,我們都放在 LoginRelam.java 文件中
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zyxx.sbm.entity.UserInfo;
import com.zyxx.sbm.service.RolePermissionService;
import com.zyxx.sbm.service.UserInfoService;
import com.zyxx.sbm.service.UserRoleService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Set;
/**
* 登錄授權(quán)
*/
public class LoginRelam extends AuthorizingRealm {
@Autowired
private UserInfoService userInfoService;
@Autowired
private UserRoleService userRoleService;
@Autowired
private RolePermissionService rolePermissionService;
/**
* 身份認(rèn)證
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 獲取基于用戶名和密碼的令牌:實際上這個authcToken是從LoginController里面currentUser.login(token)傳過來的
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//根據(jù)用戶名查找到用戶信息
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("account", token.getUsername());
UserInfo userInfo = userInfoService.getOne(queryWrapper);
// 沒找到帳號
if (null == userInfo) {
throw new UnknownAccountException();
}
// 校驗用戶狀態(tài)
if ("1".equals(userInfo.getStatus())) {
throw new DisabledAccountException();
}
// 認(rèn)證緩存信息
return new SimpleAuthenticationInfo(userInfo, userInfo.getPassword(), ByteSource.Util.bytes(userInfo.getAccount()), getName());
}
/**
* 角色授權(quán)
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
UserInfo authorizingUser = (UserInfo) principalCollection.getPrimaryPrincipal();
if (null != authorizingUser) {
//權(quán)限信息對象info,用來存放查出的用戶的所有的角色(role)及權(quán)限(permission)
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//獲得用戶角色列表
Set<String> roleSigns = userRoleService.listUserRoleByUserId(authorizingUser.getId());
simpleAuthorizationInfo.addRoles(roleSigns);
//獲得權(quán)限列表
Set<String> permissionSigns = rolePermissionService.listRolePermissionByUserId(authorizingUser.getId());
simpleAuthorizationInfo.addStringPermissions(permissionSigns);
return simpleAuthorizationInfo;
}
return null;
}
/**
* 自定義加密規(guī)則
*
* @param credentialsMatcher
*/
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
// 自定義認(rèn)證加密方式
CustomCredentialsMatcher customCredentialsMatcher = new CustomCredentialsMatcher();
// 設(shè)置自定義認(rèn)證加密方式
super.setCredentialsMatcher(customCredentialsMatcher);
}
}
以上就是登錄時,需要指明 shiro 對用戶的一些驗證、授權(quán)等操作,還有自定義密碼驗證規(guī)則,在第3步會講到,獲取角色列表,權(quán)限列表,需要獲取到角色與權(quán)限的標(biāo)識,每一個角色,每一個權(quán)限都有唯一的標(biāo)識,裝入 Set 中
3、自定義密碼驗證規(guī)則
密碼的驗證規(guī)則,我們放在了 CustomCredentialsMatcher.java 文件中
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.apache.shiro.crypto.hash.SimpleHash;
/**
* @ClassName CustomCredentialsMatcher
* 自定義密碼加密規(guī)則
* @Author Lizhou
* @Date 2020-07-10 16:24:24
**/
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info) {
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
//加密類型,密碼,鹽值,迭代次數(shù)
Object tokenCredentials = new SimpleHash("md5", token.getPassword(), token.getUsername(), 6).toHex();
// 數(shù)據(jù)庫存儲密碼
Object accountCredentials = getCredentials(info);
// 將密碼加密與系統(tǒng)加密后的密碼校驗,內(nèi)容一致就返回true,不一致就返回false
return equals(tokenCredentials, accountCredentials);
}
}
我們采用的密碼加密方式為 MD5 加密,加密 6 次,使用登錄賬戶作為加密密碼的鹽進(jìn)行加密
4、密碼加密工具
上面我們自定義了密碼加密規(guī)則,我們創(chuàng)建一個密碼加密的工具類 PasswordUtils.java 文件
import org.apache.shiro.crypto.hash.Md5Hash;
/**
* 密碼加密的處理工具類
*/
public class PasswordUtils {
/**
* 迭代次數(shù)
*/
private static final int ITERATIONS = 6;
private PasswordUtils() {
throw new AssertionError();
}
/**
* 字符串加密函數(shù)MD5實現(xiàn)
*
* @param password 密碼
* @param loginName 用戶名
* @return
*/
public static String getPassword(String password, String loginName) {
return new Md5Hash(password, loginName, ITERATIONS).toString();
}
}
三、開始登錄
上面,我們已經(jīng)配置了 shiro 的一系列操作,從登錄驗證、密碼驗證規(guī)則、用戶授權(quán)等等,下面我們就開始登錄,登錄的操作,放在了 LoginController.java 文件中
import com.zyxx.common.consts.SystemConst;
import com.zyxx.common.enums.StatusEnums;
import com.zyxx.common.kaptcha.KaptchaUtil;
import com.zyxx.common.shiro.SingletonLoginUtils;
import com.zyxx.common.utils.PasswordUtils;
import com.zyxx.common.utils.ResponseResult;
import com.zyxx.sbm.entity.UserInfo;
import com.zyxx.sbm.service.PermissionInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @ClassName LoginController
* @Description
* @Author Lizhou
* @Date 2020-07-02 10:54:54
**/
@Api(tags = "后臺管理端--登錄")
@Controller
public class LoginController {
@Autowired
private PermissionInfoService permissionInfoService;
@ApiOperation(value = "請求登錄頁面", notes = "請求登錄頁面")
@GetMapping("login")
public String init() {
return "login";
}
@ApiOperation(value = "請求主頁面", notes = "請求主頁面")
@GetMapping("/")
public String index() {
return "index";
}
@ApiOperation(value = "登錄驗證", notes = "登錄驗證")
@ApiImplicitParams({
@ApiImplicitParam(name = "account", value = "賬號", required = true),
@ApiImplicitParam(name = "password", value = "密碼", required = true),
@ApiImplicitParam(name = "resCode", value = "驗證碼", required = true),
@ApiImplicitParam(name = "rememberMe", value = "記住登錄", required = true)
})
@PostMapping("doLogin")
@ResponseBody
public ResponseResult doLogin(String account, String password, String resCode, Boolean rememberMe, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 驗證碼
if (!KaptchaUtil.validate(resCode, request)) {
return ResponseResult.getInstance().error(StatusEnums.KAPTCH_ERROR);
}
// 驗證帳號和密碼
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(account, password);
// 記住登錄狀態(tài)
token.setRememberMe(rememberMe);
try {
// 執(zhí)行登錄
subject.login(token);
// 將用戶保存到session中
UserInfo userInfo = (UserInfo) subject.getPrincipal();
request.getSession().setAttribute(SystemConst.SYSTEM_USER_SESSION, userInfo);
return ResponseResult.getInstance().success();
} catch (UnknownAccountException e) {
return ResponseResult.getInstance().error("賬戶不存在");
} catch (DisabledAccountException e) {
return ResponseResult.getInstance().error("賬戶已被凍結(jié)");
} catch (IncorrectCredentialsException e) {
return ResponseResult.getInstance().error("密碼不正確");
} catch (ExcessiveAttemptsException e) {
return ResponseResult.getInstance().error("密碼連續(xù)輸入錯誤超過5次,鎖定半小時");
} catch (RuntimeException e) {
return ResponseResult.getInstance().error("未知錯誤");
}
}
@ApiOperation(value = "登錄成功,跳轉(zhuǎn)主頁面", notes = "登錄成功,跳轉(zhuǎn)主頁面")
@PostMapping("success")
public String success() {
return "redirect:/";
}
@ApiOperation(value = "初始化菜單數(shù)據(jù)", notes = "初始化菜單數(shù)據(jù)")
@GetMapping("initMenu")
@ResponseBody
public String initMenu() {
return permissionInfoService.initMenu();
}
@ApiOperation(value = "退出登錄", notes = "退出登錄")
@GetMapping(value = "loginOut")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "login";
}
}
當(dāng)執(zhí)行 subject.login(token); 時,就會進(jìn)入我們在 第二步中第二條登錄驗證中,對用戶密碼、狀態(tài)進(jìn)行檢查,對用戶授權(quán)等操作,登錄的密碼,一定是通過密碼加密工具得到的,不然驗證不通過
四、頁面權(quán)限控制
我們本次使用的是 thymeleaf 模板引擎,我們需要在 html 文件中加入以下內(nèi)容
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
引入了 thymeleaf 的依賴,以及 shiro 的依賴,這樣我們就能在 html 文件中使用 thymeleaf、shiro 的標(biāo)簽了
例如:
1、判斷當(dāng)前用戶有無此權(quán)限,通過權(quán)限標(biāo)識
<button class="layui-btn" shiro:hasPermission="user_info_add"><i class="layui-icon"></i> 新增 </button>
2、與上面相反,判斷當(dāng)前用戶無此權(quán)限,通過權(quán)限標(biāo)識,沒有時驗證通過
<button class="layui-btn" shiro:lacksPermission="user_info_add"><i class="layui-icon"></i> 新增 </button>
3、判斷當(dāng)前用戶有無以下全部權(quán)限,通過權(quán)限標(biāo)識
<button class="layui-btn" shiro:hasAllPermissions="user_info_add"><i class="layui-icon"></i> 新增 </button>
4、判斷當(dāng)前用戶有無以下任一權(quán)限,通過權(quán)限標(biāo)識
<button class="layui-btn" shiro:hasAnyPermissions="user_info_add"><i class="layui-icon"></i> 新增 </button>
5、判斷當(dāng)前用戶有無此角色,通過角色標(biāo)識
<a shiro:hasRole="admin" href="admin.html" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Administer the system</a>
6、與上面相反,判斷當(dāng)前用戶無此角色,通過角色標(biāo)識,沒有時驗證通過
<a shiro:lacksRole="admin" href="admin.html" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Administer the system</a>
7、判斷當(dāng)前用戶有無以下全部角色,通過角色標(biāo)識
<a shiro:hasAllRoles="admin,role1,role2" href="admin.html" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Administer the system</a>
8、判斷當(dāng)前用戶有無以下任一角色,通過角色標(biāo)識
<a shiro:hasAnyRoles="admin,role1,role2" href="admin.html" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >Administer the system</a>
到此這篇關(guān)于SpringBoot中整合Shiro實現(xiàn)權(quán)限管理的示例代碼的文章就介紹到這了,更多相關(guān)SpringBoot整合Shiro權(quán)限內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot集成Shiro進(jìn)行權(quán)限控制和管理的示例
- SpringBoot集成shiro,MyRealm中無法@Autowired注入Service的問題
- SpringBoot2.0整合Shiro框架實現(xiàn)用戶權(quán)限管理的示例
- SpringBoot+Shiro+LayUI權(quán)限管理系統(tǒng)項目源碼
- springboot集成shiro詳細(xì)總結(jié)
- SpringBoot整合Shiro框架,實現(xiàn)用戶權(quán)限管理
- 詳解springboot shiro jwt實現(xiàn)權(quán)限管理
- springboot集成shiro遭遇自定義filter異常的解決
- springboot集成shiro權(quán)限管理簡單實現(xiàn)
相關(guān)文章
mybatis參數(shù)類型不匹配錯誤argument type mismatch的處理方案
這篇文章主要介紹了mybatis參數(shù)類型不匹配錯誤argument type mismatch的處理方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01
Kotlin基礎(chǔ)教程之?dāng)?shù)據(jù)類型
這篇文章主要介紹了Kotlin基礎(chǔ)教程之?dāng)?shù)據(jù)類型的相關(guān)資料,需要的朋友可以參考下2017-05-05
Java反射機(jī)制詳解_動力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了Java反射機(jī)制的相關(guān)資料,主要包括反射的概念、作用2017-06-06
springboot2.0以上調(diào)度器配置線程池的實現(xiàn)
這篇文章主要介紹了springboot2.0以上調(diào)度器配置線程池的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
spring?NamedContextFactory在Fegin配置及使用詳解
在我們?nèi)粘m椖恐?,使用FeignClient實現(xiàn)各系統(tǒng)接口調(diào)用變得更加簡單,?在各個系統(tǒng)集成過程中,難免會遇到某些系統(tǒng)的Client需要特殊的配置、返回讀取等需求。Feign使用NamedContextFactory來為每個Client模塊構(gòu)造單獨(dú)的上下文(ApplicationContext)2023-11-11
Struts2中接收表單數(shù)據(jù)的三種驅(qū)動方式
這篇文章簡單給大家介紹了Struts2中接收表單數(shù)據(jù)的三種驅(qū)動方式,非常不錯,具有參考借鑒價值,需要的的朋友參考下吧2017-07-07
Java項目實戰(zhàn)之在線考試系統(tǒng)的實現(xiàn)(系統(tǒng)介紹)
這篇文章主要介紹了Java項目實戰(zhàn)之在線考試系統(tǒng)的實現(xiàn)(系統(tǒng)介紹),本文通過實例代碼,截圖的形式給大家展示系統(tǒng)技術(shù)架構(gòu),需要的朋友可以參考下2020-02-02
詳解JavaFX桌面應(yīng)用開發(fā)-Group(容器組)
這篇文章主要介紹了JavaFX桌面應(yīng)用開發(fā)-Group(容器組),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04

