zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    中考 2020 游记
    CodeChef 2020 July Long Challenge 题解
    GDOI2020 游记
    AtCoder Grand Contest 044 题解
    ISIJ2020 不知道算不算游记
    WC2020 拿铁记
    UOJ Round 19 题解
    本博客采用 CC BY-NC-SA 4.0 进行许可
    [算法模版]回文树
    AddressSanitizer
  • 原文地址:https://www.cnblogs.com/e206842/p/9047263.html
Copyright © 2011-2022 走看看