zoukankan      html  css  js  c++  java
  • SpringBoot学习:整合shiro(身份认证和权限认证),使用EhCache缓存

    一、在pom中引入依赖jar包

     1 <properties>  
     2     <shiro.version>1.3.2</shiro.version>  
     3 </properties>  
     4 
     5 <!--shiro start-->  
     6     <dependency>  
     7       <groupId>org.apache.shiro</groupId>  
     8       <artifactId>shiro-core</artifactId>  
     9       <version>${shiro.version}</version>  
    10     </dependency>  
    11     <dependency>  
    12       <groupId>org.apache.shiro</groupId>  
    13       <artifactId>shiro-web</artifactId>  
    14       <version>${shiro.version}</version>  
    15     </dependency>  
    16     <dependency>  
    17       <groupId>org.apache.shiro</groupId>  
    18       <artifactId>shiro-ehcache</artifactId>  
    19       <version>${shiro.version}</version>  
    20     </dependency>  
    21     <dependency>  
    22       <groupId>org.apache.shiro</groupId>  
    23       <artifactId>shiro-spring</artifactId>  
    24       <version>${shiro.version}</version>  
    25     </dependency>  
    26 <!--shiro end-->  

    二、shiro配置类:

      ShiroConfiguration:

      1 package com.example.demo;
      2 
      3 import org.apache.log4j.Logger;
      4 import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
      5 import org.springframework.context.annotation.Bean;
      6 import org.springframework.context.annotation.Configuration;
      7 
      8 import java.util.LinkedHashMap;
      9 import java.util.Map;
     10 
     11 /**
     12  * Shiro 配置
     13  * Apache Shiro 核心通过 Filter 来实现,就好像SpringMvc 通过DispachServlet 来主控制一样。
     14  * 既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。
     15  * Created by sun on 2017-4-2.
     16  */
     17 @Configuration
     18 @EnableTransactionManagement
     19 public class ShiroConfiguration{
     20 
     21     private final Logger logger = Logger.getLogger(ShiroConfiguration.class);
     22 
     23     /**
     24      * ShiroFilterFactoryBean 处理拦截资源文件问题。
     25      * 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在
     26      * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
     27      *
     28      Filter Chain定义说明
     29      1、一个URL可以配置多个Filter,使用逗号分隔
     30      2、当设置多个过滤器时,全部验证通过,才视为通过
     31      3、部分过滤器可指定参数,如perms,roles
     32      *
     33      */
     34     @Bean
     35     public EhCacheManager getEhCacheManager(){
     36         EhCacheManager ehcacheManager = new EhCacheManager();
     37         ehcacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
     38         return ehcacheManager;
     39     }
     40 
     41     //配置shiro仓库
     42     @Bean(name = "myShiroRealm")
     43     public MyShiroRealm myShiroRealm(EhCacheManager ehCacheManager){
     44         MyShiroRealm realm = new MyShiroRealm();
     45         realm.setCacheManager(ehCacheManager);
     46         return realm;
     47     }
     48 
     49     //把shiro生命周期交给spring boot管理
     50     @Bean(name = "lifecycleBeanPostProcessor")
     51     public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
     52         return new LifecycleBeanPostProcessor();
     53     }
     54 
     55     //DefaultAdvisorAutoProxyCreator实现Spring自动代理
     56     @Bean
     57     public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
     58         DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
     59         creator.setProxyTargetClass(true);
     60         return creator;
     61     }
     62 
     63     //权限认证信息
     64     @Bean(name = "securityManager")
     65     public DefaultWebSecurityManager defaultWebSecurityManager(MyShiroRealm realm){
     66         System.out.println("shiro~~~~~~~~启动");
     67         DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
     68         //设置realm
     69         securityManager.setRealm(realm);
     70         securityManager.setCacheManager(getEhCacheManager());
     71         return securityManager;
     72     }
     73 
     74     @Bean
     75     public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
     76         AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
     77         advisor.setSecurityManager(securityManager);
     78         return advisor;
     79     }
     80 
     81     //shiro核心拦截器
     82     @Bean(name = "shiroFilter")
     83     public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
     84         ShiroFilterFactoryBean factoryBean = new MyShiroFilterFactoryBean();
     85         factoryBean.setSecurityManager(securityManager);
     86         // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
     87         factoryBean.setLoginUrl("/login");
     88         // 登录成功后要跳转的连接
     89         factoryBean.setSuccessUrl("/welcome");
     90         factoryBean.setUnauthorizedUrl("/403");
     91         loadShiroFilterChain(factoryBean);
     92         logger.info("shiro拦截器工厂类注入成功");
     93         return factoryBean;
     94     }
     95 
     96     //加载ShiroFilter权限控制规则
     97     private void loadShiroFilterChain(ShiroFilterFactoryBean factoryBean) {
     98         /**下面这些规则配置最好配置到配置文件中*/
     99         Map<String, String> filterChainMap = new LinkedHashMap<String, String>();
    100         // authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器
    101         // anon:它对应的过滤器里面是空的,什么都没做,可以理解为不拦截
    102         //authc:所有url都必须认证通过才可以访问; anon:所有url都可以匿名访问
    103         filterChainMap.put("/permission/userInsert", "anon");
    104         filterChainMap.put("/error", "anon");
    105         filterChainMap.put("/tUser/insert","anon");
    106         filterChainMap.put("/**", "authc");
    107 
    108         factoryBean.setFilterChainDefinitionMap(filterChainMap);
    109     }
    110 
    111     /*
    112         1.LifecycleBeanPostProcessor,这是个DestructionAwareBeanPostProcessor的子类,负责org.apache.shiro.util.Initializable类型bean的生命周期的,初始化和销毁。主要是AuthorizingRealm类的子类,以及EhCacheManager类。
    113         2.HashedCredentialsMatcher,这个类是为了对密码进行编码的,防止密码在数据库里明码保存,当然在登陆认证的生活,这个类也负责对form里输入的密码进行编码。
    114         3.ShiroRealm,这是个自定义的认证类,继承自AuthorizingRealm,负责用户的认证和权限的处理,可以参考JdbcRealm的实现。
    115         4.EhCacheManager,缓存管理,用户登陆成功后,把用户信息和权限信息缓存起来,然后每次用户请求时,放入用户的session中,如果不设置这个bean,每个请求都会查询一次数据库。
    116         5.SecurityManager,权限管理,这个类组合了登陆,登出,权限,session的处理,是个比较重要的类。
    117         6.ShiroFilterFactoryBean,是个factorybean,为了生成ShiroFilter。它主要保持了三项数据,securityManager,filters,filterChainDefinitionManager。
    118         7.DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
    119         8.AuthorizationAttributeSourceAdvisor,shiro里实现的Advisor类,内部使用AopAllianceAnnotationsAuthorizingMethodInterceptor来拦截用以下注解的方法。
    120     */
    121 }

      MyShiroRealm :

    package com.sun.configuration;  
      
    import com.sun.permission.model.Role;  
    import com.sun.permission.model.User;  
    import com.sun.permission.service.PermissionService;  
    import org.apache.commons.lang3.builder.ReflectionToStringBuilder;  
    import org.apache.commons.lang3.builder.ToStringStyle;  
    import org.apache.log4j.Logger;  
    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 org.springframework.beans.factory.annotation.Autowired;  
      
    import java.util.List;  
      
    /**  
     * shiro的认证最终是交给了Realm进行执行  
     * 所以我们需要自己重新实现一个Realm,此Realm继承AuthorizingRealm  
     * Created by sun on 2017-4-2.  
     */  
    public class MyShiroRealm extends AuthorizingRealm {  
      
        private static final Logger logger = Logger.getLogger(MyShiroRealm.class);  
        @Autowired  
        private PermissionService permissionService;  
        /**  
         * 登录认证  
         */  
        @Override  
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {  
            //UsernamePasswordToken用于存放提交的登录信息  
            UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;  
            logger.info("登录认证!");  
            logger.info("验证当前Subject时获取到token为:" + ReflectionToStringBuilder.toString(token, ToStringStyle.MULTI_LINE_STYLE));  
            User user = permissionService.findByUserEmail(token.getUsername());  
            if (user != null){  
                logger.info("用户: " + user.getEmail());  
                if(user.getStatus() == 0){  
                    throw new DisabledAccountException();  
                }  
                // 若存在,将此用户存放到登录认证info中,无需自己做密码对比,Shiro会为我们进行密码对比校验  
                return new SimpleAuthenticationInfo(user.getEmail(), user.getPswd(), getName());  
            }  
            return null;  
        }  
      
        /**  
         * 权限认证(为当前登录的Subject授予角色和权限)  
         *  
         * 该方法的调用时机为需授权资源被访问时,并且每次访问需授权资源都会执行该方法中的逻辑,这表明本例中并未启用AuthorizationCache,  
         * 如果连续访问同一个URL(比如刷新),该方法不会被重复调用,Shiro有一个时间间隔(也就是cache时间,在ehcache-shiro.xml中配置),  
         * 超过这个时间间隔再刷新页面,该方法会被执行  
         *  
         * doGetAuthorizationInfo()是权限控制,  
         * 当访问到页面的时候,使用了相应的注解或者shiro标签才会执行此方法否则不会执行,  
         * 所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回null即可  
         */  
        @Override  
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {  
            String loginName = (String) super.getAvailablePrincipal(principals);  
            User user = permissionService.findByUserEmail(loginName);  
            logger.info("权限认证!");  
            if (user != null){  
                // 权限信息对象info,用来存放查出的用户的所有的角色及权限  
                SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();  
                //用户的角色集合  
                info.setRoles(permissionService.getRolesName(user.getId()));  
                List<Role> roleList = permissionService.getRoleList(user.getId());  
                for (Role role : roleList){  
                    //用户的角色对应的所有权限  
                    logger.info("角色: "+role.getName());  
                    info.addStringPermissions(permissionService.getPermissionsName(role.getId()));  
                }  
                return info;  
            }  
            // 返回null将会导致用户访问任何被拦截的请求时都会自动跳转到unauthorizedUrl指定的地址  
            return null;  
        }  
    }  

      MyShiroFilterFactoryBean:

     1 package com.sun.configuration;  
     2   
     3 import org.apache.shiro.mgt.SecurityManager;  
     4 import org.apache.shiro.spring.web.ShiroFilterFactoryBean;  
     5 import org.apache.shiro.web.filter.mgt.FilterChainManager;  
     6 import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;  
     7 import org.apache.shiro.web.mgt.WebSecurityManager;  
     8 import org.apache.shiro.web.servlet.AbstractShiroFilter;  
     9 import org.springframework.beans.factory.BeanInitializationException;  
    10   
    11 import javax.servlet.FilterChain;  
    12 import javax.servlet.ServletException;  
    13 import javax.servlet.ServletRequest;  
    14 import javax.servlet.ServletResponse;  
    15 import javax.servlet.http.HttpServletRequest;  
    16 import java.io.IOException;  
    17 import java.util.HashSet;  
    18 import java.util.Set;  
    19   
    20 /**  
    21  * Created by sun on 2017-4-2.  
    22  */  
    23 public class MyShiroFilterFactoryBean extends ShiroFilterFactoryBean {  
    24     // ShiroFilter将直接忽略的请求  
    25     private Set<String> ignoreExt;  
    26   
    27     public MyShiroFilterFactoryBean(){  
    28         super();  
    29         ignoreExt = new HashSet<String>();  
    30         ignoreExt.add(".jpg");  
    31         ignoreExt.add(".png");  
    32         ignoreExt.add(".gif");  
    33         ignoreExt.add(".bmp");  
    34         ignoreExt.add(".js");  
    35         ignoreExt.add(".css");  
    36     }  
    37     /**  
    38      * 启动时加载  
    39      */  
    40     @Override  
    41     protected AbstractShiroFilter createInstance() throws Exception {  
    42         SecurityManager securityManager = getSecurityManager();  
    43         if (securityManager == null){  
    44             throw new BeanInitializationException("SecurityManager property must be set.");  
    45         }  
    46   
    47         if (!(securityManager instanceof WebSecurityManager)){  
    48             throw new BeanInitializationException("The security manager does not implement the WebSecurityManager interface.");  
    49         }  
    50   
    51         PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();  
    52         FilterChainManager chainManager = createFilterChainManager();  
    53         chainResolver.setFilterChainManager(chainManager);  
    54         return new MySpringShiroFilter((WebSecurityManager)securityManager, chainResolver);  
    55     }  
    56   
    57     /**  
    58      * 启动时加载  
    59      */  
    60     private class MySpringShiroFilter extends AbstractShiroFilter {  
    61         public MySpringShiroFilter(  
    62                 WebSecurityManager securityManager, PathMatchingFilterChainResolver chainResolver) {  
    63             super();  
    64             if (securityManager == null){  
    65                 throw new IllegalArgumentException("WebSecurityManager property cannot be null.");  
    66             }  
    67             setSecurityManager(securityManager);  
    68             if (chainResolver != null){  
    69                 setFilterChainResolver(chainResolver);  
    70             }  
    71         }  
    72         /**  
    73          * 页面上传输的url先进入此方法验证  
    74          */  
    75         @Override  
    76         protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse,  
    77                                         FilterChain chain)  
    78                 throws ServletException, IOException {  
    79             HttpServletRequest request = (HttpServletRequest)servletRequest;  
    80             String str = request.getRequestURI().toLowerCase();  
    81             boolean flag = true;  
    82             int idx = 0;  
    83             if ((idx = str.lastIndexOf(".")) > 0){  
    84                 str = str.substring(idx);  
    85                 if (ignoreExt.contains(str.toLowerCase())){  
    86                     flag = false;  
    87                 }  
    88             }  
    89             if (flag){  
    90                 super.doFilterInternal(servletRequest, servletResponse, chain);  
    91             } else {  
    92                 chain.doFilter(servletRequest, servletResponse);  
    93             }  
    94         }  
    95     }  
    96 }  

    ehcache-shiro.xml:

     1 <?xml version="1.0" encoding="UTF-8"?>  
     2 <ehcache updateCheck="false" name="shiroCache">  
     3     <!--  
     4        name:缓存名称。  
     5        maxElementsInMemory:缓存最大数目  
     6        maxElementsOnDisk:硬盘最大缓存个数。  
     7        eternal:对象是否永久有效,一但设置了,timeout将不起作用。  
     8        overflowToDisk:是否保存到磁盘,当系统当机时  
     9        timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。  
    10        timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。  
    11        diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.  
    12        diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。  
    13        diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。  
    14        memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。  
    15         clearOnFlush:内存数量最大时是否清除。  
    16          memoryStoreEvictionPolicy:  
    17             Ehcache的三种清空策略;  
    18             FIFO,first in first out,这个是大家最熟的,先进先出。  
    19             LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。  
    20             LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。  
    21     -->  
    22     <defaultCache  
    23             maxElementsInMemory="10000"  
    24             eternal="false"  
    25             timeToIdleSeconds="120"  
    26             timeToLiveSeconds="120"  
    27             overflowToDisk="false"  
    28             diskPersistent="false"  
    29             diskExpiryThreadIntervalSeconds="120"  
    30     />  
    31     <!-- 登录记录缓存锁定10分钟 -->  
    32     <cache name="passwordRetryCache"  
    33            maxEntriesLocalHeap="2000"  
    34            eternal="false"  
    35            timeToIdleSeconds="3600"  
    36            timeToLiveSeconds="0"  
    37            overflowToDisk="false"  
    38            statistics="true">  
    39     </cache>  
    40 </ehcache>  

    登录的controller类:

      1 package com.sun.permission.controller;  
      2   
      3 import com.sun.permission.model.User;  
      4 import com.sun.permission.service.PermissionService;  
      5 import com.sun.util.CommonUtils;  
      6 import org.apache.commons.lang3.StringUtils;  
      7 import org.apache.log4j.Logger;  
      8 import org.apache.shiro.SecurityUtils;  
      9 import org.apache.shiro.authc.*;  
     10 import org.apache.shiro.session.Session;  
     11 import org.apache.shiro.subject.Subject;  
     12 import org.springframework.beans.factory.annotation.Autowired;  
     13 import org.springframework.stereotype.Controller;  
     14 import org.springframework.validation.BindingResult;  
     15 import org.springframework.web.bind.annotation.RequestMapping;  
     16 import org.springframework.web.bind.annotation.RequestMethod;  
     17 import org.springframework.web.servlet.ModelAndView;  
     18 import org.springframework.web.servlet.mvc.support.RedirectAttributes;  
     19   
     20 import javax.validation.Valid;  
     21   
     22 /**  
     23  * Created by sun on 2017-4-2.  
     24  */  
     25 @Controller  
     26 public class LoginController {  
     27     private static final Logger logger = Logger.getLogger(LoginController.class);  
     28     @Autowired  
     29     private PermissionService permissionService;  
     30   
     31     @RequestMapping(value="/login",method= RequestMethod.GET)  
     32     public ModelAndView loginForm(){  
     33         ModelAndView model = new ModelAndView();  
     34         model.addObject("user", new User());  
     35         model.setViewName("login");  
     36         return model;  
     37     }  
     38   
     39     @RequestMapping(value="/login",method=RequestMethod.POST)  
     40     public String login(@Valid User user, BindingResult bindingResult, RedirectAttributes redirectAttributes){  
     41         if(bindingResult.hasErrors()){  
     42             return "redirect:login";  
     43         }  
     44         String email = user.getEmail();  
     45         if(StringUtils.isBlank(user.getEmail()) || StringUtils.isBlank(user.getPswd())){  
     46             logger.info("用户名或密码为空! ");  
     47             redirectAttributes.addFlashAttribute("message", "用户名或密码为空!");  
     48             return "redirect:login";  
     49         }  
     50         //验证  
     51         UsernamePasswordToken token = new UsernamePasswordToken(user.getEmail(), user.getPswd());  
     52         //获取当前的Subject  
     53         Subject currentUser = SecurityUtils.getSubject();  
     54         try {  
     55             //在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查  
     56             //每个Realm都能在必要时对提交的AuthenticationTokens作出反应  
     57             //所以这一步在调用login(token)方法时,它会走到MyRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法  
     58             logger.info("对用户[" + email + "]进行登录验证..验证开始");  
     59             currentUser.login(token);  
     60             logger.info("对用户[" + email + "]进行登录验证..验证通过");  
     61         }catch(UnknownAccountException uae){  
     62             logger.info("对用户[" + email + "]进行登录验证..验证未通过,未知账户");  
     63             redirectAttributes.addFlashAttribute("message", "未知账户");  
     64         }catch(IncorrectCredentialsException ice){  
     65             logger.info("对用户[" + email + "]进行登录验证..验证未通过,错误的凭证");  
     66             redirectAttributes.addFlashAttribute("message", "密码不正确");  
     67         }catch(LockedAccountException lae){  
     68             logger.info("对用户[" + email + "]进行登录验证..验证未通过,账户已锁定");  
     69             redirectAttributes.addFlashAttribute("message", "账户已锁定");  
     70         }catch(ExcessiveAttemptsException eae){  
     71             logger.info("对用户[" + email + "]进行登录验证..验证未通过,错误次数大于5次,账户已锁定");  
     72             redirectAttributes.addFlashAttribute("message", "用户名或密码错误次数大于5次,账户已锁定");  
     73         }catch (DisabledAccountException sae){  
     74             logger.info("对用户[" + email + "]进行登录验证..验证未通过,帐号已经禁止登录");  
     75             redirectAttributes.addFlashAttribute("message", "帐号已经禁止登录");  
     76         }catch(AuthenticationException ae){  
     77             //通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景  
     78             logger.info("对用户[" + email + "]进行登录验证..验证未通过,堆栈轨迹如下");  
     79             ae.printStackTrace();  
     80             redirectAttributes.addFlashAttribute("message", "用户名或密码不正确");  
     81         }  
     82         //验证是否登录成功  
     83         if(currentUser.isAuthenticated()){  
     84             logger.info("用户[" + email + "]登录认证通过(这里可以进行一些认证通过后的一些系统参数初始化操作)");  
     85             //把当前用户放入session  
     86             Session session = currentUser.getSession();  
     87             User tUser = permissionService.findByUserEmail(email);  
     88             session.setAttribute("currentUser",tUser);  
     89             return "/welcome";  
     90         }else{  
     91             token.clear();  
     92             return "redirect:login";  
     93         }  
     94     }  
     95   
     96     @RequestMapping(value="/logout",method=RequestMethod.GET)  
     97     public String logout(RedirectAttributes redirectAttributes ){  
     98         //使用权限管理工具进行用户的退出,跳出登录,给出提示信息  
     99         SecurityUtils.getSubject().logout();  
    100         redirectAttributes.addFlashAttribute("message", "您已安全退出");  
    101         return "redirect:login";  
    102     }  
    103   
    104     @RequestMapping("/403")  
    105     public String unauthorizedRole(){  
    106         logger.info("------没有权限-------");  
    107         return "errorPermission";  
    108     }  
    109   
    110 }  
  • 相关阅读:
    理解和学习qml
    (离线)英语词典软件推荐
    Ubuntu:安装deb文件包以及deb卸载
    mac man汉化方法
    Linux中文件和目录的权限(r, w, x)
    解决mac休眠掉电的解决方法
    线程池之ThreadPool与ForkJoinPool
    程序员的知识焦虑
    回顾2018,展望2019
    NIO基础学习——缓冲区
  • 原文地址:https://www.cnblogs.com/tongxuping/p/7209611.html
Copyright © 2011-2022 走看看