how2j.cn

-->
下载区
文件名 文件大小
shiro.rar 12m

解压rar如果失败,请用5.21版本或者更高版本的winrar

点击下载 winrar5.21
步骤 1 : 基于URL配置权限   
步骤 2 : 表中数据   
步骤 3 : 先运行,看到效果,再学习   
步骤 4 : 模仿和排错   
步骤 5 : 基于前面的知识点进行   
步骤 6 : PageController.java   
步骤 7 : PermissionService.java   
步骤 8 : PermissionServiceImpl.java   
步骤 9 : URLPathMatchingFilter.java   
步骤 10 : applicationContext-shiro.xml   
步骤 11 : jsp 们   
步骤 12 : 重启Tomcat测试   
步骤 13 : 关于角色   

步骤 1 :

基于URL配置权限

edit
在本知识点就做到,如何通过URL配置来灵活设置权限,而不是非要在Controller里通过注解来做了
在学习上个知识点权限维护一套 的过程中,同学们多半会自己修改表中数据,为了能够重现本知识点所演示的效果,请把下面数据再次导入到数据库中,否则因为数据不一致,演示的效果就会有出入,影响学习
DROP DATABASE IF EXISTS shiro; CREATE DATABASE shiro DEFAULT CHARACTER SET utf8; USE shiro; drop table if exists user; drop table if exists role; drop table if exists permission; drop table if exists user_role; drop table if exists role_permission; create table user ( id bigint auto_increment, name varchar(100), password varchar(100), salt varchar(100), constraint pk_users primary key(id) ) charset=utf8 ENGINE=InnoDB; create table role ( id bigint auto_increment, name varchar(100), desc_ varchar(100), constraint pk_roles primary key(id) ) charset=utf8 ENGINE=InnoDB; create table permission ( id bigint auto_increment, name varchar(100), desc_ varchar(100), url varchar(100), constraint pk_permissions primary key(id) ) charset=utf8 ENGINE=InnoDB; create table user_role ( id bigint auto_increment, uid bigint, rid bigint, constraint pk_users_roles primary key(id) ) charset=utf8 ENGINE=InnoDB; create table role_permission ( id bigint auto_increment, rid bigint, pid bigint, constraint pk_roles_permissions primary key(id) ) charset=utf8 ENGINE=InnoDB; INSERT INTO `permission` VALUES (1,'addProduct','增加产品','/addProduct'); INSERT INTO `permission` VALUES (2,'deleteProduct','删除产品','/deleteProduct'); INSERT INTO `permission` VALUES (3,'editeProduct','编辑产品','/editeProduct'); INSERT INTO `permission` VALUES (4,'updateProduct','修改产品','/updateProduct'); INSERT INTO `permission` VALUES (5,'listProduct','查看产品','/listProduct'); INSERT INTO `permission` VALUES (6,'addOrder','增加订单','/addOrder'); INSERT INTO `permission` VALUES (7,'deleteOrder','删除订单','/deleteOrder'); INSERT INTO `permission` VALUES (8,'editeOrder','编辑订单','/editeOrder'); INSERT INTO `permission` VALUES (9,'updateOrder','修改订单','/updateOrder'); INSERT INTO `permission` VALUES (10,'listOrder','查看订单','/listOrder'); INSERT INTO `role` VALUES (1,'admin','超级管理员'); INSERT INTO `role` VALUES (2,'productManager','产品管理员'); INSERT INTO `role` VALUES (3,'orderManager','订单管理员'); INSERT INTO `role_permission` VALUES (1,1,1); INSERT INTO `role_permission` VALUES (2,1,2); INSERT INTO `role_permission` VALUES (3,1,3); INSERT INTO `role_permission` VALUES (4,1,4); INSERT INTO `role_permission` VALUES (5,1,5); INSERT INTO `role_permission` VALUES (6,1,6); INSERT INTO `role_permission` VALUES (7,1,7); INSERT INTO `role_permission` VALUES (8,1,8); INSERT INTO `role_permission` VALUES (9,1,9); INSERT INTO `role_permission` VALUES (10,1,10); INSERT INTO `role_permission` VALUES (11,2,1); INSERT INTO `role_permission` VALUES (12,2,2); INSERT INTO `role_permission` VALUES (13,2,3); INSERT INTO `role_permission` VALUES (14,2,4); INSERT INTO `role_permission` VALUES (15,2,5); INSERT INTO `role_permission` VALUES (50,3,10); INSERT INTO `role_permission` VALUES (51,3,9); INSERT INTO `role_permission` VALUES (52,3,8); INSERT INTO `role_permission` VALUES (53,3,7); INSERT INTO `role_permission` VALUES (54,3,6); INSERT INTO `role_permission` VALUES (55,3,1); INSERT INTO `role_permission` VALUES (56,5,11); INSERT INTO `user` VALUES (1,'zhang3','a7d59dfc5332749cb801f86a24f5f590','e5ykFiNwShfCXvBRPr3wXg=='); INSERT INTO `user` VALUES (2,'li4','43e28304197b9216e45ab1ce8dac831b','jPz19y7arvYIGhuUjsb6sQ=='); INSERT INTO `user_role` VALUES (43,2,2); INSERT INTO `user_role` VALUES (45,1,1);
步骤 3 :

先运行,看到效果,再学习

edit
老规矩,先下载右上角的可运行项目,配置运行起来,确认可用之后,再学习做了哪些步骤以达到这样的效果。
通过如下地址登录:

http://127.0.0.1:8080/shiro/login

登录之后,可以看到如图所示:
zhang3因为是超级管理员admin, 有所有的权限,所以都能访问
li4 因为是productManager,所以可以访问前两个产品相关权限的,但是不能访问第三个订单相关权限的地址
而以上的做法,都不是通过固定写在方法上的注解实现的,而是通过权限灵活配置实现的
先运行,看到效果,再学习
在确保可运行项目能够正确无误地运行之后,再严格照着教程的步骤,对代码模仿一遍。
模仿过程难免代码有出入,导致无法得到期望的运行结果,此时此刻通过比较正确答案 ( 可运行项目 ) 和自己的代码,来定位问题所在。
采用这种方式,学习有效果,排错有效率,可以较为明显地提升学习速度,跨过学习路上的各个槛。

推荐使用diffmerge软件,进行文件夹比较。把你自己做的项目文件夹,和我的可运行项目文件夹进行比较。
这个软件很牛逼的,可以知道文件夹里哪两个文件不对,并且很明显地标记出来
这里提供了绿色安装和使用教程:diffmerge 下载和使用教程
步骤 5 :

基于前面的知识点进行

edit
本知识点基于 权限维护一套 扩展出来,接下来讲解做了哪些工作达到这样的效果
步骤 6 :

PageController.java

edit
首先是PageController.java 里原本通过注解方式的@RequiresPermissions和@RequiresRoles 注释掉了
package com.how2java.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; //专门用于显示页面的控制器 @Controller @RequestMapping("") public class PageController { @RequestMapping("index") public String index(){ return "index"; } // @RequiresPermissions("deleteOrder") @RequestMapping("deleteOrder") public String deleteOrder(){ return "deleteOrder"; } // @RequiresRoles("productManager") @RequestMapping("deleteProduct") public String deleteProduct(){ return "deleteProduct"; } @RequestMapping("listProduct") public String listProduct(){ return "listProduct"; } @RequestMapping(value="/login",method=RequestMethod.GET) public String login(){ return "login"; } @RequestMapping("unauthorized") public String noPerms(){ return "unauthorized"; } }
步骤 7 :

PermissionService.java

edit
增加了两个方法 needInterceptor,listPermissionURLs
package com.how2java.service; import java.util.List; import java.util.Set; import com.how2java.pojo.Permission; import com.how2java.pojo.Role; public interface PermissionService { public Set<String> listPermissions(String userName); public List<Permission> list(); public void add(Permission permission); public void delete(Long id); public Permission get(Long id); public void update(Permission permission); public List<Permission> list(Role role); public boolean needInterceptor(String requestURI); public Set<String> listPermissionURLs(String userName); }
步骤 8 :

PermissionServiceImpl.java

edit
为两个方法 needInterceptor,listPermissionURLs 增加了实现
needInterceptor 表示是否要进行拦截,判断依据是如果访问的某个url,在权限系统里存在,就要进行拦截。 如果不存在,就放行了。
这一种策略,也可以切换成另一个,即,访问的地址如果不存在于权限系统中,就提示没有拦截。 这两种做法没有对错之分,取决于业务上希望如何制定权限策略。

listPermissionURLs(User user) 用来获取某个用户所拥有的权限地址集合
package com.how2java.service.impl; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.how2java.mapper.PermissionMapper; import com.how2java.mapper.RolePermissionMapper; import com.how2java.pojo.Permission; import com.how2java.pojo.PermissionExample; import com.how2java.pojo.Role; import com.how2java.pojo.RolePermission; import com.how2java.pojo.RolePermissionExample; import com.how2java.service.PermissionService; import com.how2java.service.RoleService; import com.how2java.service.UserService; @Service public class PermissionServiceImpl implements PermissionService { @Autowired PermissionMapper permissionMapper; @Autowired UserService userService; @Autowired RoleService roleService; @Autowired RolePermissionMapper rolePermissionMapper; @Override public Set<String> listPermissions(String userName) { Set<String> result = new HashSet<>(); List<Role> roles = roleService.listRoles(userName); List<RolePermission> rolePermissions = new ArrayList<>(); for (Role role : roles) { RolePermissionExample example = new RolePermissionExample(); example.createCriteria().andRidEqualTo(role.getId()); List<RolePermission> rps = rolePermissionMapper.selectByExample(example); rolePermissions.addAll(rps); } for (RolePermission rolePermission : rolePermissions) { Permission p = permissionMapper.selectByPrimaryKey(rolePermission.getPid()); result.add(p.getName()); } return result; } @Override public void add(Permission u) { permissionMapper.insert(u); } @Override public void delete(Long id) { permissionMapper.deleteByPrimaryKey(id); } @Override public void update(Permission u) { permissionMapper.updateByPrimaryKeySelective(u); } @Override public Permission get(Long id) { return permissionMapper.selectByPrimaryKey(id); } @Override public List<Permission> list() { PermissionExample example = new PermissionExample(); example.setOrderByClause("id desc"); return permissionMapper.selectByExample(example); } @Override public List<Permission> list(Role role) { List<Permission> result = new ArrayList<>(); RolePermissionExample example = new RolePermissionExample(); example.createCriteria().andRidEqualTo(role.getId()); List<RolePermission> rps = rolePermissionMapper.selectByExample(example); for (RolePermission rolePermission : rps) { result.add(permissionMapper.selectByPrimaryKey(rolePermission.getPid())); } return result; } @Override public boolean needInterceptor(String requestURI) { List<Permission> ps = list(); for (Permission p : ps) { if (p.getUrl().equals(requestURI)) return true; } return false; } @Override public Set<String> listPermissionURLs(String userName) { Set<String> result = new HashSet<>(); List<Role> roles = roleService.listRoles(userName); List<RolePermission> rolePermissions = new ArrayList<>(); for (Role role : roles) { RolePermissionExample example = new RolePermissionExample(); example.createCriteria().andRidEqualTo(role.getId()); List<RolePermission> rps = rolePermissionMapper.selectByExample(example); rolePermissions.addAll(rps); } for (RolePermission rolePermission : rolePermissions) { Permission p = permissionMapper.selectByPrimaryKey(rolePermission.getPid()); result.add(p.getUrl()); } return result; } }
步骤 9 :

URLPathMatchingFilter.java

edit
PathMatchingFilter 是shiro 内置过滤器 PathMatchingFilter 继承了这个它。
基本思路如下:
1. 如果没登录就跳转到登录
2. 如果当前访问路径没有在权限系统里维护,则允许访问
3. 当前用户所拥有的权限如果不包含当前的访问地址,则跳转到/unauthorized,否则就允许访问
package com.how2java.filter; import java.util.Set; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.PathMatchingFilter; import org.apache.shiro.web.util.WebUtils; import org.springframework.beans.factory.annotation.Autowired; import com.how2java.service.PermissionService; public class URLPathMatchingFilter extends PathMatchingFilter { @Autowired PermissionService permissionService; @Override protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { String requestURI = getPathWithinApplication(request); System.out.println("requestURI:" + requestURI); Subject subject = SecurityUtils.getSubject(); // 如果没有登录,就跳转到登录页面 if (!subject.isAuthenticated()) { WebUtils.issueRedirect(request, response, "/login"); return false; } // 看看这个路径权限里有没有维护,如果没有维护,一律放行(也可以改为一律不放行) boolean needInterceptor = permissionService.needInterceptor(requestURI); if (!needInterceptor) { return true; } else { boolean hasPermission = false; String userName = subject.getPrincipal().toString(); Set<String> permissionUrls = permissionService.listPermissionURLs(userName); for (String url : permissionUrls) { // 这就表示当前用户有这个权限 if (url.equals(requestURI)) { hasPermission = true; break; } } if (hasPermission) return true; else { UnauthorizedException ex = new UnauthorizedException("当前用户没有访问路径 " + requestURI + " 的权限"); subject.getSession().setAttribute("ex", ex); WebUtils.issueRedirect(request, response, "/unauthorized"); return false; } } } }
步骤 10 :

applicationContext-shiro.xml

edit
首先声明URLPathMatchingFilter 过滤器

<bean id="urlPathMatchingFilter" class="com.how2java.filter.URLPathMatchingFilter"/>

在shiro中使用这个过滤器

<entry key="url" value-ref="urlPathMatchingFilter" />

过滤规则是所有访问

/** = url
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <!-- url过滤器 --> <bean id="urlPathMatchingFilter" class="com.how2java.filter.URLPathMatchingFilter"/> <!-- 配置shiro的过滤器工厂类,id- shiroFilter要和我们在web.xml中配置的过滤器一致 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- 调用我们配置的权限管理器 --> <property name="securityManager" ref="securityManager" /> <!-- 配置我们的登录请求地址 --> <property name="loginUrl" value="/login" /> <!-- 如果您请求的资源不再您的权限范围,则跳转到/403请求地址 --> <property name="unauthorizedUrl" value="/unauthorized" /> <!-- 退出 --> <property name="filters"> <util:map> <entry key="logout" value-ref="logoutFilter" /> <entry key="url" value-ref="urlPathMatchingFilter" /> </util:map> </property> <!-- 权限配置 --> <property name="filterChainDefinitions"> <value> <!-- anon表示此地址不需要任何权限即可访问 --> /login=anon /index=anon /static/**=anon <!-- 只对业务功能进行权限管理,权限配置本身不需要没有做权限要求,这样做是为了不让初学者混淆 --> /config/**=anon /doLogout=logout <!--所有的请求(除去配置的静态资源请求或请求地址为anon的请求)都要通过过滤器url --> /** = url </value> </property> </bean> <!-- 退出过滤器 --> <bean id="logoutFilter" class="org.apache.shiro.web.filter.authc.LogoutFilter"> <property name="redirectUrl" value="/index" /> </bean> <!-- 会话ID生成器 --> <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator" /> <!-- 会话Cookie模板 关闭浏览器立即失效 --> <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="sid" /> <property name="httpOnly" value="true" /> <property name="maxAge" value="-1" /> </bean> <!-- 会话DAO --> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"> <property name="sessionIdGenerator" ref="sessionIdGenerator" /> </bean> <!-- 会话验证调度器,每30分钟执行一次验证 ,设定会话超时及保存 --> <bean name="sessionValidationScheduler" class="org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler"> <property name="interval" value="1800000" /> <property name="sessionManager" ref="sessionManager" /> </bean> <!-- 会话管理器 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- 全局会话超时时间(单位毫秒),默认30分钟 --> <property name="globalSessionTimeout" value="1800000" /> <property name="deleteInvalidSessions" value="true" /> <property name="sessionValidationSchedulerEnabled" value="true" /> <property name="sessionValidationScheduler" ref="sessionValidationScheduler" /> <property name="sessionDAO" ref="sessionDAO" /> <property name="sessionIdCookieEnabled" value="true" /> <property name="sessionIdCookie" ref="sessionIdCookie" /> </bean> <!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="databaseRealm" /> <property name="sessionManager" ref="sessionManager" /> </bean> <!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) --> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager" /> <property name="arguments" ref="securityManager" /> </bean> <!-- 密码匹配器 --> <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="md5"/> <property name="hashIterations" value="2"/> <property name="storedCredentialsHexEncoded" value="true"/> </bean> <bean id="databaseRealm" class="com.how2java.realm.DatabaseRealm"> <property name="credentialsMatcher" ref="credentialsMatcher"/> </bean> <!-- 保证实现了Shiro内部lifecycle函数的bean执行 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> </beans>
显示用的jsp做了些文字上的改动
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> </head> <body> <div class="workingroom"> <div class="loginDiv"> <c:if test="${empty subject.principal}"> <a href="login">登录</a><br> </c:if> <c:if test="${!empty subject.principal}"> <span class="desc">你好,${subject.principal},</span> <a href="doLogout">退出</a><br> </c:if> <a href="listProduct">查看产品</a><span class="desc">(要有查看产品权限, zhang3有,li4 有)</span><br> <a href="deleteProduct">删除产品</a><span class="desc">(要有删除产品权限, zhang3有,li4 有)</span><br> <a href="deleteOrder">删除订单</a><span class="desc">(要有删除订单权限, zhang3有,li4没有)</span><br> </div> </body> </html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> deleteOrder.jsp,能进来<br>就表示拥有 deleteOrder 权限 <br> <a href="#" onClick="javascript:history.back()">返回</a> </div>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> deleteProduct.jsp,能进来<br>就表示拥有 deleteProduct 权限 <br> <a href="#" onClick="javascript:history.back()">返回</a> </div>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*"%> <!DOCTYPE html> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="static/css/style.css" /> <div class="workingroom"> listProduct.jsp,能进来<br>就表示拥有 listProduct 权限 <br> <a href="#" onClick="javascript:history.back()">返回</a> </div>
步骤 12 :

重启Tomcat测试

edit
重启Tomcat测试,业务测试地址:

http://127.0.0.1:8080/shiro/index

权限配置测试地址:

http://127.0.0.1:8080/shiro/config/listUser
在本知识点中,权限通过url进行灵活配置了。 但是角色还没有和url对应起来。 为什么不把角色也对应起来呢?
从代码开发的角度讲是可以做的,无非就是在 role表上增加一个url 字段。 但是从权限管理本身的角度看,当一个url 既对应权限表的数据,又对应角色表的数据,反而容易产生混淆。
反倒是现在这种,url地址,仅仅和权限表关联起来,在逻辑上明晰简单,也更容易维护。 所以就放弃了角色表也进行url维护的做法了。


HOW2J公众号,关注后实时获知最新的教程和优惠活动,谢谢。


问答区域    
2022-12-26 项目运行总是报无法找到一个文件
Gunyuan




运行不了,项目导入的时候总是报错,说是不能找到org.eclipse.jdt:ecj:jar:3.12.3 ,换了几个jdk和旧的eclipse也是一直报这个错
加载中

							

							





回答已经提交成功,正在审核。 请于 我的回答 处查看回答记录,谢谢
答案 或者 代码至少填写一项, 如果是自己有问题,请重新提问,否则站长有可能看不到





2021-01-23 登录和验证顺序问题
海东青

这个点击登录后先执行的拦截器,但是拦截器里有个登录状态判断,怎么先登录后去执行拦截器的验证判断呢







回答已经提交成功,正在审核。 请于 我的回答 处查看回答记录,谢谢
答案 或者 代码至少填写一项, 如果是自己有问题,请重新提问,否则站长有可能看不到




2020-09-26 Pojo的PermissionExample和RolePermissionExample没有注释
2020-09-03 我想问下浏览器的跨域请求怎么做?求各位知道的大佬或站长解答
2020-06-14 这个注册User加密怎么做啊?


提问太多,页面渲染太慢,为了加快渲染速度,本页最多只显示几条提问。还有 19 条以前的提问,请 点击查看

提问之前请登陆
提问已经提交成功,正在审核。 请于 我的提问 处查看提问记录,谢谢
关于 工具和中间件-Shiro-基于URL配置权限 的提问

尽量提供截图代码异常信息,有助于分析和解决问题。 也可进本站QQ群交流: 578362961
提问尽量提供完整的代码,环境描述,越是有利于问题的重现,您的问题越能更快得到解答。
对教程中代码有疑问,请提供是哪个步骤,哪一行有疑问,这样便于快速定位问题,提高问题得到解答的速度
在已经存在的几千个提问里,有相当大的比例,是因为使用了和站长不同版本的开发环境导致的,比如 jdk, eclpise, idea, mysql,tomcat 等等软件的版本不一致。
请使用和站长一样的版本,可以节约自己大量的学习时间。 站长把教学中用的软件版本整理了,都统一放在了这里, 方便大家下载: https://how2j.cn/k/helloworld/helloworld-version/1718.html

上传截图