springboot+shiro+redis项目整合
介紹:
Apache Shiro是一個(gè)強(qiáng)大且易用的Java安全框架,執(zhí)行身份驗(yàn)證、授權(quán)、密碼學(xué)和會(huì)話管理。使用Shiro的易于理解的API,您可以快速、輕松地獲得任何應(yīng)用程序,從最小的移動(dòng)應(yīng)用程序到最大的網(wǎng)絡(luò)和企業(yè)應(yīng)用程序。(摘自百度百科)
本文使用springboot+mybatisplus+shiro實(shí)現(xiàn)數(shù)據(jù)庫(kù)動(dòng)態(tài)的管理用戶、角色、權(quán)限管理,在本文的最后我會(huì)提供源碼的下載地址,想看到效果的小伙伴可以直接下載運(yùn)行就ok了
因?yàn)閟hiro的功能比較多,本章只介紹如下幾個(gè)功能
1.當(dāng)用戶沒有登陸時(shí)只能訪問登陸界面
2.當(dāng)用戶登陸成功后,只能訪問該用戶下僅有的權(quán)限
3.一個(gè)用戶不能兩個(gè)人同時(shí)在線
一、數(shù)據(jù)庫(kù)設(shè)計(jì)
本文的數(shù)據(jù)庫(kù)表為5個(gè)分別是: 用戶表、角色表、權(quán)限表、用戶角色中間表、角色權(quán)限中間表,表的結(jié)構(gòu)和數(shù)據(jù)項(xiàng)目中會(huì)提供(sql和redis工具下方的下載地址中都會(huì)有)
?
二、引入依賴
<?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.chaoqi</groupId><artifactId>springboot_mybatisplus</artifactId><version>0.0.1-SNAPSHOT</version><packaging>jar</packaging><name>springboot_mybatisplus</name><description>Demo project for Spring Boot</description><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><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.2</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- reids --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--添加jsp依賴 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></dependency><dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-jasper</artifactId></dependency><!-- SpringBoot - MyBatis 逆向工程 --><dependency><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-core</artifactId><version>1.3.2</version></dependency><!-- MyBatis 通用 Mapper --><dependency><groupId>tk.mybatis</groupId><artifactId>mapper-spring-boot-starter</artifactId><version>1.1.4</version></dependency><!-- shiro --><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.4.0</version></dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-ehcache</artifactId><version>1.4.0</version></dependency><!-- shiro+redis緩存插件 --><dependency><groupId>org.crazycake</groupId><artifactId>shiro-redis</artifactId><version>2.4.2.1-RELEASE</version></dependency><!-- fastjson阿里巴巴jSON處理器 --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.13</version></dependency><!--<dependency>--><!--<groupId>org.springframework.boot</groupId>--><!--<artifactId>spring-boot-starter-security</artifactId>--><!--</dependency>--><!--工具類--><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.7</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin><plugin><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-maven-plugin</artifactId><version>1.3.2</version><configuration><configurationFile>src/main/resources/generatorConfig.xml</configurationFile><verbose>true</verbose><overwrite>true</overwrite></configuration><executions><execution><id>Generate MyBatis Artifacts</id><goals><goal>generate</goal></goals></execution></executions><dependencies><dependency><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-core</artifactId><version>1.3.2</version></dependency><dependency><groupId>tk.mybatis</groupId><artifactId>mapper</artifactId><version>3.5.0</version></dependency></dependencies></plugin></plugins></build></project>?三、編輯application.yml
server:port: 8080spring:mvc:view:prefix: /WEB-INF/jsp/suffix: .jspdatasource:url: jdbc:mysql://localhost:3306/shiro?characterEncoding=UTF-8&useUnicode=true&useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.jdbc.Driverredis:host: localhostport: 6379jedis:pool:max-idle: 8min-idle: 0max-active: 8max-wait: -1timeout: 0mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.chaoqi.springboot_mybatisplus.domain?四、創(chuàng)建ShiroConfig配置
package com.chaoqi.springboot_shiro_redis.config;import com.chaoqi.springboot_shiro_redis.secutity.KickoutSessionControlFilter; import com.chaoqi.springboot_shiro_redis.secutity.MyShiroRealm; 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.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.crazycake.shiro.RedisCacheManager; import org.crazycake.shiro.RedisManager; import org.crazycake.shiro.RedisSessionDAO; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;import javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map;@Configuration public class ShiroConfig {@Beanpublic ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);// 沒有登陸的用戶只能訪問登陸頁面shiroFilterFactoryBean.setLoginUrl("/auth/login");// 登錄成功后要跳轉(zhuǎn)的鏈接shiroFilterFactoryBean.setSuccessUrl("/auth/index");// 未授權(quán)界面; ----這個(gè)配置了沒卵用,具體原因想深入了解的可以自行百度//shiroFilterFactoryBean.setUnauthorizedUrl("/auth/403");//自定義攔截器Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();//限制同一帳號(hào)同時(shí)在線的個(gè)數(shù)。filtersMap.put("kickout", kickoutSessionControlFilter());shiroFilterFactoryBean.setFilters(filtersMap);// 權(quán)限控制map.Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();filterChainDefinitionMap.put("/css/**", "anon");filterChainDefinitionMap.put("/js/**", "anon");filterChainDefinitionMap.put("/img/**", "anon");filterChainDefinitionMap.put("/auth/login", "anon");filterChainDefinitionMap.put("/auth/logout", "logout");filterChainDefinitionMap.put("/auth/kickout", "anon");filterChainDefinitionMap.put("/**", "authc,kickout");shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}@Beanpublic SecurityManager securityManager() {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();// 設(shè)置realm. securityManager.setRealm(myShiroRealm());// 自定義緩存實(shí)現(xiàn) 使用redis securityManager.setCacheManager(cacheManager());// 自定義session管理 使用redis securityManager.setSessionManager(sessionManager());return securityManager;}/*** 身份認(rèn)證realm; (這個(gè)需要自己寫,賬號(hào)密碼校驗(yàn);權(quán)限等)** @return*/@Beanpublic MyShiroRealm myShiroRealm() {MyShiroRealm myShiroRealm = new MyShiroRealm();return myShiroRealm;}/*** cacheManager 緩存 redis實(shí)現(xiàn)* 使用的是shiro-redis開源插件** @return*/public RedisCacheManager cacheManager() {RedisCacheManager redisCacheManager = new RedisCacheManager();redisCacheManager.setRedisManager(redisManager());return redisCacheManager;}/*** 配置shiro redisManager* 使用的是shiro-redis開源插件** @return*/public RedisManager redisManager() {RedisManager redisManager = new RedisManager();redisManager.setHost("localhost");redisManager.setPort(6379);redisManager.setExpire(1800);// 配置緩存過期時(shí)間redisManager.setTimeout(0);// redisManager.setPassword(password);return redisManager;}/*** Session Manager* 使用的是shiro-redis開源插件*/@Beanpublic DefaultWebSessionManager sessionManager() {DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();sessionManager.setSessionDAO(redisSessionDAO());return sessionManager;}/*** RedisSessionDAO shiro sessionDao層的實(shí)現(xiàn) 通過redis* 使用的是shiro-redis開源插件*/@Beanpublic RedisSessionDAO redisSessionDAO() {RedisSessionDAO redisSessionDAO = new RedisSessionDAO();redisSessionDAO.setRedisManager(redisManager());return redisSessionDAO;}/*** 限制同一賬號(hào)登錄同時(shí)登錄人數(shù)控制** @return*/@Beanpublic KickoutSessionControlFilter kickoutSessionControlFilter() {KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();kickoutSessionControlFilter.setCacheManager(cacheManager());kickoutSessionControlFilter.setSessionManager(sessionManager());kickoutSessionControlFilter.setKickoutAfter(false);kickoutSessionControlFilter.setMaxSession(1);kickoutSessionControlFilter.setKickoutUrl("/auth/kickout");return kickoutSessionControlFilter;}/**** 授權(quán)所用配置** @return*/@Beanpublic DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);return defaultAdvisorAutoProxyCreator;}/**** 使授權(quán)注解起作用不如不想配置可以在pom文件中加入* <dependency>*<groupId>org.springframework.boot</groupId>*<artifactId>spring-boot-starter-aop</artifactId>*</dependency>* @param securityManager* @return*/@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);return authorizationAttributeSourceAdvisor;}/*** Shiro生命周期處理器**/@Beanpublic LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}}五、自定義Realm
package com.chaoqi.springboot_shiro_redis.secutity;import com.chaoqi.springboot_shiro_redis.service.SysRoleService; import com.chaoqi.springboot_shiro_redis.service.UserService; import com.chaoqi.springboot_shiro_redis.dao.domain.SysUser; import org.apache.shiro.authc.*; 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.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired;import java.util.*;public class MyShiroRealm extends AuthorizingRealm {private static org.slf4j.Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);//如果項(xiàng)目中用到了事物,@Autowired注解會(huì)使事物失效,可以自己用get方法獲取值 @Autowiredprivate SysRoleService roleService;@Autowiredprivate UserService userService;/*** 認(rèn)證信息.(身份驗(yàn)證) : Authentication 是用來驗(yàn)證用戶身份**/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {logger.info("---------------- 執(zhí)行 Shiro 憑證認(rèn)證 ----------------------");UsernamePasswordToken token = (UsernamePasswordToken) authcToken;String name = token.getUsername();String password = String.valueOf(token.getPassword());SysUser user = new SysUser();user.setUserName(name);user.setPassWord(password);// 從數(shù)據(jù)庫(kù)獲取對(duì)應(yīng)用戶名密碼的用戶SysUser userList = userService.getUser(user);if (userList != null) {// 用戶為禁用狀態(tài)if (userList.getUserEnable() != 1) {throw new DisabledAccountException();}logger.info("---------------- Shiro 憑證認(rèn)證成功 ----------------------");SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userList, //用戶userList.getPassWord(), //密碼getName() //realm name );return authenticationInfo;}throw new UnknownAccountException();}/*** 授權(quán)*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {logger.info("---------------- 執(zhí)行 Shiro 權(quán)限獲取 ---------------------");Object principal = principals.getPrimaryPrincipal();SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();if (principal instanceof SysUser) {SysUser userLogin = (SysUser) principal;Set<String> roles = roleService.findRoleNameByUserId(userLogin.getId());authorizationInfo.addRoles(roles);Set<String> permissions = userService.findPermissionsByUserId(userLogin.getId());authorizationInfo.addStringPermissions(permissions);}logger.info("---- 獲取到以下權(quán)限 ----");logger.info(authorizationInfo.getStringPermissions().toString());logger.info("---------------- Shiro 權(quán)限獲取成功 ----------------------");return authorizationInfo;}}六、限制并發(fā)人數(shù)登陸
package com.chaoqi.springboot_shiro_redis.secutity;import com.alibaba.fastjson.JSON; import com.chaoqi.springboot_shiro_redis.dao.domain.SysUser; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.DefaultSessionKey; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.AccessControlFilter; import org.apache.shiro.web.util.WebUtils;import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.PrintWriter; import java.io.Serializable; import java.util.Deque; import java.util.HashMap; import java.util.LinkedList; import java.util.Map;public class KickoutSessionControlFilter extends AccessControlFilter {private String kickoutUrl; //踢出后到的地址private boolean kickoutAfter = false; //踢出之前登錄的/之后登錄的用戶 默認(rèn)踢出之前登錄的用戶private int maxSession = 1; //同一個(gè)帳號(hào)最大會(huì)話數(shù) 默認(rèn)1private SessionManager sessionManager;private Cache<String, Deque<Serializable>> cache;public void setKickoutUrl(String kickoutUrl) {this.kickoutUrl = kickoutUrl;}public void setKickoutAfter(boolean kickoutAfter) {this.kickoutAfter = kickoutAfter;}public void setMaxSession(int maxSession) {this.maxSession = maxSession;}public void setSessionManager(SessionManager sessionManager) {this.sessionManager = sessionManager;}//設(shè)置Cache的key的前綴public void setCacheManager(CacheManager cacheManager) {this.cache = cacheManager.getCache("shiro_redis_cache");}@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {return false;}@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {Subject subject = getSubject(request, response);if(!subject.isAuthenticated() && !subject.isRemembered()) {//如果沒有登錄,直接進(jìn)行之后的流程return true;}Session session = subject.getSession();SysUser user = (SysUser) subject.getPrincipal();String username = user.getUserName();Serializable sessionId = session.getId();//讀取緩存 沒有就存入Deque<Serializable> deque = cache.get(username);//如果此用戶沒有session隊(duì)列,也就是還沒有登錄過,緩存中沒有//就new一個(gè)空隊(duì)列,不然deque對(duì)象為空,會(huì)報(bào)空指針if(deque==null){deque = new LinkedList<Serializable>();}//如果隊(duì)列里沒有此sessionId,且用戶沒有被踢出;放入隊(duì)列if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {//將sessionId存入隊(duì)列 deque.push(sessionId);//將用戶的sessionId隊(duì)列緩存 cache.put(username, deque);}//如果隊(duì)列里的sessionId數(shù)超出最大會(huì)話數(shù),開始踢人while(deque.size() > maxSession) {Serializable kickoutSessionId = null;if(kickoutAfter) { //如果踢出后者kickoutSessionId = deque.removeFirst();//踢出后再更新下緩存隊(duì)列 cache.put(username, deque);} else { //否則踢出前者kickoutSessionId = deque.removeLast();//踢出后再更新下緩存隊(duì)列 cache.put(username, deque);}try {//獲取被踢出的sessionId的session對(duì)象Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));if(kickoutSession != null) {//設(shè)置會(huì)話的kickout屬性表示踢出了kickoutSession.setAttribute("kickout", true);}} catch (Exception e) {//ignore exception }}//如果被踢出了,直接退出,重定向到踢出后的地址if (session.getAttribute("kickout") != null) {//會(huì)話被踢出了try {//退出登錄 subject.logout();} catch (Exception e) { //ignore }saveRequest(request);Map<String, String> resultMap = new HashMap<String, String>();//判斷是不是Ajax請(qǐng)求if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {resultMap.put("user_status", "300");resultMap.put("message", "您已經(jīng)在其他地方登錄,請(qǐng)重新登錄!");//輸出json串 out(response, resultMap);}else{//重定向 WebUtils.issueRedirect(request, response, kickoutUrl);}return false;}return true;}private void out(ServletResponse hresponse, Map<String, String> resultMap)throws IOException {try {hresponse.setCharacterEncoding("UTF-8");PrintWriter out = hresponse.getWriter();out.println(JSON.toJSONString(resultMap));out.flush();out.close();} catch (Exception e) {System.err.println("KickoutSessionFilter.class 輸出JSON異常,可以忽略。");}} }七、異常處理類,攔截未授權(quán)頁面(未授權(quán)頁面有三種實(shí)現(xiàn)方式,我這里使用異常處理)
?
package com.chaoqi.springboot_shiro_redis.exception;import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.UnauthorizedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus;/*** 全局異常處理類*/ @ControllerAdvice public class CtrlExceptionHandler {private static Logger logger = LoggerFactory.getLogger(CtrlExceptionHandler.class);//攔截未授權(quán)頁面@ResponseStatus(value = HttpStatus.FORBIDDEN)@ExceptionHandler(UnauthorizedException.class)public String handleException(UnauthorizedException e) {logger.debug(e.getMessage());return "403";}@ResponseStatus(value = HttpStatus.FORBIDDEN)@ExceptionHandler(AuthorizationException.class)public String handleException2(AuthorizationException e) {logger.debug(e.getMessage());return "403";} }?八、最后附上logincontroller的代碼,調(diào)用login就可以調(diào)到登陸頁面
package com.chaoqi.springboot_shiro_redis.web;import com.chaoqi.springboot_shiro_redis.dao.domain.SysUser; import com.chaoqi.springboot_shiro_redis.utils.RequestUtils; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.DisabledAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod;import javax.servlet.http.HttpServletRequest;@Controller @RequestMapping(value = "/auth") public class LoginController {@RequestMapping(value = "/login", method = RequestMethod.POST)public String submitLogin(String username, String password, HttpServletRequest request) {try {UsernamePasswordToken token = new UsernamePasswordToken(username, password);Subject subject = SecurityUtils.getSubject();subject.login(token);SysUser user = (SysUser) subject.getPrincipal();} catch (DisabledAccountException e) {request.setAttribute("msg", "賬戶已被禁用");return "login";} catch (AuthenticationException e) {request.setAttribute("msg", "用戶名或密碼錯(cuò)誤");return "login";}// 執(zhí)行到這里說明用戶已登錄成功return "redirect:/auth/index";}@RequestMapping(value = "/login", method = RequestMethod.GET)public String loginPage() {return "login";}@RequestMapping(value = "/index", method = RequestMethod.GET)public String loginSuccessMessage(HttpServletRequest request) {String username = "未登錄";SysUser currentLoginUser = RequestUtils.currentLoginUser();if (currentLoginUser != null && StringUtils.isNotEmpty(currentLoginUser.getUserName())) {username = currentLoginUser.getUserName();} else {return "redirect:/auth/login";}request.setAttribute("username", username);return "index";}//被踢出后跳轉(zhuǎn)的頁面@RequestMapping(value = "/kickout", method = RequestMethod.GET)public String kickOut() {return "kickout";} }至此shiro整合完成,源碼下載地址為:https://github.com/caicahoqi/ChaoqiIsPrivateLibrary?如果在項(xiàng)目搭建中遇到問題可以在評(píng)論區(qū)留言,博主看到第一時(shí)間會(huì)給予回復(fù),謝謝
轉(zhuǎn)載于:https://www.cnblogs.com/caichaoqi/p/8900677.html
總結(jié)
以上是生活随笔為你收集整理的springboot+shiro+redis项目整合的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 熔断器---Hystrix
- 下一篇: 为什么会有这么多python?其实pyt