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知识库 -> Shrio与SpringBoot整合(二)配置多个Realm -> 正文阅读

[Java知识库]Shrio与SpringBoot整合(二)配置多个Realm

shrio与springboot的简单整合:

1、Spring Boot 与Shrio的简单整合,适合做单体项目

2、Shrio 的会话管理,可以用在前后端分离的项目

3、Shrio 使用redis做缓存

可以参考文章:Shrio与SpringBoot整合(一)_我是混IT圈的-CSDN博客

这里主要是Shiro配置多个Realm进行鉴权:

总共三个Reaml,分别是老师、家长、学生,其他老师是免密登录,家长和学生是账号密码登录。

Shrio配置的时候,这里没有配置密码加密,所以这里的账号密码都是明文。

配置多个Realm有两种情况:

1、用户的登录只能使用1个Realm进行鉴权。

2、用户的登录需要通过多个Realm进行鉴权。

shiro在多个realm同时生效时提供了三种策略:
1、org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy 至少一个成功的策略-默认使用
2、org.apache.shiro.authc.pam.AllSuccessfulStrategy 全部成功策略
3、org.apache.shiro.authc.pam.FirstSuccessFulStrategy 第一个成功策略

管理realm的类是:ModularRealmAuthenticator

实现的过程简单说下:

重写 UsernamePasswordToken,加一个 loginType 属性,subject.login() 的时候传入 loginType; 重写 ModularRealmAuthenticator 中的 doAuthenticate() 方法,根据传进来的 loginType 来指定使用哪个 Realm。

废话不多说,上代码:

1、pom.xml 文件就不写了,如果不知道,看参考文档

2、创建Realm

老师:TeacherRealm

package com.example.springbootshrio.shiro;

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.subject.PrincipalCollection;

import java.util.HashSet;
import java.util.Set;

/**
 * 老师的realm
 * 老师的登录为免密登录
 */
public class TeacherRealm extends AuthorizingRealm {
    //这里可以注入其他的服务,去查询用户的密码、查询用户的权限等信息

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //用户名
        String username = (String) SecurityUtils.getSubject().getPrincipal();
        System.out.println("-------根据用户名,获取权限--------");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //权限code,要唯一的,之后的权限验证就是通过匹配它来做的
        Set<String> stringPermissions = new HashSet<>();
        stringPermissions.add("teacher:list");
        stringPermissions.add("teacher:update");
        info.setStringPermissions(stringPermissions);
        //还可以通过角色来做权限验证
        Set<String> roles = new HashSet<>();
        roles.add("老师");
        info.setRoles(roles);
        return info;
    }

    /**
     * 这里可以注入userService,为了方便演示,我就写死了帐号了密码
     * private UserService userService;
     * <p>
     * 用户名和密码的验证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println(getName()+"-------身份认证方法--------");
        //用户名,也就是openId
        String userName = (String) authenticationToken.getPrincipal();
        //通过数据库查询这个openId是否正确,如果正确,那么就可以验证了,因为免密登录这里的密码都是固定的,比如都是123456



        //根据用户名从数据库获取密码
        String password = "123456";
        if (userName == null) {
            throw new AccountException("用户名或密码不正确");
        }
        //由shiro来做密码校验  如果身份认证验证成功,返回一个AuthenticationInfo实现;
        return new SimpleAuthenticationInfo(userName, //用户名
                password, //密码
                getName() //当前 realm 的名字
        );
    }
}

家长:ParentsRealm

package com.example.springbootshrio.shiro;

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.subject.PrincipalCollection;

import java.util.HashSet;
import java.util.Set;

/**
 * 家长的realm
 * 需要账号密码登录
 */
public class ParentsRealm extends AuthorizingRealm {
    //这里可以注入其他的服务,去查询用户的密码、查询用户的权限等信息

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //用户名
        String username = (String) SecurityUtils.getSubject().getPrincipal();
        System.out.println("-------根据用户名,获取权限--------");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //权限code,要唯一的,之后的权限验证就是通过匹配它来做的
        Set<String> stringPermissions = new HashSet<>();
        stringPermissions.add("parents:list");
        stringPermissions.add("parents:update");
        info.setStringPermissions(stringPermissions);
        //还可以通过角色来做权限验证
        Set<String> roles = new HashSet<>();
        roles.add("家长");
        info.setRoles(roles);
        return info;
    }

    /**
     * 这里可以注入userService,为了方便演示,我就写死了帐号了密码
     * private UserService userService;
     * <p>
     * 用户名和密码的验证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println(getName()+"-------身份认证方法--------");
        //用户名
        String userName = (String) authenticationToken.getPrincipal();
        if (userName == null) {
            throw new AccountException("用户名或密码不正确");
        }


        //根据用户名从数据库获取密码
        String password = "123";
        //由shiro来做密码校验  如果身份认证验证成功,返回一个AuthenticationInfo实现;
        return new SimpleAuthenticationInfo(userName, //用户名
                password, //密码
                getName() //当前 realm 的名字
        );
    }
}

学生:StudentsRealm

package com.example.springbootshrio.shiro;

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.subject.PrincipalCollection;

import java.util.HashSet;
import java.util.Set;

/**
 * 学生的realm
 * 需要账号密码登录
 */
public class StudentsRealm extends AuthorizingRealm {
    //这里可以注入其他的服务,去查询用户的密码、查询用户的权限等信息

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //用户名
        String username = (String) SecurityUtils.getSubject().getPrincipal();
        System.out.println("-------根据用户名,获取权限--------");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //权限code,要唯一的,之后的权限验证就是通过匹配它来做的
        Set<String> stringPermissions = new HashSet<>();
        stringPermissions.add("students:list");
        stringPermissions.add("students:update");
        info.setStringPermissions(stringPermissions);
        //还可以通过角色来做权限验证
        Set<String> roles = new HashSet<>();
        roles.add("学生");
        info.setRoles(roles);
        return info;
    }

    /**
     * 这里可以注入userService,为了方便演示,我就写死了帐号了密码
     * private UserService userService;
     * <p>
     * 用户名和密码的验证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println(getName()+"-------身份认证方法--------");
        //用户名
        String userName = (String) authenticationToken.getPrincipal();
        
        if (userName == null) {
            throw new AccountException("用户名或密码不正确");
        }

        //根据用户名从数据库获取密码
        String password = "123";

        //由shiro来做密码校验  如果身份认证验证成功,返回一个AuthenticationInfo实现;
        return new SimpleAuthenticationInfo(userName, //用户名
                password, //密码
                getName() //当前 realm 的名字
        );
    }
}
系统自带的Realm管理,主要针对多realm:UserModularRealmAuthenticator
package com.example.springbootshrio.shiro;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * 系统自带的Realm管理,主要针对多realm
 * 用来判断当前登录应该使用某个或者多个realm做登录授权
 */
public class UserModularRealmAuthenticator extends ModularRealmAuthenticator {
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("UserModularRealmAuthenticator:method doAuthenticate() execute ");
        // 判断getRealms()是否返回为空
        assertRealmsConfigured();
        // 强制转换回自定义的 CustomizedToken
        UserToken userToken = (UserToken) authenticationToken;
        // 登录类型
        String loginType = userToken.getLoginType();
        // 所有Realm
        Collection<Realm> realms = getRealms();
        // 登录类型对应的所有Realm
        List<Realm> typeRealms = new ArrayList<>();
        for (Realm realm : realms) {
            if (realm.getName().contains(loginType)) {
                typeRealms.add(realm);
            }
        }
        // 判断是单Realm还是多Realm
        if (typeRealms.size() == 1){
            System.out.println("doSingleRealmAuthentication() execute ");
            return doSingleRealmAuthentication(typeRealms.get(0), userToken);
        } else{
            System.out.println("doMultiRealmAuthentication() execute ");
            return doMultiRealmAuthentication(typeRealms, userToken);
        }
    }
}
重写UsernamePasswordToken类,用来区分登录账号的时候,到底该使用那个Realm来做鉴权
package com.example.springbootshrio.shiro;

import org.apache.shiro.authc.UsernamePasswordToken;

public class UserToken extends UsernamePasswordToken {
    //类型:Teacher-老师 Parents-家长 Students-学生
    private String loginType;

    public UserToken() {
    }

    public UserToken(final String username, final String password, final String loginType) {
        super(username, password);
        this.loginType = loginType;
    }

    public String getLoginType() {
        return loginType;
    }

    public void setLoginType(String loginType) {
        this.loginType = loginType;
    }
}

Shiro的配置:ShiroConfig

需要配置shiroFilter,因为写了三个Realm进行鉴权,所以拦截器这里要把这三个都配置成免登录的。

@Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/login.html");//登录页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");//未经授权就可以访问的页面
        shiroFilterFactoryBean.setSuccessUrl("/successUrl");//成功页面
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        // anon:所有url都都可以匿名访问,一般写静态资源和登录页面等
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/login2", "anon");
        filterChainDefinitionMap.put("/teacherLogin", "anon");//老师登录接口
        filterChainDefinitionMap.put("/parentsLogin", "anon");//家长登录接口
        filterChainDefinitionMap.put("/studentsLogin", "anon");//学生登录接口
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/front/**", "anon");
        filterChainDefinitionMap.put("/api/**", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");

        //authc:所有url都必须认证通过才可以访问
        filterChainDefinitionMap.put("/admin/**", "authc");
        filterChainDefinitionMap.put("/user/**", "authc");
        //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 剩余的都需要认证
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 创建 SecurityManager 并且绑定 Realm
     * @return
     */
    @Bean
    public org.apache.shiro.mgt.SecurityManager securityManager() {
        DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
        //使用自定义的realm管理器  这个是新增的
        defaultSecurityManager.setAuthenticator(modularRealmAuthenticator());
        // 配置多个Realm也可以配置一个
        List<Realm> realms = new ArrayList<>();
        realms.add(customRealm());
        realms.add(teacherRealm());
        realms.add(parentsRealm());
        realms.add(studentsRealm());
        defaultSecurityManager.setRealms(realms);
        // 自定义session管理
        defaultSecurityManager.setSessionManager(sessionManager());
        // 自定义缓存实现 使用redis
        defaultSecurityManager.setCacheManager(redisCacheManager());
        return defaultSecurityManager;
    }

    //自定义的三个Realm
    @Bean
    public TeacherRealm teacherRealm(){
        TeacherRealm teacherRealm = new TeacherRealm();
        return teacherRealm;
    }
    @Bean
    public ParentsRealm parentsRealm(){
        ParentsRealm parentsRealm = new ParentsRealm();
        return parentsRealm;
    }
    @Bean
    public StudentsRealm studentsRealm(){
        StudentsRealm studentsRealm = new StudentsRealm();
        return studentsRealm;
    }

    /**
     * 系统自带的Realm管理,主要针对多realm
     * */
    @Bean
    public ModularRealmAuthenticator modularRealmAuthenticator(){
        //自己重写的ModularRealmAuthenticator
        UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
        /*
            shiro在多个realm同时生效时提供了三种策略:
        1、org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy 至少一个成功的策略-默认使用
        2、org.apache.shiro.authc.pam.AllSuccessfulStrategy 全部成功策略
        3、org.apache.shiro.authc.pam.FirstSuccessFulStrategy 第一个成功策略
        可以通过 setAuthenticationStrategy 方法设置策略
        这里就是如果登录类型同时存在多个realm时,应该怎么通过鉴权
        */
        modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return modularRealmAuthenticator;
    }

使用:LoginController

package com.example.springbootshrio.controller;

import com.example.springbootshrio.shiro.UserToken;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class LoginController {

    /**
     * 老师免密登录
     * 这里的免密登录一般用于微信小程序,当获取到openId时,密码都写死,比如都是123456,那么每次登录验证的时候,只需要验证openId是否正确就好了,因为密码都是123456而已。
     */
    @ResponseBody
    @PostMapping(value = "/teacherLogin")
    public String teacherLogin(@RequestParam("username") String openId){
        // 从 SecurityUtils 里边创建一个 subject
        Subject subject = SecurityUtils.getSubject();
        // 在认证提交前准备 token(令牌)
        UserToken token = new UserToken(openId, "123456","Teacher");
        //session会话
        Session session = subject.getSession();
        // 执行认证登陆
        try {
            subject.login(token);
        } catch (UnknownAccountException uae) {
            return "未知账户";
        } catch (IncorrectCredentialsException ice) {
            return "密码不正确";
        } catch (LockedAccountException lae) {
            return "账户已锁定";
        } catch (ExcessiveAttemptsException eae) {
            return "用户名或密码错误次数过多";
        } catch (AuthenticationException ae) {
            return "用户名或密码不正确!";
        }
        //登录成功,跳转到index页面
        if (subject.isAuthenticated()) {
            return session.getId().toString();
        } else {
            token.clear();
            return "登录失败";
        }
    }


    /**
     * 家长用户名密码登录
     */
    @ResponseBody
    @PostMapping(value = "/parentsLogin")
    public String parentsLogin(@RequestParam("username") String username, @RequestParam("password") String password){
        // 从 SecurityUtils 里边创建一个 subject
        Subject subject = SecurityUtils.getSubject();
        // 在认证提交前准备 token(令牌)
        UserToken token = new UserToken(username, password,"Parents");
        //session会话
        Session session = subject.getSession();
        // 执行认证登陆
        try {
            subject.login(token);
        } catch (UnknownAccountException uae) {
            return "未知账户";
        } catch (IncorrectCredentialsException ice) {
            return "密码不正确";
        } catch (LockedAccountException lae) {
            return "账户已锁定";
        } catch (ExcessiveAttemptsException eae) {
            return "用户名或密码错误次数过多";
        } catch (AuthenticationException ae) {
            return "用户名或密码不正确!";
        }
        //登录成功,跳转到index页面
        if (subject.isAuthenticated()) {
            return session.getId().toString();
        } else {
            token.clear();
            return "登录失败";
        }
    }


    /**
     * 学生用户名密码登录
     */
    @ResponseBody
    @PostMapping(value = "/studentsLogin")
    public String studentsLogin(@RequestParam("username") String username, @RequestParam("password") String password){
        // 从 SecurityUtils 里边创建一个 subject
        Subject subject = SecurityUtils.getSubject();
        // 在认证提交前准备 token(令牌)
        UserToken token = new UserToken(username, password,"Students");
        //session会话
        Session session = subject.getSession();
        // 执行认证登陆
        try {
            subject.login(token);
        } catch (UnknownAccountException uae) {
            return "未知账户";
        } catch (IncorrectCredentialsException ice) {
            return "密码不正确";
        } catch (LockedAccountException lae) {
            return "账户已锁定";
        } catch (ExcessiveAttemptsException eae) {
            return "用户名或密码错误次数过多";
        } catch (AuthenticationException ae) {
            return "用户名或密码不正确!";
        }
        //登录成功,跳转到index页面
        if (subject.isAuthenticated()) {
            return session.getId().toString();
        } else {
            token.clear();
            return "登录失败";
        }
    }
}

html页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="/jquery.min.js"></script>
</head>
<body>
    <span>老师登录</span>
  <form method="post" action="/teacherLogin">
      <table>
          <tr>
              <td>用户名:</td>
              <td>密码:</td>
          </tr>
          <tr>
              <td><input type="text" name="username" id="username1"></td>
              <td><input type="text" name="password" id="password1"></td>
          </tr>
          <tr>
              <td><input type="button" id="_button1" value="当前页面" /></td>
          </tr>
      </table>
  </form>
    <span>家长登录</span>
  <form method="post" action="/parentsLogin">
      <table>
          <tr>
              <td>用户名:</td>
              <td>密码:</td>
          </tr>
          <tr>
              <td><input type="text" name="username" id="username2"></td>
              <td><input type="text" name="password" id="password2"></td>
          </tr>
          <tr>
              <td><input type="button" id="_button2" value="当前页面" /></td>
          </tr>
      </table>
  </form>
    <span>学生登录</span>
  <form method="post" action="/studentsLogin">
      <table>
          <tr>
              <td>用户名:</td>
              <td>密码:</td>
          </tr>
          <tr>
              <td><input type="text" name="username" id="username3"></td>
              <td><input type="text" name="password" id="password3"></td>
          </tr>
          <tr>
              <td><input type="button" id="_button3" value="当前页面" /></td>
          </tr>
      </table>
  </form>
</body>
<script>
    var token = "";
    //老师登录
    $("#_button1").click(function () {
        var username = $("#username1").val();
        var password = $("#password1").val();
        $.ajax({
            type: "post",
            url:"/teacherLogin",
            data:{"username":username,"password":password},
            success: function (res) {
                alert(res);
                token = res;
            }
        });
    });
    //家长登录
    $("#_button2").click(function () {
        var username = $("#username2").val();
        var password = $("#password2").val();
        $.ajax({
            type: "post",
            url:"/parentsLogin",
            data:{"username":username,"password":password},
            success: function (res) {
                alert(res);
                token = res;
            }
        });
    });
    //学生登录
    $("#_button3").click(function () {
        var username = $("#username3").val();
        var password = $("#password3").val();
        $.ajax({
            type: "post",
            url:"/studentsLogin",
            data:{"username":username,"password":password},
            success: function (res) {
                alert(res);
                token = res;
            }
        });
    });
</script>
</html>

需要注意的是:

这里总共是需要重写两个类:

ModularRealmAuthenticator:shrio自带的多个Realm管理器,重写里面的doAuthenticate方法,来自定义让登录的账号执行其中某个或者多个Realm进行鉴权。

UsernamePasswordToken:登录时,存入账号密码的地方,在Realm鉴权时会从这里面取出来,但是这个类不能写入登录类型的字段,所以需要重写它,在里面加入登录类型字段,那么它就可以在?ModularRealmAuthenticator? 的重写方法里面根据登录类型的字段进行判,让登录用户使用其中某个或多个Realm进行鉴权了。

注意:登录类型的值要和创建的Realm的类名要一样,因为在?ModularRealmAuthenticator 的重写类中就是通过Realm的类名进行判断区分的。

代码如下:可以根据实际情况进行修改,代码都在??ModularRealmAuthenticator 的重写的类中。

// 所有Realm
        Collection<Realm> realms = getRealms();
        // 登录类型对应的所有Realm
        List<Realm> typeRealms = new ArrayList<>();
        for (Realm realm : realms) {
            if (realm.getName().contains(loginType)) {
                typeRealms.add(realm);
            }
        }

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-12-11 15:35:19  更:2021-12-11 15:36:15 
 
开发: 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/24 5:26:04-

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