• Springboot2.0 集成shiro权限管理


    在springboot中结合shiro搭建权限管理,其中几个小细节的地方对新手不友好,搭建过程容易遇坑,记录一下。关键的地方也给注释了。

    版本:springboot版本2.x,shiro1.4

    一、依赖

    <dependency>
       <groupId>org.apache.shiro</groupId>
       <artifactId>shiro-spring</artifactId>
       <version>1.4.0</version>
    </dependency>

    二、实体

    实体有三个,根据规则会自动生成两个中间表,数据库实际上会生成5张表。

    分别是User,SysRole,SysPermission,中间表按照@JoinTable来生成。

    @Entity
    public class User {
        @Id
        @GenericGenerator(name="generator",strategy = "native")
        @GeneratedValue(generator = "generator")
        private Integer userId;
        @Column(nullable = false, unique = true)
        private String userName; //登录用户名
        @Column(nullable = false)
        private String name;//名称(昵称或者真实姓名,根据实际情况定义)
        @Column(nullable = false)
        private String password;
        private String salt;//加密密码的盐
        private byte state;//用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定.
        @ManyToMany(fetch= FetchType.EAGER)//立即从数据库中进行加载数据;
        @JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "userId") }, inverseJoinColumns ={@JoinColumn(name = "roleId") })
        private List<SysRole> roleList;// 一个用户具有多个角色
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
        private LocalDateTime createTime;//创建时间
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        private LocalDate expiredDate;//过期日期
        private String email;
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    
        public LocalDateTime getCreateTime() {
            return createTime;
        }
    
        public void setCreateTime(LocalDateTime createTime) {
            this.createTime = createTime;
        }
    
        public LocalDate getExpiredDate() {
            return expiredDate;
        }
    
        public void setExpiredDate(LocalDate expiredDate) {
            this.expiredDate = expiredDate;
        }
    
        public Integer getUserId() {
            return userId;
        }
    
        public void setUserId(Integer userId) {
            this.userId = userId;
        }
    
        public String getUserName() {
            return userName;
        }
    
        public void setUserName(String userName) {
            this.userName = userName;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public String getSalt() {
            return salt;
        }
    
        public void setSalt(String salt) {
            this.salt = salt;
        }
    
        public byte getState() {
            return state;
        }
    
        public void setState(byte state) {
            this.state = state;
        }
    
        public List<SysRole> getRoleList() {
            return roleList;
        }
    
        public void setRoleList(List<SysRole> roleList) {
            this.roleList = roleList;
        }
    
        /**
         * 密码盐. 重新对盐重新进行了定义,用户名+salt,这样就不容易被破解,可以采用多种方式定义加盐
         * @return
         */
        public String getCredentialsSalt(){
            return this.userName+this.salt;
        }
    
    }
    @Entity
    public class SysRole {
        @Id
        @GenericGenerator(name="generator",strategy = "native")
        @GeneratedValue(generator = "generator")
        private Integer roleId; // 编号
        @Column(nullable = false, unique = true)
        private String role; // 角色标识程序中判断使用,如"admin",这个是唯一的:
        private String description; // 角色描述,UI界面显示使用
        private Boolean available = Boolean.TRUE; // 是否可用,如果不可用将不会添加给用户
    
        //角色 -- 权限关系:多对多关系;
        @ManyToMany(fetch= FetchType.EAGER)
        @JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")})
        private List<SysPermission> permissions;
    
        // 用户 - 角色关系定义;
        @ManyToMany
        @JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="userId")})
        private List<User> users;// 一个角色对应多个用户
    
        public Integer getRoleId() {
            return roleId;
        }
    
        public void setRoleId(Integer roleId) {
            this.roleId = roleId;
        }
    
        public String getRole() {
            return role;
        }
    
        public void setRole(String role) {
            this.role = role;
        }
    
        public String getDescription() {
            return description;
        }
    
        public void setDescription(String description) {
            this.description = description;
        }
    
        public Boolean getAvailable() {
            return available;
        }
    
        public void setAvailable(Boolean available) {
            this.available = available;
        }
    
        public List<SysPermission> getPermissions() {
            return permissions;
        }
    
        public void setPermissions(List<SysPermission> permissions) {
            this.permissions = permissions;
        }
    
        public List<User> getUsers() {
            return users;
        }
    
        public void setUsers(List<User> users) {
            this.users = users;
        }
    }
    @Entity
    public class SysPermission {
        @Id
        @GenericGenerator(name="generator",strategy = "native")
        @GeneratedValue(generator = "generator")
        private Integer permissionId;//主键.
        @Column(nullable = false)
        private String permissionName;//名称.
        @Column(columnDefinition="enum('menu','button')")
        private String resourceType;//资源类型,[menu|button]
        private String url;//资源路径.
        private String permission; //权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
        private Long parentId; //父编号
        private String parentIds; //父编号列表
        private Boolean available = Boolean.TRUE;
        //角色 -- 权限关系:多对多关系;
        @ManyToMany
        @JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")})
        private List<SysRole> roles;
    
        public Integer getPermissionId() {
            return permissionId;
        }
    
        public void setPermissionId(Integer permissionId) {
            this.permissionId = permissionId;
        }
    
        public String getPermissionName() {
            return permissionName;
        }
    
        public void setPermissionName(String permissionName) {
            this.permissionName = permissionName;
        }
    
        public String getResourceType() {
            return resourceType;
        }
    
        public void setResourceType(String resourceType) {
            this.resourceType = resourceType;
        }
    
        public String getUrl() {
            return url;
        }
    
        public void setUrl(String url) {
            this.url = url;
        }
    
        public String getPermission() {
            return permission;
        }
    
        public void setPermission(String permission) {
            this.permission = permission;
        }
    
        public Long getParentId() {
            return parentId;
        }
    
        public void setParentId(Long parentId) {
            this.parentId = parentId;
        }
    
        public String getParentIds() {
            return parentIds;
        }
    
        public void setParentIds(String parentIds) {
            this.parentIds = parentIds;
        }
    
        public Boolean getAvailable() {
            return available;
        }
    
        public void setAvailable(Boolean available) {
            this.available = available;
        }
    
        public List<SysRole> getRoles() {
            return roles;
        }
    
        public void setRoles(List<SysRole> roles) {
            this.roles = roles;
        }
    }

    三、DAO,这里用JPA

    public interface UserRepository extends JpaRepository<User,Integer> {
        User findByUserName(String userName);
    }

    依赖是

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>


    四、Service

    public interface UserService {
        User findByUserName(String userName);
    }
    
    public interface LoginService {
        LoginResult login(String userName,String password);
        void logout();
    }

    五、Service.impl

    @Service
    public class UserServiceImpl implements UserService {
        @Resource
        private UserRepository userRepository;
        @Override
        public User findByUserName(String userName) {
            return userRepository.findByUserName(userName);
        }
    }

    //内部使用的一个model,根据需要扩展

    public class LoginResult {
        private boolean isLogin = false;
        private String result;
    
        public boolean isLogin() {
            return isLogin;
        }
    
        public void setLogin(boolean login) {
            isLogin = login;
        }
    
        public String getResult() {
            return result;
        }
    
        public void setResult(String result) {
            this.result = result;
        }
    }
    @Service
    public class LoginServiceImpl implements LoginService {
        @Override
        public LoginResult login(String userName, String password) {
            LoginResult loginResult = new LoginResult();
            if(userName==null || userName.isEmpty())
            {
                loginResult.setLogin(false);
                loginResult.setResult("用户名为空");
                return loginResult;
            }
            String msg="";
            // 1、获取Subject实例对象
            Subject currentUser = SecurityUtils.getSubject();
    
    //        // 2、判断当前用户是否登录
    //        if (currentUser.isAuthenticated() == false) {
    //
    //        }
    
            // 3、将用户名和密码封装到UsernamePasswordToken
            UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
    
            // 4、认证
            try {
                currentUser.login(token);// 传到MyAuthorizingRealm类中的方法进行认证
                Session session = currentUser.getSession();
                session.setAttribute("userName", userName);
                loginResult.setLogin(true);
                return loginResult;
                //return "/index";
            }catch (UnknownAccountException e)
            {
                e.printStackTrace();
                msg = "UnknownAccountException -- > 账号不存在:";
            }
            catch (IncorrectCredentialsException e)
            {
                msg = "IncorrectCredentialsException -- > 密码不正确:";
            }
            catch (AuthenticationException e) {
                e.printStackTrace();
                msg="用户验证失败";
            }
    
            loginResult.setLogin(false);
            loginResult.setResult(msg);
    
            return loginResult;
        }
    
        @Override
        public void logout() {
            Subject subject = SecurityUtils.getSubject();
            subject.logout();
        }
    }

    六、config,配置类

    shiro核心配置

    public class MyShiroRealm extends AuthorizingRealm {
        @Resource
        private UserService userService;
    
        //权限信息,包括角色以及权限
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            //如果身份认证的时候没有传入User对象,这里只能取到userName
            //也就是SimpleAuthenticationInfo构造的时候第一个参数传递需要User对象
            User user  = (User)principals.getPrimaryPrincipal();
    
            for(SysRole role:user.getRoleList()){
                authorizationInfo.addRole(role.getRole());
                for(SysPermission p:role.getPermissions()){
                    authorizationInfo.addStringPermission(p.getPermission());
                }
            }
            return authorizationInfo;
        }
    
        /*主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。*/
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
                throws AuthenticationException {
            System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
            //获取用户的输入的账号.
            String userName = (String)token.getPrincipal();
            System.out.println(token.getCredentials());
            //通过username从数据库中查找 User对象.
            //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
            User user = userService.findByUserName(userName);
            System.out.println("----->>user="+user);
            if(user == null){
                return null;
            }
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                    user, //这里传入的是user对象,比对的是用户名,直接传入用户名也没错,但是在授权部分就需要自己重新从数据库里取权限
                    user.getPassword(), //密码
                    ByteSource.Util.bytes(user.getCredentialsSalt()),//salt=username+salt
                    getName()  //realm name
            );
            return authenticationInfo;
        }
    
    }
    @Configuration
    public class ShiroConfig {
       @Bean
       public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
          System.out.println("ShiroConfiguration.shirFilter()");
          ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
          shiroFilterFactoryBean.setSecurityManager(securityManager);
          //拦截器.
          Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
          // 配置不会被拦截的链接 顺序判断,因为前端模板采用了thymeleaf,这里不能直接使用 ("/static/**", "anon")来配置匿名访问,必须配置到每个静态目录
          filterChainDefinitionMap.put("/css/**", "anon");
          filterChainDefinitionMap.put("/fonts/**", "anon");
            filterChainDefinitionMap.put("/img/**", "anon");
            filterChainDefinitionMap.put("/js/**", "anon");
            filterChainDefinitionMap.put("/html/**", "anon");
          //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
          filterChainDefinitionMap.put("/logout", "logout");
          //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
          //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
          filterChainDefinitionMap.put("/**", "authc");
          // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
          shiroFilterFactoryBean.setLoginUrl("/login");
          // 登录成功后要跳转的链接
          shiroFilterFactoryBean.setSuccessUrl("/index");
    
          //未授权界面;
          shiroFilterFactoryBean.setUnauthorizedUrl("/403");
          shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
          return shiroFilterFactoryBean;
       }
    
       /**
        * 凭证匹配器
        * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
        * )
        * @return
        */
       @Bean
       public HashedCredentialsMatcher hashedCredentialsMatcher(){
          HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
          hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
          hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
          return hashedCredentialsMatcher;
       }
    
       @Bean
       public MyShiroRealm myShiroRealm(){
          MyShiroRealm myShiroRealm = new MyShiroRealm();
          myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
          return myShiroRealm;
       }
    
    
       @Bean
       public SecurityManager securityManager(){
          DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
          securityManager.setRealm(myShiroRealm());
          return securityManager;
       }
    
       /**
        *  开启shiro aop注解支持.
        *  使用代理方式;所以需要开启代码支持;
        * @param securityManager
        * @return
        */
       @Bean
       public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
          AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
          authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
          return authorizationAttributeSourceAdvisor;
       }
    
       @Bean(name="simpleMappingExceptionResolver")
       public SimpleMappingExceptionResolver
       createSimpleMappingExceptionResolver() {
          SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
          Properties mappings = new Properties();
          mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理
          mappings.setProperty("UnauthorizedException","/user/403");
          r.setExceptionMappings(mappings);  // None by default
          r.setDefaultErrorView("error");    // No default
          r.setExceptionAttribute("exception");     // Default is "exception"
          //r.setWarnLogCategory("example.MvcLogger");     // No default
          return r;
       }
    }

    七、controller

    HomeController用来处理登录

    @Controller
    public class HomeController {
        @Resource
        private LoginService loginService;
    
        @RequestMapping({"/","/index"})
        public String index(){
            return"/index";
        }
    
        @RequestMapping("/403")
        public String unauthorizedRole(){
            System.out.println("------没有权限-------");
            return "/user/403";
        }
    
        @RequestMapping(value = "/login",method = RequestMethod.GET)
        public String toLogin(Map<String, Object> map,HttpServletRequest request)
        {
            loginService.logout();
            return "/user/login";
        }
    
        @RequestMapping(value = "/login",method = RequestMethod.POST)
        public String login(Map<String, Object> map,HttpServletRequest request) throws Exception{
            System.out.println("login()");
            String userName = request.getParameter("userName");
            String password = request.getParameter("password");
    
            LoginResult loginResult = loginService.login(userName,password);
            if(loginResult.isLogin())
            {
                return "/index";
            }
            else {
                map.put("msg",loginResult.getResult());
                map.put("userName",userName);
                return "/user/login";
            }
        }
    
        @RequestMapping("/logout")
        public String logOut(HttpSession session) {
            loginService.logout();
            return "/user/login";
        }
    }

    UserController用来测试访问,权限全部采用注解的方式。

    @Controller
    @RequestMapping("/user")
    public class UserController {
        /**
         * 用户查询.
         * @return
         */
        @RequestMapping("/userList")
        @RequiresPermissions("user:view")//权限管理;
        public String userInfo(){
            return "userList";
        }
    
        /**
         * 用户添加;
         * @return
         */
        @RequestMapping("/userAdd")
        @RequiresPermissions("user:add")//权限管理;
        public String userInfoAdd(){
            return "userAdd";
        }
    
        /**
         * 用户删除;
         * @return
         */
        @RequestMapping("/userDel")
        @RequiresPermissions("user:del")//权限管理;
        public String userDel(){
            return "userDel";
        }
    }

    八、html

    Login.html

    <!DOCTYPE html>
    <html lang="en"
          xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="../static/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}"></link>
    
    
        <title>用户登录</title>
    
        <style>
            .bgColor{
                background-color:rgba(243,66,111,0.15)
            }
            .divBorder{
                border: solid 1px rgba(12,24,255,0.15);
                padding: 10px;
                margin-top: 10px;
                border-radius: 10px;
                text-align: center;
                vertical-align: middle;
            }
            .h4font{
                margin-top: 0px;
                font-family: 微软雅黑;
                font-weight: 500;
            }
    
            .center {
               padding: 20% 0;
            }
    
    
    
        </style>
    
    </head>
    <body>
    
    <div class="container">
        <div class="row center">
            <div class="divBorder col-sm-offset-3 col-sm-6">
                <h3 class="panel panel-heading h4font">
                    用户登录
                </h3>
                <h4 th:text="${msg}"></h4>
                <form class="form-horizontal" th:action="@{/login}" method="post">
                    <div class="input-group input-group-lg">
                        <span class="input-group-addon"><i class="glyphicon glyphicon-user" aria-hidden="true"></i></span>
                        <input type="text" class="form-control" id="userName" name="userName" placeholder="请输入用户名称" th:value="${userName}"/>
                    </div>
                    <br>
                    <div class="input-group input-group-lg">
                        <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i></span>
                        <input type="password" class="form-control" id="password" name="password" placeholder="请输入密码"/>
                    </div>
    
    
                    <br>
                    <input type="submit" class="btn btn-lg btn-block btn-info" value="登 录">
                </form>
            </div>
    
    
        </div>
    
    </div>
    
    
    
    
    </body>
    </html>

    九、数据库里预设一些数据

    注意admin的密码是123456,这里保存的是加密后的密码,根据前面的设置,是md5,散列2次。

    登录的时候shiro会根据配置自动给密码123456加密,然后与数据库里取出的密码比对。

    注意先运行一遍程序,JPA生成数据库表后,手工执行sql脚本插入样本数据。

    INSERT INTO `user` (`userId`,`username`,`name`,`password`,`salt`,`state`)
    VALUES ('1', 'admin', '管理员', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0);
    
    INSERT INTO `syspermission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`)
    VALUES (1,0,'用户管理',0,'0/','user:view','menu','user/userList');
    INSERT INTO `syspermission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`)
    VALUES (2,0,'用户添加',1,'0/1','user:add','button','user/userAdd');
    INSERT INTO `syspermission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`)
    VALUES (3,0,'用户删除',1,'0/1','user:del','button','user/userDel');
    
    INSERT INTO `sysrole` (`roleid`,`available`,`description`,`role`) VALUES (1,0,'管理员','admin');
    INSERT INTO `sysrole` (`roleid`,`available`,`description`,`role`) VALUES (2,0,'VIP会员','vip');
    INSERT INTO `sysrole` (`roleid`,`available`,`description`,`role`) VALUES (3,1,'test','test');
    
    INSERT INTO `sysrolepermission` (`permissionid`,`roleid`) VALUES (1,1);
    INSERT INTO `sysrolepermission` (`permissionid`,`roleid`) VALUES (2,1);
    INSERT INTO `sysrolepermission` (`permissionid`,`roleid`) VALUES (3,2);
    
    INSERT INTO `sysuserrole` (`roleid`,`userId`) VALUES (1,1);

    十、其他配置

    Application.yml

    server:
      servlet:
        context-path: /
      port: 80
    
    spring:
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/crmData?characterEncoding=utf8&useSSL=false
        username: root
        password: root
      jpa:
        hibernate:
          ddl-auto: update
          naming:
            physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl #按字段名字建表
        show-sql: true
        database: mysql
        database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    
      thymeleaf:
        cache: false
    
      messages:
        basename: myconfig

    所有依赖:

        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
    
            <dependency>
                <groupId>nz.net.ultraq.thymeleaf</groupId>
                <artifactId>thymeleaf-layout-dialect</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.thymeleaf.extras</groupId>
                <artifactId>thymeleaf-extras-java8time</artifactId>
                <!--<version>3.0.1.RELEASE</version>-->
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
                <scope>compile</scope>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
            </dependency>
    
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <optional>true</optional>
            </dependency>
    
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <version>1.4.0</version>
            </dependency>
        </dependencies>

    目录图:

    其他的html页面自己随便生成就可以。

    到此基本实现简单的权限管理。

    代码可以参考这个地址:https://github.com/asker124143222/bus

  • 相关阅读:
    安装EPP的调试Zend Debugger
    PHP一周学习小结
    工作小记
    JQuery实现日期联动
    编程珠玑位图法排序
    Asp.net MVC 4 异步方法
    Powershell实现创建zip压缩文件
    使用JustDecompile修改程序集
    Asp.net MVC 3 异步操作
    HTAs文件应用
  • 原文地址:https://www.cnblogs.com/asker009/p/9471712.html
Copyright © 2020-2023  润新知