shiro學習筆記


1.shiro介紹

1.1介紹

shiro是apache的一個開源框架,是一個權限管理的框架,實現 用戶認證、用戶授權。

shiro不依賴於spring,shiro不僅可以實現 web應用的權限管理,還可以實現c/s系統,分布式系統權限管理,shiro屬於輕量框架,越來越多企業項目開始使用shiro。

使用shiro實現系統的權限管理,有效提高開發效率,從而降低開發成本。

1.2shrio功能特點:

 

1) Authentication:身份認證/登錄,驗證用戶是不是擁有相應的身份。

2) Authorization:授權,即權限驗證,驗證某個已認證的用戶是否擁有某個權限;即判斷用戶是否能做事情,常見的如:驗證某個用戶是否擁有某個角色。或者細粒度的驗證某個用戶對某個資源是否具有某個權限。

3) Session Manager:會話管理,即用戶登錄后就是一次會話,在沒有退出之前,它的所有信息都在會話中;會話可以是普通 JavaSE 環境的,也可以是如 Web 環境的。

4) Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲。

5) Web Support:Web支持,可以非常容易的集成到 web 環境。

6) Caching:緩存,比如用戶登錄后,其用戶信息、擁有的角色/權限不必每次去查,這樣可以提高效率。

7) Concurrency:shiro 支持多線程應用的並發驗證,即如在一個線程中開啟另一個線程,能把權限自動傳播過去。

8) Testing:提供測試支持。

9) Run As:允許一個用戶假裝為另一個用戶(如果他們允許)的身份進行訪問。

10) Remember Me:記住我,這個是非常常見的功能,即一次登錄后,下次再來的話不用登錄了。

 

1.3shiro運行原理

 

 

1) Subject:主體,可以看到主體可以是任何與應用交互的“用戶”。

2) SecurityManager:相當於 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher。它是 Shiro 的核心,所有具體的交互都通過 SecurityManager 進行控制。它管理着所有 Subject、且負責進行認證和授權、及會話、緩存的管理。

3) Authenticator:認證器,負責主體認證的,這是一個擴展點,如果用戶覺得 Shiro 默認的不好,我們可以自定義實現。其需要認證策略(Authentication Strategy),即什么情況下算用戶認證通過了。

4) Authrizer:授權器,或者訪問控制器。它用來決定主體是否有權限進行相應的操作,即控制着用戶能訪問應用中的哪些功能。

5) Realm:可以有1個或多個 Realm,可以認為是安全實體數據源,即用於獲取安全實體的。它可以是 JDBC 實現,也可以是 LDAP 實現,或者內存實現等。

6) SessionManager:如果寫過 Servlet 就應該知道 Session 的概念,Session 需要有人去管理它的生命周期,這個組件就是 SessionManager。而 Shiro 並不僅僅可以用在 Web 環境,也可以用在如普通的 JavaSE 環境。

7) SessionDAO:DAO 大家都用過,數據訪問對象,用於會話的 CRUD。我們可以自定義 SessionDAO 的實現,控制 session 存儲的位置。如通過 JDBC 寫到數據庫或通過 jedis 寫入 redis 中。另外 SessionDAO 中可以使用 Cache 進行緩存,以提高性能。

8) CacheManager:緩存管理器。它來管理如用戶、角色、權限等的緩存的。因為這些數據基本上很少去改變,放到緩存中后可以提高訪問的性能。

9) Cryptography:密碼模塊,Shiro 提高了一些常見的加密組件用於如密碼加密/解密的。

 

1.4過濾器

當 Shiro 被運用到 web 項目時,Shiro 會自動創建一些默認的過濾器對客戶端請求進行過濾。以下是 Shiro 提供的過濾器:

過濾器簡稱

對應的 Java

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

logout

org.apache.shiro.web.filter.authc.LogoutFilter

noSessionCreation

org.apache.shiro.web.filter.session.NoSessionCreationFilter

解釋:

/admins/**=anon               # 表示該 uri 可以匿名訪問

/admins/**=auth               # 表示該 uri 需要認證才能訪問

/admins/**=authcBasic         # 表示該 uri 需要 httpBasic 認證

/admins/**=perms[user:add:*]  # 表示該 uri 需要認證用戶擁有 user:add:* 權限才能訪問

/admins/**=port[8081]         # 表示該 uri 需要使用 8081 端口

/admins/**=rest[user]         # 相當於 /admins/**=perms[user:method],其中,method 表示  get、post、delete 等

/admins/**=roles[admin]       # 表示該 uri 需要認證用戶擁有 admin 角色才能訪問

/admins/**=ssl                # 表示該 uri 需要使用 https 協議

/admins/**=user               # 表示該 uri 需要認證或通過記住我認證才能訪問

/logout=logout                # 表示注銷,可以當作固定配置

注意:

anon,authcBasic,auchc,user 是認證過濾器。

perms,roles,ssl,rest,port 是授權過濾器。

 

2.spring+springmvc+mybatis+shrio權限認證

2.1 pom.xml

<!-- shiro -->

        <dependency>

            <groupId>org.apache.shiro</groupId>

            <artifactId>shiro-spring</artifactId>

            <version>1.2.3</version>

        </dependency>

        <dependency>

            <groupId>org.apache.shiro</groupId>

            <artifactId>shiro-ehcache</artifactId>

            <version>1.2.3</version>

        </dependency>

        <dependency>

            <groupId>org.apache.shiro</groupId>

            <artifactId>shiro-core</artifactId>

            <version>1.2.3</version>

        </dependency>

        <dependency>

            <groupId>org.apache.shiro</groupId>

            <artifactId>shiro-web</artifactId>

            <version>1.2.3</version>

        </dependency>

        <dependency>

            <groupId>org.apache.shiro</groupId>

            <artifactId>shiro-quartz</artifactId>

            <version>1.2.3</version>

        </dependency>

 

2.2 web.xml配置shiro

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">

  <context-param>

    <param-name>contextConfigLocation</param-name>

    <param-value>

            classpath*:applicationContext.xml

            classpath*:applicationContext-shiro.xml

       </param-value>

  </context-param>

  <listener>

    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>

  </listener>

  <!-- 用戶單會話過濾器 -->

  <!--  <filter>

    <filter-name>userSingleSessionFilter</filter-name>

    <filter-class>com.vulnverify.web.filter.UserSingleSessionFilter</filter-class>

  </filter>

   <filter-mapping>

    <filter-name>userSingleSessionFilter</filter-name>

    <url-pattern>/*</url-pattern>

  </filter-mapping>-->

 

  <!-- 字符編碼過濾器 -->

  <filter>

    <filter-name>encodingFilter</filter-name>

    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>

    <init-param>

      <param-name>encoding</param-name>

      <param-value>UTF-8</param-value>

    </init-param>

    <init-param>

      <param-name>forceEncoding</param-name>

      <param-value>true</param-value>

    </init-param>

  </filter>

  <filter-mapping>

    <filter-name>encodingFilter</filter-name>

    <url-pattern>/*</url-pattern>

  </filter-mapping>

   <filter>

   <!-- shiro權限 -->

    <filter-name>shiroFilter</filter-name>

    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>

    <async-supported>true</async-supported>

    <init-param>

      <param-name>targetFilterLifecycle</param-name>

      <param-value>true</param-value>

    </init-param>

  </filter>

  <filter-mapping>

    <filter-name>shiroFilter</filter-name>

    <url-pattern>/*</url-pattern>

  </filter-mapping>

  <!-- log4j日志 -->

  <context-param>

    <param-name>log4jConfigLocation</param-name>

    <param-value>classpath:log4j.properties</param-value>

  </context-param>

  <context-param>

    <param-name>log4jRefreshInterval</param-name>

    <param-value>60000</param-value>

  </context-param>

  <listener>

    <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>

  </listener>

  <!-- springmvc DispatcherServlet -->

  <servlet>

    <servlet-name>dispatcher</servlet-name>

    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <init-param>

      <param-name>contextConfigLocation</param-name>

      <param-value>classpath*:spring-mvc.xml</param-value>

    </init-param>

    <load-on-startup>1</load-on-startup>

  </servlet>

  <servlet-mapping>

    <servlet-name>dispatcher</servlet-name>

    <url-pattern>/</url-pattern>

  </servlet-mapping>

  <welcome-file-list>

    <welcome-file>/index.jsp</welcome-file>

  </welcome-file-list>

  <error-page>

    <error-code>404</error-code>

    <location>/page/404</location>

  </error-page>

  <error-page>

    <error-code>500</error-code>

    <location>/page/500</location>

  </error-page>

  <error-page>

    <exception-type>org.apache.shiro.authz.AuthorizationException</exception-type>

    <location>/page/401</location>

  </error-page>

  <error-page>

    <error-code>400</error-code>

    <location>/page/400</location>

  </error-page>

</web-app>

 

2.3 shrio配置文件 applicationContext-shiro.xml

 

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="

       http://www.springframework.org/schema/beans classpath:schema/spring-beans.xsd

       http://www.springframework.org/schema/util classpath:schema/spring-util.xsd">

 

    <description>apache shiro配置</description>

 

     <!-- web.xml中shiro的filter對應的bean -->

    <!-- Shiro 的Web過濾器 -->

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">

        <property name="securityManager" ref="securityManager"/>

         <!-- loginUrl認證提交地址,如果沒有認證將會請求此地址進行認證 -->

        <property name="loginUrl" value="/static/login/loginPanel/login.html"/>

        <!-- 認證成功跳轉界面 -->

        <property name="successUrl" value="/static/index.html"/>

        <!-- 通過unauthorizedUrl指定沒有權限操作時跳轉頁面 -->

        <property name="unauthorizedUrl" value="/page/401"/>

        <property name="filterChainDefinitions">

            <value>

                <!-- 靜態資源允許訪問 -->

                /static/** = anon

                <!-- 登錄頁允許訪問 -->

                /user/login = anon

                /publicKey = anon

                /page/exception = anon

                /verificationCode = anon

                <!-- 其他資源需要認證 -->

                /** = authc

            </value>

        </property>

    </bean>

 

 <!-- 安全管理器 -->

    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">

        <!-- 注入realm -->

        <property name="realms">

            <list>

                <ref bean="securityRealm"/>

            </list>

            </property>

        <!-- 注入緩存管理器 -->

        <property name="cacheManager" ref="shiroEhcacheManager" />

        <!-- 注入sessiong管理器 -->

        <property name="sessionManager" ref="sessionManager" />

         <!-- 記住我 -->

        <property name="rememberMeManager" ref="rememberMeManager" />

    </bean>

 

    <!-- 緩存管理器 使用Ehcache實現 -->

    <bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">

        <property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>

    </bean>

 

    <!-- 會話DAO -->

    <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.MemorySessionDAO"/>

   <!--  <bean id="sessionDAO" class="com.vulnverify.web.session.SessionRedisDao"/> -->

 

    <!-- 會話管理器 -->

    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">

<!--     <bean id="sessionManager" class="com.vulnverify.web.security.WebSessionManager"> -->

       <!-- session的失效時長,單位毫秒 -->

        <property name="globalSessionTimeout" value="600000" />

        <!-- 刪除失效的session -->

        <property name="deleteInvalidSessions" value="true" />

    </bean>

 

    <!-- sessionIdCookie的實現,用於重寫覆蓋容器默認的JSESSIONID --> 

    <bean id="sharesession" class="org.apache.shiro.web.servlet.SimpleCookie"

        <!-- cookie的name,對應的默認是 JSESSIONID --> 

        <constructor-arg name="name" value="SHAREJSESSIONID" />

        <!-- jsessionId的path為 / 用於多個系統共享jsessionId --> 

        <property name="path" value="/" /> 

        <property name="httpOnly" value="true"/> 

    </bean>

      <!-- rememberMeManager管理器,寫cookie,取出cookie生成用戶信息 -->

    <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">

        <property name="cookie" ref="rememberMeCookie" />

    </bean>

    <!-- 記住我cookie -->

    <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">

        <!-- rememberMe是cookie的名字 -->

        <constructor-arg value="rememberMe" />

        <!-- 記住我cookie生效時間30天 -->

        <property name="maxAge" value="2592000" />

    </bean>

  

    <!-- 自定義 realm 安全數據 -->

    <bean id="securityRealm" class="com.vulnverify.web.security.SecurityRealm"></bean>

   

    <!-- Shiro生命周期處理器 -->

    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

 

</beans>

 

2.4 自定義realm 安全數據庫

package com.vulnverify.web.security;

 

import java.util.List;

 

import javax.annotation.Resource;

 

import org.apache.shiro.SecurityUtils;

import org.apache.shiro.authc.AuthenticationException;

import org.apache.shiro.authc.AuthenticationInfo;

import org.apache.shiro.authc.AuthenticationToken;

import org.apache.shiro.authc.SimpleAuthenticationInfo;

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 org.springframework.stereotype.Component;

 

import com.vulnverify.web.model.TSysRights;

import com.vulnverify.web.model.TSysRole;

import com.vulnverify.web.model.TUser;

import com.vulnverify.web.service.SysRightsService;

import com.vulnverify.web.service.SysRoleService;

import com.vulnverify.web.service.UserService;

 

/**

 * 用戶身份驗證,授權 Realm 組件

 *

 * @author linan

 **/

@Component(value = "securityRealm")

public class SecurityRealm extends AuthorizingRealm {

 

    @Resource

    private UserService sysUserService;

   

    @Resource

    private SysRoleService sysRoleService;

   

    @Resource

    private SysRightsService sysRightsService;

   

    /**

     * 權限檢查

     */

    @Override

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

        final TUser user = (TUser)SecurityUtils.getSubject().getSession(false).getAttribute("userInfo");

        String roleType = user.getUserType();//用戶角色

        //角色信息

        final TSysRole roleInfos = sysRoleService.selectRoleByRoleType(Integer.parseInt(roleType));

        if(null!=roleInfos){

            // 添加角色

            authorizationInfo.addRole(roleInfos.getRoleName());

            //根據角色id查詢角色權限

            final List<TSysRights> sysRightsList = sysRightsService.selectSysRightsByRoleId(roleInfos.getRoleId());

            for (TSysRights sysRights : sysRightsList) {

                // 添加權限

                authorizationInfo.addStringPermission(sysRights.getRightCode());

            }

        }

        return authorizationInfo;

    }

 

    /**

     * 身份驗證信息

     */

    @Override

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

             //身份

        String loginName = String.valueOf(token.getPrincipal());

        //密碼

        String password = new String((char[]) token.getCredentials());

        TUser su = new TUser();

        su.setUserAccount(loginName);

        su.setPassword(password);

        // 通過數據庫進行驗證

        final TUser authentication = sysUserService.authentication(su);

        if (authentication == null) {

            throw new AuthenticationException("用戶名或密碼錯誤.");

        }

        //交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配

        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(loginName, password, getName());

        return authenticationInfo;

    }

 

}

 

2.5 登陸controller

package com.vulnverify.web.controller;

 

import java.util.Date;

 

import javax.annotation.Resource;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import javax.servlet.http.HttpSession;

import javax.validation.Valid;

 

import org.apache.shiro.SecurityUtils;

import org.apache.shiro.authc.UsernamePasswordToken;

import org.apache.shiro.session.Session;

import org.apache.shiro.subject.Subject;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

import org.springframework.web.bind.annotation.ResponseBody;

 

import com.vulnverify.core.entity.PageData;

import com.vulnverify.core.entity.PageQuery;

import com.vulnverify.core.entity.SimpleException;

import com.vulnverify.core.orm.mybatis.Page;

import com.vulnverify.core.redis.RedisDb;

import com.vulnverify.core.utils.ApplicationUtils;

import com.vulnverify.core.utils.DateUtil;

import com.vulnverify.web.constant.Constant;

import com.vulnverify.web.model.TUser;

import com.vulnverify.web.model.requestbody.IdReqBody;

import com.vulnverify.web.model.requestbody.LoginReqBody;

import com.vulnverify.web.model.requestbody.UserCreateReqBody;

import com.vulnverify.web.model.requestbody.UserListReqBody;

import com.vulnverify.web.model.requestbody.UserModifyReqBody;

import com.vulnverify.web.model.responsebody.LoginResBody;

import com.vulnverify.web.model.view.UserView;

import com.vulnverify.web.service.UserService;

 

 

/**

 * 用戶控制類

 * @author linan

 * @date 2018年4月23日 

 *

 */

@Controller

@RequestMapping(value="/user")

public class UserController extends BaseController{

         private static Logger logger=LoggerFactory.getLogger(UserController.class);

         @Resource

         private UserService userService;

          /**驗證碼驗證是否開啟的標識*/

    @Value("${verfication.code.check}") 

    private String verficationCodeCheck = "true";

 

         /**

          * 登陸

          * @param loginBody

          * @param request

          * @param response

          * @return

          * @throws Exception

          */

         @RequestMapping(value = "/login", method = RequestMethod.POST)

         public Object login(@Valid @RequestBody LoginReqBody loginBody,HttpServletRequest request,HttpServletResponse response) throws Exception{

                   Subject subject = SecurityUtils.getSubject();

        Session session = subject.getSession(false);

             try{

           

            //驗證碼

                       /*String verificationCode = (String)session.getAttribute("verificationCode");

            if("true".equals(verficationCodeCheck)){

                     if(verificationCode == null || !verificationCode.equalsIgnoreCase(loginBody.getVerificationCode())){

                         throw new SimpleException(Constant.EXCEPTION_S0010005,

                                            ApplicationUtils.getMessage(Constant.EXCEPTION_S0010005));

                }

            }*/

           

            if (subject.isAuthenticated()) {

                     throw new SimpleException(Constant.EXCEPTION_S0010006,

                                        ApplicationUtils.getMessage(Constant.EXCEPTION_S0010006));

            }

            

            final TUser authUserInfo = userService.getUserByUserAccout(loginBody.getUserAccount());

            if(authUserInfo != null){

                     if(authUserInfo.getStatus() == Constant.USER_STATE_UNABLE){

                               throw new Exception("用戶"+authUserInfo.getUserAccount()+"已被停用");

                     }

            }

            String sha256Hex = ApplicationUtils.sha256Hex(loginBody.getPassword());

            String password = authUserInfo.getPassword();

            if(sha256Hex.equals(password)){

                     System.out.println("------true---------");

            }

//調用shrio的自定義realm的doGetAuthenticationInfo驗證身份           

 subject.login(                          new UsernamePasswordToken(

                                                 loginBody.getUserAccount(), ApplicationUtils.sha256Hex(loginBody.getPassword())));

           

            session.setAttribute("userInfo", authUserInfo);

            session.setAttribute("userKey",loginBody.getUserKey());

            logger.info("login userKey is "+session.getId()+":"+loginBody.getUserKey());

           

          /*  String key = "loginUser."+authUserInfo.getUserAccount();

            RedisDb.setString(key, session.getId().toString());

            RedisDb.expireString(key, 1800);*/

           

            LoginResBody lrb = new LoginResBody();

            lrb.setId(authUserInfo.getUserId()+"");

            lrb.setUserAccount(authUserInfo.getUserAccount());

            lrb.setUserName(authUserInfo.getUserName());

           

            return generateResultData(lrb);

             }catch(Exception e){

                       TUser sysUser = userService.getUserByUserAccout(loginBody.getUserAccount());

                       if(sysUser != null){

                                session.setAttribute("failUserInfo", sysUser);

//                             ApplicationUtils.optData2Request(sysUser.getUserName());

                       }

                       throw e;

             }finally{

                       session.removeAttribute("verificationCode");

             }

         }

         }

 

2.6權限驗證:

2.6.1 shiro什么時候會進入doGetAuthorizationInfo(PrincipalCollection principals)

會進入授權方法一共有三種情況!

1、subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去調用這個是否有什么角色或者是否有什么權限的時候;

2、在方法上加注解的時候

    @RequiresRoles("admin") :角色驗證

   @RequiresPermissions(value = PermissionSign.GET_ORGIP_LIST)權限驗證

    PermissionSign為常量類。與realm中授權的添加權限對應。

 

3、[@shiro.hasPermission name = "admin"][/@shiro.hasPermission]:在頁面上加shiro標簽的時候,即進這個頁面的時候掃描到有這個標簽的時候。

 

2.7運行流程:

1.登陸調用userController登陸中的 subject.login(new UsernamePasswordToken(                        loginBody.getUserAccount(), ApplicationUtils.sha256Hex(loginBody.getPassword())));方法

2.調用自定義realm中的doGetAuthenticationInfo(AuthenticationToken token)進行身份登陸驗證。

 

3.方法中

@RequiresPermissions()權限會去自定義realm的授權接口doGetAuthorizationInfo(PrincipalCollection principals) 去授權,然后判斷是否有操作此方法的權限。

 

3.參考文檔:

https://www.cnblogs.com/moonlightL/p/8126910.html

https://blog.csdn.net/mine_song/article/details/61616259

http://jinnianshilongnian.iteye.com/blog/2022468


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM