shiro框架是什么_中国历史知识框架

shiro框架是什么_中国历史知识框架Shiro 1.权限管理概述2.Shiro权限框架  2.1概念  2.2ApacheShiro与SpringSecurity区别3.Shiro认证  3.1基于ini认证  3.2自定义Realm–认证4.Shiro授权  4.1基于ini授权  4.2自定义realm–授权5.项目集成shiro认证-授权注意点  5.1认证  5.2授权  5.3注解@RequiresPermissions()  5.4标签式权限验证6.S

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

Shiro

 
1.权限管理概述

2.Shiro权限框架
  2.1 概念
  2.2 Apache Shiro 与Spring Security区别

3.Shiro认证
  3.1 基于ini认证
  3.2 自定义Realm –认证

4.Shiro授权
  4.1 基于ini授权
  4.2 自定义realm – 授权

5.项目集成shiro 认证-授权注意点
  5.1 认证
  5.2 授权
  5.3 注解@RequiresPermissions()
  5.4 标签式权限验证

6.Shiro加密

7.Shiro缓存

权限管理概述

RBAC(Role Based Access Control) :某个用户拥有什么角色,被允许做什么事情(权限)

用户登录—>分配角色—->(权限关联映射)—->鉴权(拥有什么什么权限)

熟悉框架流程

  • 概念 能做什么
  • 架构是怎样
  • 代码怎么实现
  • 项目运用

Shiro权限框架

  • 概念: Apache Shiro 是一个强大且易用的 Java 安全框架
  • 能做什么:Shiro可以帮我们完成 :认证授权、加密、会话管理、与 Web 集成、缓存等。
  • 架构是怎样的

在这里插入图片描述

主要认识:

  • Subject(用户):当前的操作用户 获取当前用户Subject currentUser = SecurityUtils.getSubject()
  • SecurityManager(安全管理器):Shiro的核心,负责与其他组件进行交互,实现 subject 委托的各种功能
  • Realms(数据源) :Realm会查找相关数据源,充当与安全管理间的桥梁,经过Realm找到数据源进行认证,授权等操作
  • Authenticator(认证器): 用于认证,从 Realm 数据源取得数据之后进行执行认证流程处理。
  • Authorizer(授权器):用户访问控制授权,决定用户是否拥有执行指定操作的权限。
  • SessionManager (会话管理器):支持会话管理
  • CacheManager (缓存管理器):用于缓存认证授权信息
  • Cryptography(加密组件):提供了加密解密的工具包


Apache Shiro 与Spring Security区别

Shiro::用于中小型项目比较常见,简单易上手,可以支持多种环境
       Shiro 可以不跟任何的框架或者容器绑定,可独立运行
Spring Security:一般多用于spring环境,中大型项目,更强大
                Spring Security 则必须要有Spring环境

Shiro认证

基于ini认证

1.新建项目 --导入依赖
           --新建 配置文件ini
    
2. shiro帮我们创建用户
    创建令牌 ,类比页面传入的账号密码
    密码错误--  IncorrectCredentialsException
    账号错误--  UnknownAccountException
    
   源码分析--
      

在这里插入图片描述

  • 导入依赖
<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1.3</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.5.2</version>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.22</version>
    <scope>provided</scope>
</dependency>
  • 编写ini,shiro默认支持的是ini配置的方式(只是演示) shiro-au.ini,真实项目使用xml
#用户的身份、凭据
[users]
zhangsan=555
xiaoluo=666
  • 使用 Shiro 相关的 API 完成身份认证
@Test
public void testLogin(){ 
   
    //创建Shiro的安全管理器,是shiro的核心
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    //加载shiro.ini配置,得到配置中的用户信息(账号+密码)
    IniRealm iniRealm = new IniRealm("classpath:shiro-au.ini");
    securityManager.setRealm(iniRealm);
    //把安全管理器注入到当前的环境中
    SecurityUtils.setSecurityManager(securityManager);
    //无论有无登录都可以获取到subject主体对象,但是判断登录状态需要利用里面的属性来判断
    Subject subject = SecurityUtils.getSubject();
    System.out.println("认证状态:"+subject.isAuthenticated());
    //创建令牌(携带登录用户的账号和密码)
    UsernamePasswordToken token = new UsernamePasswordToken("xiaoluo","666");
    //执行登录操作(将用户的和 ini 配置中的账号密码做匹配)
    subject.login(token);
    System.out.println("认证状态:"+subject.isAuthenticated());
    //登出
    //subject.logout();
    //System.out.println("认证状态:"+subject.isAuthenticated());
}

自定义Realm–认证

模拟

@Getter
@Setter
public class Employee { 
   
    private String username;
    private String password;}

思路:

//继承 AuthorizingRealm
//参数的token ,subject进行登录匹配传入的token : UsernamePasswordToken
1.获取用户名
    方式一:将token强转为UsernamePasswordToken-->getUsername()
    方式二:通过token.getPrincipal()    
    
2.以用户名为条件,查询mysql数据库,得到用户对象(用户名/密码)
        模拟从数据库查询的对象 ------->自己new一个
        
3.将用户对象封装成认证info对象返回
        //存在两种可能
        1.用户对象为null-----> return null
        2.不为空 ---->封装数据 return new SimpleAuthenticationInfo(3个参数 
               1.employee ,  //从数据库查询出来需要进行密码匹配的用户对象
               2.employee.getPassword(),  //匹配对象密码
               3.super.getName()  //指定realm的名称 ,可自定义
    )

实现:

public class Realm extends AuthorizingRealm { 
   
    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { 
   
        //1.获取登录用户名
        String username = (String) token.getPrincipal();
        //2.以用户名为条件查询mysql数据库,得到用户对象
        //模拟数据
        Employee employee = new Employee();
        employee.setUsername("xiaoluo");
        employee.setPassword("123");

        //封装成一个认证info对象
        //判断用户是否为空
        if (employee!=null){ 
   
            return new SimpleAuthenticationInfo(
                    employee,
                    employee.getPassword(),
                    super.getName()
            );
        }
        return null;
    }
}
@Test
public void testLogin(){ 
   
    //创建Shiro的安全管理器,是shiro的核心
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
  
        //自定义Realm,查出用户信息
        Realm realm = new Realm();
        securityManager.setRealm(realm);
    
    //把安全管理器注入到当前的环境中
    SecurityUtils.setSecurityManager(securityManager);
    //无论有无登录都可以获取到subject主体对象,但是判断登录状态需要利用里面的属性来判断
    Subject subject = SecurityUtils.getSubject();
    System.out.println("认证状态:"+subject.isAuthenticated());
    //创建令牌(携带登录用户的账号和密码)
    UsernamePasswordToken token = new UsernamePasswordToken("xiaoluo","123");
    //执行登录操作(将用户的和 ini 配置中的账号密码做匹配)
    subject.login(token);
    System.out.println("认证状态:"+subject.isAuthenticated());
    //登出
    //subject.logout();
    //System.out.println("认证状态:"+subject.isAuthenticated());
}

Shiro授权

基于ini授权

  • 登录
  • 分配
  • 鉴权
    权限表达式   资源:操作
    admin=*:*  //超级管理员
    emp=employee:*
//判断是否有某个角色
subject.hasRole("role");
//用户拥有所有指定角色返回true
subject.hasAllRoles(Arrays.aList("role1","role2"));
//判断用户是否有指定角色,将结果返回,封装到boolean数组中
boolean[] booleans=subject.hasRoles(Arrays.aList("role1","role2"));                        
//check开头的是没有返回值,当没有权限时就会抛出异常
subject.checkRole("role"); 

//判断用户是否有某个权限
subject.isPermitted("权限表达式")
boolean[] booleans=subject.isPermitted("权限表达式1","权限表达式2")

在这里插入图片描述

自定义realm — 授权

思路:

1.获取当前登录用户的id/name
    //获取当前登录用户对象 ,登录传过来的第一个参数
    方式一 :principals.getPrimaryPrincipal();
    方式二:SecurityUtils.getSubject().getPrincipal();

2.以用户id作为条件查询mysql数据,查询该用户拥有角色/权限
    //假装查数据
    //List<String> roles= roleService.queryByEmployee(Employee.getId());
   //List<String> //List<String> roles= permissionService.queryByEmployee(Employee.getId());
= permissionService.queryByEmployee(Employee.getId());
    
    
3.将角色与权限封装到授权info对象中 
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    info.addRoles(roles) //添加角色
    info.addStringPermission(permission);//添加权限 

实现:

 //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { 
   
        //获取当前用户对象
        Employee employee = (Employee)principalCollection.getPrimaryPrincipal();

        //以用户id查询数据库,判断该角色/用户是否拥有权限 模拟
        List<String> roles= Arrays.asList("seller");
        List<String> permissions= Arrays.asList("customer:list","customer:save");

        //封装到info对象
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRoles(roles);
        info.addStringPermissions(permissions);

        return info;
    }
public class ShiroDemo { 
   
    @Test
    public void testLogin(){ 
   
        //创建Shiro的安全管理器,是shiro的核心
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        //加载shiro.ini配置,得到配置中的用户信息(账号+密码)
        IniRealm iniRealm = new IniRealm("classpath:shiro-author.ini");

        //自定义Realm,查出用户信息
        Realm realm = new Realm();
        securityManager.setRealm(realm);

        //把安全管理器注入到当前的环境中
        SecurityUtils.setSecurityManager(securityManager);
        //无论有无登录都可以获取到subject主体对象,但是判断登录状态需要利用里面的属性来判断
        Subject subject = SecurityUtils.getSubject();

        System.out.println("认证状态:"+subject.isAuthenticated());

        //创建令牌(携带登录用户的账号和密码)
        UsernamePasswordToken token = new UsernamePasswordToken("xiaoluo","12");
        //执行登录操作(将用户的和 ini 配置中的账号密码做匹配)
        subject.login(token);

        System.out.println("认证状态:"+subject.isAuthenticated());

        //登出
        //subject.logout();
        //System.out.println("认证状态:"+subject.isAuthenticated());

        //判断用户是否有某个角色
        System.out.println("hr:"+subject.hasRole("hr"));
        System.out.println("seller:"+subject.hasRole("seller"));

        //是否同时拥有多个角色
        System.out.println("是否同时拥有role1和role2:"+subject.hasAllRoles(Arrays.asList("hr", "seller")));
        boolean[] booleans = subject.hasRoles(Arrays.asList("hr", "seller"));
        //System.out.println(booleans);

        //check开头的是没有返回值的,当没有权限时就会抛出异常
        subject.checkRole("seller");

        //判断用户是否有某个权限
        System.out.println("user:delete:"+subject.isPermitted("user:delete"));
        subject.checkPermission("customer:list");
    }
}
	//如果用户是超级管理员 
    //设置超级管理员角色
   info.addRole("admin")
    //设置超级管理员权限
info.addStringPermission("*:*");

项目集成shiro 认证-授权注意点

认证

1.添加对应依赖
2.配置代理过滤器
   为什么不用配置拦截器?
   Shiro是选择使用filter过滤器来进行拦截的,因为Shiro不依赖Spring容器,所以当没有springmvc时意味着不能用拦截器,但过滤器则不同,只要是web项目都可以使用
3.创建shiro.xml
4.配置shiro过滤器
   <!--引用指定的安全管理器-->
   <!--shiro默认的登录地址是/login.jsp 现在要指定我们自己的登录页面地址-->
   <!--路径对应的规则-->
5.配置安全管理器  DefaultWebSecurityManager
6.修改LoginController
7.配置自定义Realm
8.将自定义Realm交给容器管理


shiro中的过滤器

过滤器的名称 Java
anon org.apache.shiro.web. lter.authc.AnonymousFilter
authc org.apache.shiro.web. lter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web. lter.authc.BasicHttpAuthenticationFilter
roles org.apache.shiro.web. lter.authz.RolesAuthorizationFilter
perms org.apache.shiro.web. lter.authz.PermissionsAuthorizationFilter
user org.apache.shiro.web. lter.authc.UserFilter
logout org.apache.shiro.web. lter.authc.LogoutFilter
port org.apache.shiro.web. lter.authz.PortFilter
rest org.apache.shiro.web. lter.authz.HttpMethodPermissionFilter
ssl org.apache.shiro.web. lter.authz.SslFilter
  • anon: 匿名处理过滤器,即不需要登录即可访问;一般用于静态资源过滤;/static/**=anon
  • authc: 表示需要认证(登录)才能使用;(放最后) /**=authc
  • logout: 注销过滤器 /logout=logout
  • roles: 角色授权过滤器,验证用户是否拥有资源角色; /employee/input=perms["user:update"]

授权

  • 没有权限的异常 :org.apache.shiro.authz.UnauthorizedException
shiro注解鉴权操作方式:
类比RBAC:
 1.自定义权限注解
 2.将注解贴在请求映射方法上面
 3.将注解标注的权限表达式加载到数据库中
 4.将这些表达式根据用户角色进行权限分配
 5.当用户登录之后,访问某个请求映射方法时,先经过权限拦截器,进行鉴权操作
     1.获取当前登录用户权限表达式集合
     2.获取当前请求映射方法头顶上权限表达式
     3.判断用户权限表达式集合中是否包含该表达式

Shiro 权限验证三种方式

  • 编程式
  • 注解式
  • 页面标签式
1.编程式
    Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("hr")) { 
   
	//有权限
} else { 
   
	//无权限
}
--------------------------------------
 2.注解式
@RequiresRoles("admin") 
@RequiresPermissions("user:create") 
public void hello() { 
   
    //有权限
}
----------------------------------------
 3.页面标签式
    <@shiro.hasPermission name="employee:list">
    <!-- 有权限 -->
</@shiro.hasRole>

授权步骤

1.贴注解
2.开启 Shiro 注解扫描器
3.查询数据库真实数据 自定义realm    

注解@RequiresPermissions()

  • value属性: 这个属性是一个数组
  • Logical.AND: 必须同时拥有value配置所有权限才允许访问
  • Logical.OR:只需要拥有value配置所有权限中一个即可允许访问
约定:权限表达式,第一值权限表达式,第二值为权限名称
 @RequiresPermissions(value={ 
   "employee:list","员工列表"},logical=Logical.OR)
    逻辑为OR,value满足其中一个条件成立

标签式权限验证


拓展FreeMarker标签

默认的freemarker是不支持shiro标签,所以需要做功能拓展
 ----------------   
  public class ShiroFreeMarkerConfig extends FreeMarkerConfigurer { 
   
    @Override
    public void afterPropertiesSet() throws IOException, TemplateException { 
   
        //继承之前的属性配置,这不能省
        super.afterPropertiesSet();
        Configuration cfg = this.getConfiguration();
        cfg.setSharedVariable("shiro", new ShiroTags());//注册shiro 标签
    }
}  
--------------------------让之前的配置文件
    中的FreeMarkerConfigurer修改为自定义的ShiroFreeMarkerConfig<!-- 注册 FreeMarker 配置类 -->
    <bean class="cn.k.shiro.ShiroFreeMarkerConfig">
        <!-- 配置 FreeMarker 的文件编码 -->
        <property name="defaultEncoding" value="UTF-8" />
        <!-- 配置 FreeMarker 寻找模板的路径 -->
        <property name="templateLoaderPath" value="/WEB-INF/views/" />
        <property name="freemarkerSettings">
            <props>
                <!-- 兼容模式 ,配了后不需要另外处理空值问题,时间格式除外 -->
                <prop key="classic_compatible">true</prop>
            </props>
        </property>
    </bean>

使用shiro标签

  • authenticated 标签:已认证通过的用户。
<@shiro.authenticated> </@shiro.authenticated>
  • notAuthenticated 标签:未认证通过的用户。
<@shiro.notAuthenticated></@shiro.notAuthenticated>
  • principal 标签 :输出当前用户信息,通常为登录帐号信息
<@shiro.principal property="name" /> //对应name属性
  • hasRole 标签:验证当前用户是否拥有该角色
<@shiro.hasRole name="admin">我是管理员</@shiro.hasRole>
  • hasAnyRoles 标签:验证当前用户是否拥有这些角色中的任何一个,角色之间逗号分隔
<@shiro.hasAnyRoles name="admin,user,hr">Hello admin</@shiro.hasAnyRoles>
  • hasPermission 标签:验证当前用户是否拥有该权限
<@shiro.hasPermission name="department:delete">删除</@shiro.hasPermission>

Shiro加密

MD5

//参数1 :原文,参数2:盐 ,参数3:散列次数(加密次数)
Md5Hash hash=new Md5Hash("1","kent",3);

盐一般要求是固定长度的字符串,且每个用户的盐不同。

一般盐的选择的是用户的唯一数据(账号名等),盐是要求不能改变的,不然下次加密结果就对应不上了

Shiro缓存

当我们登录时,授权信息是要从数据库中查询的,如果每次刷新刷新都需要获取你到底有没有权限,对性能影响不好,用户登录后,授权信息一般很少改动,所以,我们可以将第一次授权后,将信息存在缓存中,下次直接再缓存中获取,就很好的避免了多次访问数据库

Shiro没有实现自己的缓存机制,只提供了支持缓存的API接口,这里使用的是EhCache

mybatis 一级  二级缓存------百度(ing)补充


集成EhCache

第三方EhCache
  1.配置缓存管理器并引用缓存管理器
    <!--安全管理器-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <!--注册自定义数据源-->
    <property name="realm" ref="employeeRealm"/>
    <!--注册缓存管理器-->
    <property name="cacheManager" ref="cacheManager"/>
</bean>

<!-- 缓存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> 
    <!-- 设置配置文件 -->
    <property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
</bean>
    ----------------------------------------
    2.添加缓存配置文件  shiro-ehcache.xml
    <ehcache>
    <defaultCache
            maxElementsInMemory="1000"
            eternal="false"
            timeToIdleSeconds="600"
            timeToLiveSeconds="600"
            memoryStoreEvictionPolicy="LRU">
    </defaultCache>
</ehcache>

配置缓存文件属性说明

  • maxElementsInMemory: 缓存对象最大个数。
  • eternal:对象是否永久有效 ,一般为false
  • timeToIdleSeconds: 对象空闲时间
  • timeToLiveSeconds:对象存活时间
  • memoryStoreEvictionPolicy:当达到 maxElementsInMemory 限制时,Ehcache 将会根据指定的策略去清理内存。
  • LFU(较少使用,意思是一直以来最少被使用的,缓存的元素有一个hit 属性(命中率),hit 值最小的将会被清出缓存)默认


拓展 统一全局异常

@ControllerAdvice 控制器功能增强注解

1.在进入请求映射方法之前做功能增强,经典用法:date日期格式化
2.在进入请求映射方法之后做功能增强,经典用法:统一异常处理 

3.处理异常的方法,方法需要贴ExceptionHandler注解


配置文件

配置代理过滤器

<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>
        org.springframework.web.filter.DelegatingFilterProxy
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

配置shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--引入自定义Realm-->
    <bean id="employeeRealm" class="cn.k.shiro.EmployeeRealm">
    </bean>

    <!--配置安全管理器-->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="employeeRealm"/>
        <!--注册缓存管理器-->
        <property name="cacheManager" ref="cacheManager"/>
    </bean>

    <!--配置shiro过滤器-->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <!--引用指定的安全管理器-->
        <property name="securityManager" ref="securityManager"/>
        <!--shiro默认的登录地址是/login.jsp 现在要指定我们自己的登录页面地址-->
        <property name="loginUrl" value="/login.html"/>
        <!--路径对应的规则-->
        <property name="filterChainDefinitions">
            <value>
                /userLogin=anon
                /css/**=anon
                /js/**=anon
                /img/**=anon
                /upload/**=anon
                /userLogout=logout
                /**=authc
            </value>
        </property>
    </bean>


    <!--开启 Shiro 注解扫描器-->
    <!-- <aop:config/> 会扫描配置文件中的所有advisor,并为其创建代理 -->
    <aop:config/>
    <!-- Pointcut advisor通知器, 会匹配所有加了shiro权限注解的方法 -->
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>


    <!-- 缓存管理器 -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <!-- 设置配置文件 -->
        <property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
    </bean>

</beans>

关联mvc.xml

<import resource="classpath:shiro.xml"/>

loginController

@RequestMapping("/userLogin")
    @ResponseBody
    public JsonResult login(String username, String password){ 
   
        try { 
   
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            SecurityUtils.getSubject().login(token);
            return new JsonResult();
        } catch (UnknownAccountException e) { 
   
            return new JsonResult(false, "账号不存在");
        } catch (IncorrectCredentialsException e) { 
   
            return new JsonResult(false, "密码错误");
        } catch (Exception e) { 
   
            e.printStackTrace();
            return new JsonResult(false, "登录异常,请联系管理员");
        }
    }

关联mvc.xml

<import resource="classpath:shiro.xml"/>

loginController

@RequestMapping("/userLogin")
    @ResponseBody
    public JsonResult login(String username, String password){ 
   
        try { 
   
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            SecurityUtils.getSubject().login(token);
            return new JsonResult();
        } catch (UnknownAccountException e) { 
   
            return new JsonResult(false, "账号不存在");
        } catch (IncorrectCredentialsException e) { 
   
            return new JsonResult(false, "密码错误");
        } catch (Exception e) { 
   
            e.printStackTrace();
            return new JsonResult(false, "登录异常,请联系管理员");
        }
    }
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/195851.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号