IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> SpringBoot+Vue从零开始做网站6-集成shiro实现登录和权限控制 -> 正文阅读

[Java知识库]SpringBoot+Vue从零开始做网站6-集成shiro实现登录和权限控制

到上一篇已经把前后端的项目底子搭好了,今天开始做功能,首先就是后台管理系统登录功能。

Shiro简介

Apache Shiro是一个轻量级的身份验证与授权Java安全框架。对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用简单易用的Shiro就足够了,灵活性高。springboot本身是提供了对security的支持。springboot暂时没有集成shiro,这得自己配。

Shiro三个核心概念:Subject、SecurityManager 和 Realms,还有四大功能——Authentication(认证)、Authorization(授权)、Session Management(会话管理)、Cryptography(加密)

Subject一词是一个安全术语

狭指: 当前的操作用户(用户主体—把操作交给securityManager)

泛指:当前跟软件交互的东西(人,第三方进程、后台帐户(Daemon Account)、定时作业(Corn Job)等等)

在程序中你都能轻易的获得Subject,允许在任何需要的地方进行安全操作。每个Subject对象都必须与一个SecurityManager进行绑定,你访问Subject对象其实都是在与SecurityManager里的特定Subject进行交互。

SecurityManager

Subject的“幕后”推手是SecurityManager(安全管理器,关联realm)。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。它是Shiro框架的核心,充当“保护伞”,引用了多个内部嵌套安全组件,它们形成了对象图。但是,一旦SecurityManager及其内部对象图配置好,它就会退居幕后,应用开发人员几乎把他们的所有时间都花在Subject API调用上。

Realms

Shiro的第三个也是最后一个概念是Realm(连接数据的桥梁)。Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当与像用户帐户这类安全相关数据进行交互,执行认证(登录)和授权(访问控制)时,Shiro会从应用配置的Realm中查找很多内容。

从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。

详细就每个点去看些文章了解吧,不做过多描述。

本系统密码加密使用md5+盐加密

加盐,是提高 hash 算法的安全性的一个常用手段。下面是加盐加密与验证的逻辑:

用户注册时,输入用户名密码(明文),向后台发送请求

后台将密码加上随机生成的盐并 hash,再将 hash 后的值作为密码存入数据库,盐也作为单独的字段存起来

用户登录时,输入用户名密码(明文),向后台发送请求

后台根据用户名查询出盐,和密码组合并 hash,将得到的值与数据库中存储的密码比对,若一致则通过验证

然后就是开搞---实现登录功能 直接上代码

添加依赖

??<dependency>
????????????<groupId>org.apache.shiro</groupId>
????????????<artifactId>shiro-spring</artifactId>
????????????<version>1.2.5</version>
????????</dependency>
????????<dependency>
????????????<groupId>org.apache.shiro</groupId>
????????????<artifactId>shiro-ehcache</artifactId>
????????????<version>1.2.5</version>
????????</dependency>

shiro配置的顺序如下:

创建 Realm 并重写获取认证与授权信息的方法

创建配置类,包括创建并配置 SecurityManager 等

创建shiro包、在shiro包下创建ShiroRealm类

package?com.zjlovelt.shiro;

import?com.zjlovelt.entity.SysUser;
import?com.zjlovelt.service.SysUserService;
import?org.apache.shiro.SecurityUtils;
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.session.Session;
import?org.apache.shiro.subject.PrincipalCollection;
import?org.apache.shiro.util.ByteSource;
import?org.slf4j.Logger;
import?org.slf4j.LoggerFactory;
import?org.springframework.beans.factory.annotation.Autowired;

public?class?ShiroRealm??extends?AuthorizingRealm?{
????private?Logger?logger?=??LoggerFactory.getLogger(this.getClass());

????@Autowired
????private?SysUserService?userService;


????//重写获取授权信息方法
????@Override
????protected?AuthorizationInfo?doGetAuthorizationInfo(PrincipalCollection?principalCollection)?{
????????logger.info("doGetAuthorizationInfo+"+principalCollection.toString());
????????SysUser?user?=?userService.getByUserName((String)?principalCollection.getPrimaryPrincipal());


????????//把principals放session中?key=userId?value=principals
????????SecurityUtils.getSubject().getSession().setAttribute(String.valueOf(user.getId()),SecurityUtils.getSubject().getPrincipals());

????????SimpleAuthorizationInfo?info?=?new?SimpleAuthorizationInfo();
????????//赋予角色
???????/*?for(Role?userRole:user.getRoles()){
????????????info.addRole(userRole.getName());
????????}
????????//赋予权限
????????for(Permission?permission:permissionService.getByUserId(user.getId())){
//????????????if(StringUtils.isNotBlank(permission.getPermCode()))
????????????info.addStringPermission(permission.getName());
????????}*/

????????//设置登录次数、时间
//????????userService.updateUserLogin(user);
????????return?info;
????}


????//?获取认证信息,即根据?token?中的用户名从数据库中获取密码、盐等并返回
????@Override
????protected?AuthenticationInfo?doGetAuthenticationInfo(AuthenticationToken?authenticationToken)?throws?AuthenticationException?{
????????logger.info("doGetAuthenticationInfo?+"??+?authenticationToken.toString());

????????UsernamePasswordToken?token?=?(UsernamePasswordToken)?authenticationToken;
????????String?userName?=?token.getUsername();
????????logger.info(userName+token.getPassword());

????????SysUser?user?=?userService.getByUserName(token.getUsername());
????????if?(user?!=?null)?{
???????????/*?byte[]?salt?=?Encodes.decodeHex(user.getSalt());
????????????ShiroUser?shiroUser=new?ShiroUser(user.getId(),?user.getLoginName(),?user.getName());*/
????????????String?salt?=?user.getSalt();?//用户盐值?最后需转byte[]
????????????//设置用户session
????????????Session?session?=?SecurityUtils.getSubject().getSession();
????????????session.setAttribute("user",?user);
????????????return?new?SimpleAuthenticationInfo(userName,user.getPassword(),?ByteSource.Util.bytes(salt),getName());
????????}?else?{
????????????return?null;
????????}
????}

}

在shiro包下创建ShiroConfiguration?

package?com.zjlovelt.shiro;

import?org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.filter.authc.LogoutFilter;
import?org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import?org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import?org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import?org.springframework.context.annotation.Bean;
import?org.springframework.context.annotation.Configuration;
import?org.springframework.context.annotation.DependsOn;

import?java.util.LinkedHashMap;
import?java.util.Map;
import?javax.servlet.Filter;

/**
?*?shiro配置类
?*?Created?by?zj?on?2022/4/19.
?*/
@Configuration
public?class?ShiroConfiguration?{

????/**
?????*?LifecycleBeanPostProcessor,这是个DestructionAwareBeanPostProcessor的子类,
?????*?负责org.apache.shiro.util.Initializable类型bean的生命周期的,初始化和销毁。
?????*?主要是AuthorizingRealm类的子类,以及EhCacheManager类。
?????*/
????@Bean(name?=?"lifecycleBeanPostProcessor")
????public?LifecycleBeanPostProcessor?lifecycleBeanPostProcessor()?{
????????return?new?LifecycleBeanPostProcessor();
????}

????/**
?????*?HashedCredentialsMatcher,这个类是为了对密码进行编码的,
?????*?防止密码在数据库里明码保存,当然在登陆认证的时候,
?????*?这个类也负责对form里输入的密码进行编码。
?????*/
????@Bean(name?=?"hashedCredentialsMatcher")
????public?HashedCredentialsMatcher?hashedCredentialsMatcher()?{
????????HashedCredentialsMatcher?credentialsMatcher?=?new?HashedCredentialsMatcher();
????????credentialsMatcher.setHashAlgorithmName("MD5");
????????credentialsMatcher.setHashIterations(2);
????????credentialsMatcher.setStoredCredentialsHexEncoded(true);
????????return?credentialsMatcher;
????}

????/**
?????*?ShiroRealm,这是个自定义的认证类,继承自AuthorizingRealm,
?????*?负责用户的认证和权限的处理,可以参考JdbcRealm的实现。
?????*/
????@Bean(name?=?"shiroRealm")
????@DependsOn("lifecycleBeanPostProcessor")
????public?ShiroRealm?shiroRealm()?{
????????ShiroRealm?realm?=?new?ShiroRealm();
????????realm.setCredentialsMatcher(hashedCredentialsMatcher());
????????return?realm;
????}

//????/**
//?????*?EhCacheManager,缓存管理,用户登陆成功后,把用户信息和权限信息缓存起来,
//?????*?然后每次用户请求时,放入用户的session中,如果不设置这个bean,每个请求都会查询一次数据库。
//?????*/
//????@Bean(name?=?"ehCacheManager")
//????@DependsOn("lifecycleBeanPostProcessor")
//????public?EhCacheManager?ehCacheManager()?{
//????????return?new?EhCacheManager();
//????}

????/**
?????*?SecurityManager,权限管理,这个类组合了登陆,登出,权限,session的处理,是个比较重要的类。
?????*?//
?????*/
????@Bean(name?=?"securityManager")
????public?DefaultWebSecurityManager?securityManager()?{
????????DefaultWebSecurityManager?securityManager?=?new?DefaultWebSecurityManager();
????????securityManager.setRealm(shiroRealm());
//????????securityManager.setCacheManager(ehCacheManager());
????????return?securityManager;
????}

????/**
?????*?ShiroFilterFactoryBean,是个factorybean,为了生成ShiroFilter。
?????*?它主要保持了三项数据,securityManager,filters,filterChainDefinitionManager。
?????*/
????@Bean(name?=?"shiroFilter")
????public?ShiroFilterFactoryBean?shiroFilterFactoryBean()?{
????????ShiroFilterFactoryBean?shiroFilterFactoryBean?=?new?ShiroFilterFactoryBean();
????????//Shiro的核心安全接口,这个属性是必须的
????????shiroFilterFactoryBean.setSecurityManager(securityManager());

????????Map<String,?Filter>?filters?=?new?LinkedHashMap<String,?Filter>();
????????LogoutFilter?logoutFilter?=?new?LogoutFilter();
????????logoutFilter.setRedirectUrl("/login");
????????shiroFilterFactoryBean.setFilters(filters);
	//anon:没有参数,表示可以匿名使用。例子:/admin/**=anon
	//authc:没有参数,表四需要认证(登录)才能使用。例子:/user/**=authc
	//roles:角色过滤器,判断当前用户是否拥有指定角色。例子:admins/**=roles[“admin,guest”]
????????Map<String,?String>?filterChainDefinitionManager?=?new?LinkedHashMap<String,?String>();
????????filterChainDefinitionManager.put("/logout",?"logout");
????????filterChainDefinitionManager.put("/api/**",?"authc");
????????filterChainDefinitionManager.put("/**",?"anon");
????????shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionManager);

????????shiroFilterFactoryBean.setSuccessUrl("/");
????????shiroFilterFactoryBean.setUnauthorizedUrl("/403");
????????return?shiroFilterFactoryBean;
????}

????/**
?????*?DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
?????*/
????@Bean
????@ConditionalOnMissingBean
????public?DefaultAdvisorAutoProxyCreator?defaultAdvisorAutoProxyCreator()?{
????????DefaultAdvisorAutoProxyCreator?defaultAAP?=?new?DefaultAdvisorAutoProxyCreator();
????????defaultAAP.setProxyTargetClass(true);
????????return?defaultAAP;
????}

????/**
?????*?AuthorizationAttributeSourceAdvisor,shiro里实现的Advisor类,
?????*?内部使用AopAllianceAnnotationsAuthorizingMethodInterceptor来拦截用以下注解的方法。
?????*/
????@Bean
????public?AuthorizationAttributeSourceAdvisor?authorizationAttributeSourceAdvisor()?{
????????AuthorizationAttributeSourceAdvisor?aASA?=?new?AuthorizationAttributeSourceAdvisor();
????????aASA.setSecurityManager(securityManager());
????????return?aASA;
????}

}

最后使用 shiro 验证登录,编写登录接口方法

???
@Autowired
????private?SysUserService?userService;


????@RequestMapping(value?=?"/admin/login",?method?=?RequestMethod.POST)
????public?Result?login(SysUser?user)?{

????????String?username?=?user.getUsername();
????????Subject?subject?=?SecurityUtils.getSubject();

????????UsernamePasswordToken?usernamePasswordToken?=?new?UsernamePasswordToken(username,?user.getPassword());
????????try?{
????????????subject.login(usernamePasswordToken);
????????????return?Result.ok("登录成功").setData(usernamePasswordToken?);
????????}?catch?(IncorrectCredentialsException?e)?{
????????????return?Result.fail("密码错误");
????????}?catch?(UnknownAccountException?e)?{
????????????return?Result.fail("账号不存在");
????????}
????}

因为博客暂不需要注册功能,就后端直接生成用户名和密码吧,如果需要注册改成接口即可

?public?static?void?main(String[]?args)?{
????????SysUser?user?=?new?SysUser();
????????String?username?=?"admin";
????????String?password?=?"123456";
????????username?=?HtmlUtils.htmlEscape(username);
????????//?生成盐,默认长度?16?位
????????String?salt?=?new?SecureRandomNumberGenerator().nextBytes().toString();
????????//?设置?hash?算法迭代次数
????????int?times?=?2;
????????//?得到?hash?后的密码
????????String?encodedPassword?=?new?SimpleHash("md5",?password,?salt,?times).toString();
????????//?存储用户信息,包括?salt?与?hash?后的密码
????????System.out.println("salt:"?+?salt);
????????System.out.println("password:"+?encodedPassword);
????}

后端开发好了,然后就是前端了

首先是登录页代码,.vue页面分为三个模块,template是组件的模板结构页面元素,script是组件的 JavaScript 行为,style是组件的样式

<template>
????<div?class="login-wrap">
????????<div?class="ms-login">
????????????<div?class="ms-title">ltBlog-甜宝快更系统</div>
????????????<el-form?:model="param"?:rules="rules"?ref="login"?label-width="0px"?class="ms-content">
????????????????<el-form-item?prop="username">
????????????????????<el-input?v-model="param.username"?placeholder="用户名">
????????????????????????<template?#prepend>
????????????????????????????<el-button?icon="el-icon-user"></el-button>
????????????????????????</template>
????????????????????</el-input>
????????????????</el-form-item>
????????????????<el-form-item?prop="password">
????????????????????<el-input?type="password"?placeholder="密码"?v-model="param.password"
????????????????????????@keyup.enter="submitForm()">
????????????????????????<template?#prepend>
????????????????????????????<el-button?icon="el-icon-lock"></el-button>
????????????????????????</template>
????????????????????</el-input>
????????????????</el-form-item>
????????????????<div?class="login-btn">
????????????????????<el-button?type="primary"?@click="submitForm()">登录</el-button>
????????????????</div>
????????????????<p?class="login-tips">Tips?:?甜宝登陆后记得发文章呀。</p>
????????????</el-form>
????????</div>
????</div>
</template>

<script>
import?{?ref,?reactive,getCurrentInstance?}?from?"vue";
import?{?useStore?}?from?"vuex";
import?{?useRouter?}?from?"vue-router";
import?{?ElMessage?}?from?"element-plus";

export?default?{
????setup()?{
????????const?router?=?useRouter();
????????const?param?=?reactive({
????????????username:?"",
????????????password:?"",
????????});

????????const?rules?=?{
????????????username:?[
????????????????{
????????????????????required:?true,
????????????????????message:?"请输入用户名",
????????????????????trigger:?"blur",
????????????????},
????????????],
????????????password:?[
????????????????{?required:?true,?message:?"请输入密码",?trigger:?"blur"?},
????????????],
????????};
????????const?login?=?ref(null);
????????const?$http?=?getCurrentInstance()?.appContext.config.globalProperties.$http;
????????const?submitForm?=?()?=>?{
????????????console.log(param);
????????????login.value.validate((valid)?=>?{
????????????????if?(valid)?{
??????????????????$http({method:'post',url:'/admin/login',params:?param}).then(data?=>?{
????????????????????console.log(data)
????????????????????if?(data.success?===?true)?{
??????????????????????ElMessage.success(data.msg);
??????????????????????localStorage.setItem("ms_token",?data.data);??//记住登入状态,将用户信息放到localStorage
		??localStorage.setItem("ms_username",?username);
??????????????????????router.push("/****");?//登入成功后跳转到后台首页
????????????????????}?else?{
??????????????????????ElMessage.error(data.msg);
????????????????????}
??????????????????})
????????????????}?else?{
????????????????????ElMessage.error("登录失败");
????????????????????return?false;
????????????????}
????????????});
????????};

????????const?store?=?useStore();
????????store.commit("clearTags");

????????return?{
????????????param,
????????????rules,
????????????login,
????????????submitForm,
????????};
????},
};
</script>

<style?scoped>
.login-wrap?{
????position:?relative;
????width:?100%;
????height:?100%;
????background-image:?url(src/assets/img/login-bg.jpg);
????background-size:?cover;
????background-repeat:?no-repeat;
????background-position:?center;
}
.ms-title?{
????width:?100%;
????line-height:?50px;
????text-align:?center;
????font-size:?20px;
????color:?#fff;
????border-bottom:?1px?solid?#ddd;
}
.ms-login?{
????position:?absolute;
????left:?44%;
????top:?50%;
????width:?550px;
????margin:?-190px?0?0?-175px;
????border-radius:?5px;
????background:?rgba(255,?255,?255,?0.3);
????overflow:?hidden;
}
.ms-content?{
????padding:?30px?30px;
}
.login-btn?{
????text-align:?center;
}
.login-btn?button?{
????width:?100%;
????height:?36px;
????margin-bottom:?10px;
}
.login-tips?{
????font-size:?12px;
????line-height:?30px;
????color:?#fff;
}
</style>

页面的样子

登入成功的样子

登入失败的样子

?

就这样,springboot+shiro+vue的登录功能就开发好了

使用 Web Storage 存储键值对比存储 Cookie 方式更直观,而且容量更大,它包含两种:localStorage 和 sessionStorage 。cookie 和 local/session Storage 分工又有所不同,cookie 可以作为传递的参数,并可通过后端进行控制,local/session Storage 则主要用于在客户端中保存数据,其传输需要借助 cookie 或其它方式完成。

cookie

一般由服务器生成,可设置失效时间。如果在浏览器端生成cookie,默认是关闭浏览器后失效。每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题。

sessionStorage

临时存储,为每一个数据源维持一个存储区域,在浏览器打开期间存在,包括页面重新加载。仅在客户端(即浏览器)中保存,不参与和服务器的通信。

localStorage

长期存储,与 sessionStorage 一样,但是浏览器关闭后,数据依然会一直存在。仅在客户端(即浏览器)中保存,不参与和服务器的通信。

1?保存
//?Json对象
const?user?=?{name:?'sugar',?'cnt':?'22'};
localStorage.setItem('userJson',?JSON.stringify(user));

//?字符串
const?str?=?"sugar";
localStorage.setItem('userString',?str);

2?获取
//?Json对象
var?data1?=?JSON.parse(localStorage.getItem('userJson'));

//?字符串
var?data2?=?localStorage.getItem('userString');

3?删除
//?删除一个
localStorage.removeItem('userJson');

//?删除所有
localStorage.clear();

不过用localStorage存储用户数据,然后路由再根据localStorage是否有用户信息校验用户是否登录还是有问题的,在控制台输入window.localStorage.setItem('user', JSON.stringify({"name":"admin"}));? 就可以伪造信息从而避过登录了。

通常来说,在可以使用 cookie 的场景下,作为验证用途进行传输的用户名密码、sessionId、token 直接放在 cookie 里即可。而后端传来的其它信息则可以根据需要放在 local/session Storage 中,作为全局变量之类进行处理。

不过我们还是选择使用localStorage来存储用户信息,但是存入的信息是根据用户信息在后台生成的token,然后再修改下router/index.js的beforeEach方法,每次页面跳转都不再是判断localStorage中是否有用户信息,而是是否有token,如果有再去请求后台校验这个token是否正确,是否过期,如果错误或已过期就需要跳转到login重新登陆。

登入成功后还得有个退出登登入的功能

直接上代码

前端:

<el-dropdown-item?divided?command="loginout">退出登录</el-dropdown-item>
?if?(command?==?"loginout")?{
??????????????$http({method:'post',url:'/logout'}).then(data?=>?{
????????????????if?(data.success?===?true)?{
??????????????????ElMessage.success(data.msg);
??????????localStorage.removeItem("ms_token");?
??????????????????localStorage.removeItem("ms_username");?//去掉localStorage中的用户信息
??????????????????router.push("/login");
????????????????}?else?{
??????????????????ElMessage.error(data.msg);
????????????????}
??????????????})
????????????}

后端:

?@RequestMapping(value?=?"/logout",?method?=?RequestMethod.POST)
????public?Result?logout()?{

????????Subject?subject?=?SecurityUtils.getSubject();
????????subject.logout();?//shiro提供的方法,该方法会清除?session、principals,并把?authenticated?设置为?false

????????return?Result.ok("退出成功");
????}

遗留问题,用户认证问题下篇解决。

关于菜单、按钮授权,菜单、角色管理,是个大工程,要搞比较久就先不做了,最主要的是对这个系统来说无用,个人博客,后台管理系统也就一个人用,一个账号所有权限都有就够了。等博客问世后面有时间再搞吧。

在博客中查看:从零开始做网站6-springboot集成shiro+vue实现登录和权限控制 - ZJBLOG?

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-05-26 15:14:28  更:2022-05-26 15:15:08 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 20:55:42-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码