在参考中发现了 《Apache Shiro 参考手册》,强烈建议参看学习。

建议看完之后配合 Shiro保姆级教程 学习,他的内容介绍不多,但代码实现过程很完整


Shiro 简介

Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。


首先创建一个 SpringBoot 项目,并在 pom.xml 文件中引入如下会用到的依赖

    <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</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>2.2.2</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- 导入shiro和spring整合依赖 --><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.4.0</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>

resources 文件夹下创建 shiro.ini 文件

[users]
zhangsan=123
lisi=123
wangwu=123

测试 Shiro

@Test
public void test01() {// 创建安全管理器,设置它的 RealmDefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini"));//将安装工具类中设置默认安全管理器SecurityUtils.setSecurityManager(defaultSecurityManager);//获取主体对象Subject subject = SecurityUtils.getSubject();//创建token令牌UsernamePasswordToken token = new UsernamePasswordToken("xiaochen1", "123");try {subject.login(token);//用户登录System.out.println("登录成功~~");} catch (UnknownAccountException e) {e.printStackTrace();System.out.println("用户名错误!!");}catch (IncorrectCredentialsException e){e.printStackTrace();System.out.println("密码错误!!!");}}

在这个测试中,我们可以把 ini 当做是个账号的数据库,UsernamePasswordToken token = new UsernamePasswordToken("xiaochen1", "123"); 这一行看作是用户登陆时发来的账号和密码

subject.login(token); 这里进行登陆,开始进行验证上面的 token,这时设置的 IniRealm 对象会查找数据库(当前是 shiro.ini 文件)里是否有匹配的账号,然后验证密码,如果没有报错就是登陆成功,报错那就是不对。

该方法主要执行以下操作:

  1. 检查提交的进行认证的令牌信息
  2. 根据令牌信息从数据源(通常为数据库)中获取用户信息
  3. 对用户信息进行匹配验证。
  4. 验证通过将返回一个封装了用户信息的AuthenticationInfo实例。
  5. 验证失败则抛出AuthenticationException异常信息。

以下是几种出现的错误:

  • UnknownAccountException (账号错误/没有账号)

  • IncorrectCredentialsException (密码错误)

  • DisabledAccountException(帐号被禁用)

  • LockedAccountException(帐号被锁定)

  • ExcessiveAttemptsException(登录失败次数过多)

  • ExpiredCredentialsException(凭证过期)等

上面代码运行过程:

上边的程序使用的是读取本地 ini 文件的方式进行测试进行验证判断用户名和密码是否正确,但正式开发中是不能用这种的,都需要从数据库中读取对应登陆的用户的信息,所以需要自定义 Realm 处理这些逻辑,以及添加更多样的操作。

Subject 即主体,外部应用与 subject 进行交互,subject 记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。 Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权

Realm 即领域,相当于 datasource 数据源,SecurityManager 进行安全认证需要通过 Realm 获取用户权限数据,比如:如果用户身份数据在数据库那么 Realm 就需要从数据库获取用户身份信息。就是在 Realm 里写认证和授权逻辑的,执行的时候会执行 Realm 里的认证和授权逻辑。

注意:不要把 Realm 理解成只是从数据源取数据,在 Realm 中还有认证授权校验的相关的代码。

一般在真实的项目中,我们不会直接实现 Realm 接口,也不会直接继承最底层的功能贼复杂的 IniRealm。我们一般的情况就是直接继承 AuthorizingRealm,能够继承到认证与授权功能。它需要强制重写两个方法:doGetAuthenticationInfodoGetAuthorizationInfo

[

Shiro 提供的 Realm 体系较为复杂,一般我们为了使用 Shiro 的基本目的就是:认证授权。可以看以下 SimpleAccountRealm 的部分源码中的两个方法。授权(doGetAuthorizationInfo)、认证(doGetAuthenticationInfo)。两部分的源码:

public class SimpleAccountRealm extends AuthorizingRealm {//.......省略protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {UsernamePasswordToken upToken = (UsernamePasswordToken) token;SimpleAccount account = getUser(upToken.getUsername());if (account != null) {if (account.isLocked()) {throw new LockedAccountException("Account [" + account + "] is locked.");}if (account.isCredentialsExpired()) {String msg = "The credentials for account [" + account + "] are expired";throw new ExpiredCredentialsException(msg);}}return account;}protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {String username = getUsername(principals);USERS_LOCK.readLock().lock();try {return this.users.get(username);} finally {USERS_LOCK.readLock().unlock();}}
}

上面就是他的一个认证授权时的逻辑,我们可以自定义一个 UserRealm,以实现更复杂更强大的认证、授权逻辑。

public class UserRealm extends AuthorizingRealm {//授权方法(访问不同的 controller 里的接口时进行授权)@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {// 这个 username 是下面 doGetAuthenticationInfo 方法 return 的对象的第一个参数的值final String username = (String) principalCollection.getPrimaryPrincipal();System.out.println("执行授权:授权的用户名 " + username);final SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();// 查询设置用户的权限 permission(下面假装是数据库查询到的数据)Set<String> permissions = new HashSet<>();permissions.add("auth:add_user");permissions.add("auth:update_user");info.setStringPermissions(permissions);// 查询设置用户的角色 Role(下面假装是数据库查询到的数据)Set<String> roles = new HashSet<>();roles.add("管理员");info.setRoles(roles);return info;}//认证方法,在登陆的时候会进行验证@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {System.out.println("执行认证");final UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;/* //按数据库查询应该是以下代码,但为了方便起见,后面的写个假数据User user = userService.findUser(token.getUsername());if(user != null){// 当前 Realm 中的 doGetAuthorizationInfo 方法那个参数就是这个第一个参数,保存了当前用户信息return new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());}*/// 数据库查询用户名和密码(这里假作已经查询到了结果)String name = "zhangsan";String password = "123";if (name.equals(token.getUsername())) {// new SimpleAuthenticationInfo 的时候会自动校验密码return new SimpleAuthenticationInfo(token.getPrincipal(),password,this.getName());}return null;}
}

测试

@Test
public void test01() {// 创建安全管理器,设置它的 Realmfinal DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();defaultSecurityManager.setRealm(new UserRealm());// 设置处理认证和授权的安全管理器SecurityUtils.setSecurityManager(defaultSecurityManager);final Subject subject = SecurityUtils.getSubject();// 前端发送来的账号和密码生成的通行令牌,用来让 Realm 的 doGetAuthenticationInfo 方法里执行认证过程UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123");try {subject.login(token);} catch (UnknownAccountException e) {e.printStackTrace();System.out.println("用户名错误!!");} catch (IncorrectCredentialsException e) {e.printStackTrace();System.out.println("密码错误!!!");}
}

上面运行过程

配置 Shiro

下面开始写 Controller 层逻辑需要用到的类。

创建 ShiroConfig,配置 shiro 的类:

import com.example.shirotest.common.UserRealm;   // 引用自定义的 UserRealm
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.LinkedHashMap;
import java.util.Map;/*** Shiro** @author z* @datetime 2022-5-16*/
@Configuration
public class ShiroConfig {/*** 创建 Realm,bean会让方法返回的对象放入到spring的环境,以便使用*/@Bean(name = "userRealm")public UserRealm getRealm() {return new UserRealm();}/*** @Qualifier 注释指定注入 Bean 的名称,用来消除歧义的*/@Bean(name = "defaultWebSecurityManager")public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();defaultWebSecurityManager.setRealm(userRealm);return defaultWebSecurityManager;}@Beanpublic ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();//设置一个安全管理器来关联 SecurityManagershiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);// 默认登陆界面shiroFilterFactoryBean.setLoginUrl("/loginPage");// 设置权限(非注解方式设置对应 url 的权限)// 注意要用 LinkedHashMap 遍历列表时时候按顺序进行匹配判断 URL// 所以下面的 /** 要放在最后,否则如果放在第一个则所有的 URL 都// 可以被匹配到,那么之后的就失效了Map<String, String> filterMap = new LinkedHashMap<>();filterMap.put("/loginPage", "anon");filterMap.put("/user/login", "anon");filterMap.put("/add", "perms[user:add, admin]");filterMap.put("/update", "perms[user:update]");filterMap.put("/**", "authc");shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);return shiroFilterFactoryBean;}/***  开启Shiro的注解 (如@RequiresRoles,@RequiresPermissions)* @return*/@Beanpublic DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();advisorAutoProxyCreator.setProxyTargetClass(true);return advisorAutoProxyCreator;}
}

上面的 ShiroFilterFactoryBean 方法中主要主要配置接口的角色权限,确定接口由哪些角色或者哪些权限的用户可以访问。至于 shiro 是怎么知道当前用户是否具有某个角色或权限需要用到后面的 doGetAuthorizationInfo() 方法。

Filter 解释
anon 无参,开放权限,可以理解为匿名用户或游客
authc 无参,需要认证
user 无参,表示必须存在用户,当登入操作时不做检查
perms[user] 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms["user:add, admin"],当有多个参数时必须每个参数都通过才算通过
roles[admin] 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles["admin, user"],当有多个参数时必须每个参数都通过才算通过

Shiro内置的 FilterChain

Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter
  • anon: 所有url都都可以匿名访问
  • authc: 需要认证才能进行访问
  • user: 配置记住我或认证通过可以访问

Shiro 权限字符串

  1. 组成规则
    在Shiro中使用权限字符串必须按照Shiro指定的规则。

权限字符串组合规则为:“资源类型标识符:操作:资源实例标识符

  • 资源类型标识符: 一般会按模块,对系统划分资源。比如user模块,product 模块,order模块等,对应的资源类型标识符就是:userproductorder
  • 操作: 一般为增删改查(createdeleteupdatefind),还有 * 标识统配。
  • 资源实例标识符: 如果Subject控制的是资源类型,那么资源实例标识符就是 “*” ;如果Subject控制的是资源实例,那么就需要在资源实例标识符就是该资源的唯一标识(ID等)。
  1. 示例
  • *:*:* 表示 Subject 对所有类型的所有实例有所有操作权限,相当于超级管理员。

  • user:create:* 表示 Subject 对 user 类型的所有实例有创建的权限,可以简写为:user:create

  • user:update:001 表示 Subject 对 ID001 的 user 实例有修改的权限。

  • user:*:001 表示 Subject 对 ID001user 实例有所有权限。

Filter Chain定义说明

  • 1、一个URL可以配置多个Filter,使用逗号分隔
  • 2、当设置多个过滤器时,全部验证通过,才视为通过
  • 3、部分过滤器可指定参数,如perms,roles

上面的自定义的 UserRealmShiroConfig 创建的步骤你可以把它算作一个固定的套路,只要使用 Shiro 就难以避免,必须要创建这两个类,以及实现其中的方法。

MD5 加密

我们创建一个 MD5Utils 工具类,对密码进行加密,加密后的密码破解难度是“不可能破解出来”,即便黑客获取到数据库,也无法知道具体密码

import org.apache.shiro.crypto.hash.Md5Hash;public class MD5Utils {/*** 加密盐值*/public static final String SALT = "^3&5as@9.[km0";/*** 密码进行MD5加密*/public static String md5Password(String password) {// 加密 1024 次Md5Hash md5Hash = new Md5Hash(password, SALT, 1024);return md5Hash.toHex();}}

用户注册的时候,我们对他们的密码进行加密保存到数据库中,
那么在提交用户输入原始的密码的时候,我们需要进行加密跟我们已经保存的加密后的密码进行匹配,如果两个加密后的密码相同,那自然两个密码都是一样的

对测试类中的 token 添加加密,也就是对发来的密码进行 MD5 加密

UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", MD5Utils.md5Password("123"));

UserRealm 中的 doGetAuthenticationInfo() 因为我们写的假数据,所以也要改一下,改为如下:

@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {System.out.println("执行认证");// 数据库查询用户名和密码(这里假作已经查询到了结果)// 如果是从数据库查找到的则不用进行 MD5Utils.md5Password 这个操作,因为数据库里已经加过密了// 这里因为这是我们手动设置的假数据,所以需要 md5 加密一下String name = "zhangsan";String password = MD5Utils.md5Password("123");final UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;if (name.equals(token.getUsername())) {// new SimpleAuthenticationInfo 的时候会自动校验密码return new SimpleAuthenticationInfo(token.getPrincipal(),password,this.getName());}return null;}

权限注解

注:shiro 提供了相应的注解用于权限控制,如果使用这些注解就需要使用 aop 的功能来进行判断。shiro 提供了 spring aop 集成,用于权限注解的解析和验证

(1)@RequiresAuthentication :方法在访问或调用时,当前 Subject 必须在当前 session 中已经过认证。表示当前 Subject 已经通过 login 进行了身份验证;即 Subject.isAuthenticated() 返回 true

(2)@RequiresUser:表示当前 Subject 已经身份验证或者通过记住我登录的,才能访问或调用被该注解标注的类,实例,方法。

(3)@RequiresGuest :表示当前 Subject 没有身份验证或通过记住我登录过,即是游客身份。使用该注解标注的类,实例,方法在访问或调用时,当前 Subject 可以是 guest 身份,不需要经过认证或者在原先的 session 中存在记录。

(4)@RequiresRoles(value={"admin", "user"}, logical= Logical.AND) 当前 Subject 必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天 Subject 不同时拥有所有指定角色,则方法不会执行还会抛出 AuthorizationException 异常。

(5)@RequiresPermissions (value={"user:a", "user:b"}, logical= Logical.OR) :表示当前 Subject 需要权限 user:auser:b。当前 Subject 需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前 Subject 不具有这样的权限,则方法不会被执行。

Shiro 的认证注解处理是有内定的处理顺序的,如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回,处理顺序依次为(与实际声明顺序无关):

RequiresRoles
RequiresPermissions
RequiresAuthentication
RequiresUser
RequiresGuest

例如:你同时声明了 @RequiresRoles@RequiresPermissions,那就要求拥有此角色的同时还得拥有相应的权限。

@RequiresRoles

可以用在 Controller 或者方法上。可以多个 roles,多个roles 时默认逻辑为 AND 也就是所有具备所有 role 才能访问。

示例:

//属于user角色
@RequiresRoles("user")//必须同时属于user和admin角色
@RequiresRoles({"user","admin"})//属于user或者admin之一;修改logical为OR 即可
@RequiresRoles(value={"user","admin"},logical=Logical.OR)

@RequiresPermissions

@RequiresRoles 类似。示例:

//符合index:hello权限要求
@RequiresPermissions("index:hello")//必须同时复核index:hello和index:world权限要求
@RequiresPermissions({"index:hello","index:world"})//符合index:hello或index:world权限要求即可
@RequiresPermissions(value={"index:hello","index:world"},logical=Logical.OR)

@RequiresAuthentication,@RequiresUser,@RequiresGuest

这三个的使用方法一样

@RequiresAuthentication
@RequiresUser
@RequiresGusst

其他例子:

@RequiresPermissions({"file:read", "write:aFile.txt"} )
void someMethod();

要求subject中必须同时含有file:readwrite:aFile.txt的权限才能执行方法someMethod()。否则抛出异常AuthorizationException

@RequiresRoles({"admin"})
void method();

只有 admin 角色才能访问该方法,其他角色访问将会抛出异常

注意事项

在日常开发时,往往会在 Service 层添加“@Transactional”注解,为的是当 Service 发送数据库异常时,所有数据库操作可以回滚。

当在 Service 层添加“@Transactional”注解后,执行 Service 方法前,会开启事务。此时的 Service 已经是一个代理对象了,此时如果我们将 Shiro 的权限注解加载 Service 层是不合适的,此时需要加到 Controller 层。这是因为不能让 Service 是“代理的代理”,如果强行注入,会发生类型转换异常。

建议看完之后配合 Shiro保姆级教程 学习,他的文章中还包含有 SQL 的创建和实现步骤

参考

  • Shiro 实战教程(上)

  • springboot(十四):springboot整合shiro-登录认证和权限管理

  • 第二节 自定义Realm之继承AuthorizingRealm

  • shiro框架的权限设定(三)

  • Spring Boot整合Shiro实现用户身份认证和权限认证

  • 第三节 Shiro对加密的支持

  • 【Shiro】3. Shiro授权流程

  • shiro注解权限控制-5个权限注解


如有错误,还请指正和建议。

Shiro 详细教程(集各教程内容为一体)相关推荐

  1. Hadoop全分布式集群搭建(全网最详细,保姆级教程)

    在上一篇Hadoop环境搭建(全网最详细,保姆级教程)中已经搭建好了一个单机Hadoop环境,接下来搭建全分布式Hadoop集群 首先对Hadoop全分布示集群进行简单介绍和规划 一个集群由一个主机, ...

  2. python免费教学视频400集-如何入门 Python 爬虫?400集免费教程视频带你从0-1全面掌握...

    学习Python大致可以分为以下几个阶段: 1.刚上手的时候肯定是先过一遍Python最基本的知识,比如说:变量.数据结构.语法等,基础过的很快,基本上1~2周时间就能过完了,我当时是在这儿看的基础: ...

  3. 最详细的Hadoop安装教程

    最详细的Hadoop安装教程 前言 Hadoop 在大数据技术体系中的地位至关重要,Hadoop 是大数据技术的基础,对Hadoop基础知识的掌握的扎实程度,会决定在大数据技术道路上走多远. 这是一篇 ...

  4. openlayers3教程详细_OpenLayers 3 入门教程

    OpenLayers 3 入门教程 摘要 OpenLayers 3对OpenLayers网络地图库进行了根本的重新设计.版本2虽然被广泛使用,但从JavaScript开发的早期发展阶段开始,已日益现实 ...

  5. 超详细的MySQL入门教程(四)

    MySQL:简单的增删改查 查询数据 基本语法介绍 打印任意值 查询表中全部数据 查询表中部分字段 限定条件查询 例1:查询编号值小于指定值的记录 例2:查询地址不等于某值的记录 例3:查询一级地址等 ...

  6. Redis创建高可用集群教程【Windows环境】

    模仿的过程中,加入自己的思考和理解,也会有进步和收获. 在这个互联网时代,在高并发和高流量可能随时爆发的情况下,单机版的系统或者单机版的应用已经无法生存,越来越多的应用开始支持集群,支持分布式部署了. ...

  7. MySQL 5.7.21详细下载安装配置教程

    MySQL 5.7.21详细下载安装配置教程 前言 在安装MySQL的时候会遇到很多问题,博客上有很多解决问题的办法,在这里我附上一些链接,遇到问题的朋友们可以阅读参考哈~本文主要针对于刚接触数据库的 ...

  8. [转]详细的GStreamer开发教程

    详细的GStreamer开发教程 文章目录 详细的GStreamer开发教程 1. 什么是GStreamer? 2. GStreamer架构 2.1 Media Applications 2.2 Co ...

  9. mysql 5.5.29 winx64_【转载】MySQL 5.7.29详细下载安装配置教程winx64

    版权声明:本文为CSDN博主「liu_dong_mei_mei」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明. 原文链接:https://blog.csdn.n ...

  10. 电脑录屏怎么录?超详细的录屏教程来了

    案例:电脑录屏怎么录?求详细的电脑录屏教程! "最近的工作需要用到电脑录屏,但是我不知道电脑录屏怎么录?去网上搜索了一下教程都是比较大概的,我这个新手小白根本看不懂.在这里想问问大家,有没有 ...

最新文章

  1. Linux Bash变量-数值运算与运算符
  2. 转-项目管理心得:一个项目经理的个人体会、经验总结
  3. Python3实现旋转数组的3种算法
  4. 做事用人 用人做事_做事:构建我的第一个Web应用程序的经验教训
  5. pythonrequest函数_[Python]requests模块:HTTP请求时的回调函数
  6. 视觉SLAM笔记(31) 特征提取和匹配
  7. 洛谷——P1085 [NOIP2004 普及组] 不高兴的津津
  8. uses-sdk标签详解
  9. Boruta特征筛选
  10. PHP语言25周年,PHP是世界上最好的语言
  11. vnc远程控制软件,五款良心推荐的vnc远程控制软件
  12. H3CNE-生成树协议(STP)
  13. Android 腾讯优图开发问题总结
  14. 什么是群、什么是阿贝尔群(abel群、阿贝尔群也称为交换群或可交换群)、群论入门
  15. Flutter如何实现下拉刷新和上拉加载更多
  16. 苹果鼠标右键怎么按_Mac触控板常用的手势操作,让你告别Windows鼠标!
  17. 怎么会这样!超声刀两年后面部塌陷,超声刀失败可以补救吗,让人头大!不要啊
  18. Qt实用技巧:仅去掉标题栏,保持对话框边框
  19. 腾讯游戏助手运行闪退日志查看
  20. 研究生计算机论文怎么写,研究生计算机论文摘要怎么写 研究生计算机论文摘要范文参考...

热门文章

  1. js实现全国省份下拉
  2. 老男孩python全栈day01
  3. oppo自带计算机版本,OPPO手机助手
  4. STM32F103和STM32F107区别
  5. mysql绿色版安装、局域网访问配置
  6. UML用例图的画法详细介绍【软件工程】
  7. Java怎样实现验证码?
  8. Qt第一章:pyside6安装与配置
  9. 图解机器学习笔记-1
  10. 17SWFObject使用