引自:http://www.cnblogs.com/learnhow/p/5694876.html

本篇内容大多总结自张开涛的《跟我学Shiro》原文地址:http://jinnianshilongnian.iteye.com/blog/2018936

我并没有全部看完,只是选择了一部分对我来说急需在项目中使用的知识加以学习。并且对于大多数第一次接触Shiro的同学来说,掌握这些也应该足够了。

一、架构

要学习如何使用Shiro必须先从它的架构谈起,作为一款安全框架Shiro的设计相当精妙。Shiro的应用不依赖任何容器,它也可以在JavaSE下使用。但是最常用的环境还是JavaEE。下面以用户登录为例:

(1)使用用户的登录信息创建令牌

  1. UsernamePasswordToken token = new UsernamePasswordToken(username, password);

token可以理解为用户令牌,登录的过程被抽象为Shiro验证令牌是否具有合法身份以及相关权限。

(2)执行登陆动作

  1. SecurityUtils.setSecurityManager(securityManager); // 注入SecurityManager
  2. Subject subject = SecurityUtils.getSubject(); // 获取Subject单例对象
  3. subject.login(token); // 登陆

Shiro的核心部分是SecurityManager,它负责安全认证与授权。Shiro本身已经实现了所有的细节,用户可以完全把它当做一个黑盒来使用。SecurityUtils对象,本质上就是一个工厂类似Spring中的ApplicationContext。Subject是初学者比较难于理解的对象,很多人以为它可以等同于User,其实不然。Subject中文翻译:项目,而正确的理解也恰恰如此。它是你目前所设计的需要通过Shiro保护的项目的一个抽象概念。通过令牌(token)与项目(subject)的登陆(login)关系,Shiro保证了项目整体的安全。

(3)判断用户

Shiro本身无法知道所持有令牌的用户是否合法,因为除了项目的设计人员恐怕谁都无法得知。因此Realm是整个框架中为数不多的必须由设计者自行实现的模块,当然Shiro提供了多种实现的途径,本文只介绍最常见也最重要的一种实现方式——数据库查询。

(4)两条重要的英文

我在学习Shiro的过程中遇到的第一个障碍就是这两个对象的英文名称:AuthorizationInfo,AuthenticationInfo。不用怀疑自己的眼睛,它们确实长的很像,不但长的像,就连意思都十分近似。

在解释它们前首先必须要描述一下Shiro对于安全用户的界定:和大多数操作系统一样。用户具有角色和权限两种最基本的属性。例如,我的Windows登陆名称是learnhow,它的角色是administrator,而administrator具有所有系统权限。这样learnhow自然就拥有了所有系统权限。那么其他人需要登录我的电脑怎么办,我可以开放一个guest角色,任何无法提供正确用户名与密码的未知用户都可以通过guest来登录,而系统对于guest角色开放的权限极其有限。

同理,Shiro对用户的约束也采用了这样的方式。AuthenticationInfo代表了用户的角色信息集合,AuthorizationInfo代表了角色的权限信息集合。如此一来,当设计人员对项目中的某一个url路径设置了只允许某个角色或具有某种权限才可以访问的控制约束的时候,Shiro就可以通过以上两个对象来判断。说到这里,大家可能还比较困惑。先不要着急,继续往后看就自然会明白了。

二、实现Realm

如何实现Realm是本文的重头戏,也是比较费事的部分。这里大家会接触到几个新鲜的概念:缓存机制、散列算法、加密算法。由于本文不会专门介绍这些概念,所以这里仅仅抛砖引玉的谈几点,能帮助大家更好的理解Shiro即可。

(1)缓存机制

Ehcache是很多Java项目中使用的缓存框架,Hibernate就是其中之一。它的本质就是将原本只能存储在内存中的数据通过算法保存到硬盘上,再根据需求依次取出。你可以把Ehcache理解为一个Map<String,Object>对象,通过put保存对象,再通过get取回对象。

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <ehcache name="shirocache">
  3. <diskStore path="java.io.tmpdir" />
  4.  
  5. <cache name="passwordRetryCache"
  6. maxEntriesLocalHeap="2000"
  7. eternal="false"
  8. timeToIdleSeconds="1800"
  9. timeToLiveSeconds="0"
  10. overflowToDisk="false"
  11. statistics="true">
  12. </cache>
  13. </ehcache>

以上是ehcache.xml文件的基础配置,timeToLiveSeconds为缓存的最大生存时间,timeToIdleSeconds为缓存的最大空闲时间,当eternal为false时ttl和tti才可以生效。更多配置的含义大家可以去网上查询。

(2)散列算法与加密算法

md5是本文会使用的散列算法,加密算法本文不会涉及。散列和加密本质上都是将一个Object变成一串无意义的字符串,不同点是经过散列的对象无法复原,是一个单向的过程。例如,对密码的加密通常就是使用散列算法,因此用户如果忘记密码只能通过修改而无法获取原始密码。但是对于信息的加密则是正规的加密算法,经过加密的信息是可以通过秘钥解密和还原。

(3)用户注册

请注意,虽然我们一直在谈论用户登录的安全性问题,但是说到用户登录首先就是用户注册。如何保证用户注册的信息不丢失,不泄密也是项目设计的重点。

  1. public class PasswordHelper {
  2. private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
  3. private String algorithmName = "md5";
  4. private final int hashIterations = 2;
  5.  
  6. public void encryptPassword(User user) {
  7. // User对象包含最基本的字段Username和Password
  8. user.setSalt(randomNumberGenerator.nextBytes().toHex());
  9. // 将用户的注册密码经过散列算法替换成一个不可逆的新密码保存进数据,散列过程使用了盐
  10. String newPassword = new SimpleHash(algorithmName, user.getPassword(),
  11. ByteSource.Util.bytes(user.getCredentialsSalt()), hashIterations).toHex();
  12. user.setPassword(newPassword);
  13. }
  14. }

如果你不清楚什么叫加盐可以忽略散列的过程,只要明白存储在数据库中的密码是根据户注册时填写的密码所产生的一个新字符串就可以了。经过散列后的密码替换用户注册时的密码,然后将User保存进数据库。剩下的工作就丢给UserService来处理。

那么这样就带来了一个新问题,既然散列算法是无法复原的,当用户登录的时候使用当初注册时的密码,我们又应该如何判断?答案就是需要对用户密码再次以相同的算法散列运算一次,再同数据库中保存的字符串比较。

(4)匹配

CredentialsMatcher是一个接口,功能就是用来匹配用户登录使用的令牌和数据库中保存的用户信息是否匹配。当然它的功能不仅如此。本文要介绍的是这个接口的一个实现类:HashedCredentialsMatcher

  1. public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
  2. // 声明一个缓存接口,这个接口是Shiro缓存管理的一部分,它的具体实现可以通过外部容器注入
  3. private Cache<String, AtomicInteger> passwordRetryCache;
  4.  
  5. public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
  6. passwordRetryCache = cacheManager.getCache("passwordRetryCache");
  7. }
  8.  
  9. @Override
  10. public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
  11. String username = (String) token.getPrincipal();
  12. AtomicInteger retryCount = passwordRetryCache.get(username);
  13. if (retryCount == null) {
  14. retryCount = new AtomicInteger(0);
  15. passwordRetryCache.put(username, retryCount);
  16. }
  17. // 自定义一个验证过程:当用户连续输入密码错误5次以上禁止用户登录一段时间
  18. if (retryCount.incrementAndGet() > 5) {
  19. throw new ExcessiveAttemptsException();
  20. }
  21. boolean match = super.doCredentialsMatch(token, info);
  22. if (match) {
  23. passwordRetryCache.remove(username);
  24. }
  25. return match;
  26. }
  27. }

可以看到,这个实现里设计人员仅仅是增加了一个不允许连续错误登录的判断。真正匹配的过程还是交给它的直接父类去完成。连续登录错误的判断依靠Ehcache缓存来实现。显然match返回true为匹配成功。

(5)获取用户的角色和权限信息

说了这么多才到我们的重点Realm,如果你已经理解了Shiro对于用户匹配和注册加密的全过程,真正理解Realm的实现反而比较简单。我们还得回到上文提及的两个非常类似的对象AuthorizationInfo和AuthenticationInfo。因为Realm就是提供这两个对象的地方。

  1. public class UserRealm extends AuthorizingRealm {
  2. // 用户对应的角色信息与权限信息都保存在数据库中,通过UserService获取数据
  3. private UserService userService = new UserServiceImpl();
  4.  
  5. /**
  6. * 提供用户信息返回权限信息
  7. */
  8. @Override
  9. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  10. String username = (String) principals.getPrimaryPrincipal();
  11. SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
  12. // 根据用户名查询当前用户拥有的角色
  13. Set<Role> roles = userService.findRoles(username);
  14. Set<String> roleNames = new HashSet<String>();
  15. for (Role role : roles) {
  16. roleNames.add(role.getRole());
  17. }
  18. // 将角色名称提供给info
  19. authorizationInfo.setRoles(roleNames);
  20. // 根据用户名查询当前用户权限
  21. Set<Permission> permissions = userService.findPermissions(username);
  22. Set<String> permissionNames = new HashSet<String>();
  23. for (Permission permission : permissions) {
  24. permissionNames.add(permission.getPermission());
  25. }
  26. // 将权限名称提供给info
  27. authorizationInfo.setStringPermissions(permissionNames);
  28.  
  29. return authorizationInfo;
  30. }
  31.  
  32. /**
  33. * 提供账户信息返回认证信息
  34. */
  35. @Override
  36. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  37. String username = (String) token.getPrincipal();
  38. User user = userService.findByUsername(username);
  39. if (user == null) {
  40. // 用户名不存在抛出异常
  41. throw new UnknownAccountException();
  42. }
  43. if (user.getLocked() == 0) {
  44. // 用户被管理员锁定抛出异常
  45. throw new LockedAccountException();
  46. }
  47. SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(),
  48. user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), getName());
  49. return authenticationInfo;
  50. }
  51. }

根据Shiro的设计思路,用户与角色之前的关系为多对多,角色与权限之间的关系也是多对多。在数据库中需要因此建立5张表,分别是用户表(存储用户名,密码,盐等)、角色表(角色名称,相关描述等)、权限表(权限名称,相关描述等)、用户-角色对应中间表(以用户ID和角色ID作为联合主键)、角色-权限对应中间表(以角色ID和权限ID作为联合主键)。具体dao与service的实现本文不提供。总之结论就是,Shiro需要根据用户名和密码首先判断登录的用户是否合法,然后再对合法用户授权。而这个过程就是Realm的实现过程。

(6)会话

用户的一次登录即为一次会话,Shiro也可以代替Tomcat等容器管理会话。目的是当用户停留在某个页面长时间无动作的时候,再次对任何链接的访问都会被重定向到登录页面要求重新输入用户名和密码而不需要程序员在Servlet中不停的判断Session中是否包含User对象。启用Shiro会话管理的另一个用途是可以针对不同的模块采取不同的会话处理。以淘宝为例,用户注册淘宝以后可以选择记住用户名和密码。之后再次访问就无需登陆。但是如果你要访问支付宝或购物车等链接依然需要用户确认身份。当然,Shiro也可以创建使用容器提供的Session最为实现。

三、与SpringMVC集成

有了注册模块和Realm模块的支持,下面就是如何与SpringMVC集成开发。有过框架集成经验的同学一定知道,所谓的集成基本都是一堆xml文件的配置,Shiro也不例外。

(1)配置前端过滤器

先说一个题外话,Filter是过滤器,interceptor是拦截器。前者基于回调函数实现,必须依靠容器支持。因为需要容器装配好整条FilterChain并逐个调用。后者基于代理实现,属于AOP的范畴。

如果希望在WEB环境中使用Shiro必须首先在web.xml文件中配置

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xmlns="http://java.sun.com/xml/ns/javaee"
  4. xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  5. id="WebApp_ID" version="3.0">
  6. <display-name>Shiro_Project</display-name>
  7. <welcome-file-list>
  8. <welcome-file>index.jsp</welcome-file>
  9. </welcome-file-list>
  10. <servlet>
  11. <servlet-name>SpringMVC</servlet-name>
  12. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  13. <init-param>
  14. <param-name>contextConfigLocation</param-name>
  15. <param-value>classpath:springmvc.xml</param-value>
  16. </init-param>
  17. <load-on-startup>1</load-on-startup>
  18. <async-supported>true</async-supported>
  19. </servlet>
  20. <servlet-mapping>
  21. <servlet-name>SpringMVC</servlet-name>
  22. <url-pattern>/</url-pattern>
  23. </servlet-mapping>
  24. <listener>
  25. <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  26. </listener>
  27. <listener>
  28. <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
  29. </listener>
  30. <context-param>
  31. <param-name>contextConfigLocation</param-name>
  32. <!-- 将Shiro的配置文件交给Spring监听器初始化 -->
  33. <param-value>classpath:spring.xml,classpath:spring-shiro-web.xml</param-value>
  34. </context-param>
  35. <context-param>
  36. <param-name>log4jConfigLoaction</param-name>
  37. <param-value>classpath:log4j.properties</param-value>
  38. </context-param>
  39. <!-- shiro配置 开始 -->
  40. <filter>
  41. <filter-name>shiroFilter</filter-name>
  42. <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  43. <async-supported>true</async-supported>
  44. <init-param>
  45. <param-name>targetFilterLifecycle</param-name>
  46. <param-value>true</param-value>
  47. </init-param>
  48. </filter>
  49. <filter-mapping>
  50. <filter-name>shiroFilter</filter-name>
  51. <url-pattern>/*</url-pattern>
  52. </filter-mapping>
  53. <!-- shiro配置 结束 -->
  54. </web-app>

熟悉Spring配置的同学可以重点看有绿字注释的部分,这里是使Shiro生效的关键。由于项目通过Spring管理,因此所有的配置原则上都是交给Spring。DelegatingFilterProxy的功能是通知Spring将所有的Filter交给ShiroFilter管理。

接着在classpath路径下配置spring-shiro-web.xml文件

  1. <beans xmlns="http://www.springframework.org/schema/beans"
  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
  3. xmlns:context="http://www.springframework.org/schema/context"
  4. xmlns:mvc="http://www.springframework.org/schema/mvc"
  5. xsi:schemaLocation="http://www.springframework.org/schema/beans
  6. http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
  7. http://www.springframework.org/schema/context
  8. http://www.springframework.org/schema/context/spring-context-3.1.xsd
  9. http://www.springframework.org/schema/mvc
  10. http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
  11.  
  12. <!-- 缓存管理器 使用Ehcache实现 -->
  13. <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
  14. <property name="cacheManagerConfigFile" value="classpath:ehcache.xml" />
  15. </bean>
  16.  
  17. <!-- 凭证匹配器 -->
  18. <bean id="credentialsMatcher" class="utils.RetryLimitHashedCredentialsMatcher">
  19. <constructor-arg ref="cacheManager" />
  20. <property name="hashAlgorithmName" value="md5" />
  21. <property name="hashIterations" value="2" />
  22. <property name="storedCredentialsHexEncoded" value="true" />
  23. </bean>
  24.  
  25. <!-- Realm实现 -->
  26. <bean id="userRealm" class="utils.UserRealm">
  27. <property name="credentialsMatcher" ref="credentialsMatcher" />
  28. </bean>
  29.  
  30. <!-- 安全管理器 -->
  31. <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
  32. <property name="realm" ref="userRealm" />
  33. </bean>
  34.  
  35. <!-- Shiro的Web过滤器 -->
  36. <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
  37. <property name="securityManager" ref="securityManager" />
  38. <property name="loginUrl" value="/" />
  39. <property name="unauthorizedUrl" value="/" />
  40. <property name="filterChainDefinitions">
  41. <value>
  42. /authc/admin = roles[admin]
  43. /authc/** = authc
  44. /** = anon
  45. </value>
  46. </property>
  47. </bean>
  48.  
  49. <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
  50. </beans>

需要注意filterChainDefinitions过滤器中对于路径的配置是有顺序的,当找到匹配的条目之后容器不会再继续寻找。因此带有通配符的路径要放在后面。三条配置的含义是: /authc/admin需要用户有用admin权限、/authc/**用户必须登录才能访问、/**其他所有路径任何人都可以访问。

说了这么多,大家一定关心在Spring中引入Shiro之后到底如何编写登录代码呢。

  1. @Controller
  2. public class LoginController {
  3. @Autowired
  4. private UserService userService;
  5.  
  6. @RequestMapping("login")
  7. public ModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password) {
  8. UsernamePasswordToken token = new UsernamePasswordToken(username, password);
  9. Subject subject = SecurityUtils.getSubject();
  10. try {
  11. subject.login(token);
  12. } catch (IncorrectCredentialsException ice) {
  13. // 捕获密码错误异常
  14. ModelAndView mv = new ModelAndView("error");
  15. mv.addObject("message", "password error!");
  16. return mv;
  17. } catch (UnknownAccountException uae) {
  18. // 捕获未知用户名异常
  19. ModelAndView mv = new ModelAndView("error");
  20. mv.addObject("message", "username error!");
  21. return mv;
  22. } catch (ExcessiveAttemptsException eae) {
  23. // 捕获错误登录过多的异常
  24. ModelAndView mv = new ModelAndView("error");
  25. mv.addObject("message", "times error");
  26. return mv;
  27. }
  28. User user = userService.findByUsername(username);
  29. subject.getSession().setAttribute("user", user);
  30. return new ModelAndView("success");
  31. }
  32. }

登录完成以后,当前用户信息被保存进Session。这个Session是通过Shiro管理的会话对象,要获取依然必须通过Shiro。传统的Session中不存在User对象。

  1. @Controller
  2. @RequestMapping("authc")
  3. public class AuthcController {
  4. // /authc/** = authc 任何通过表单登录的用户都可以访问
  5. @RequestMapping("anyuser")
  6. public ModelAndView anyuser() {
  7. Subject subject = SecurityUtils.getSubject();
  8. User user = (User) subject.getSession().getAttribute("user");
  9. System.out.println(user);
  10. return new ModelAndView("inner");
  11. }
  12.  
  13. // /authc/admin = user[admin] 只有具备admin角色的用户才可以访问,否则请求将被重定向至登录界面
  14. @RequestMapping("admin")
  15. public ModelAndView admin() {
  16. Subject subject = SecurityUtils.getSubject();
  17. User user = (User) subject.getSession().getAttribute("user");
  18. System.out.println(user);
  19. return new ModelAndView("inner");
  20. }
  21. }

四、总结

Shiro是一个功能很齐全的框架,使用起来也很容易,但是要想用好却有相当难度。完整项目的源码就不在这里提供了,需要交流的同学可以给我留言或直接查阅张开涛的博客。如果大家感觉我写的还可以,也希望能给我一些反馈意见。

转:30分钟学会如何使用Shiro的更多相关文章

  1. 30分钟学会如何使用Shiro

    本篇内容大多总结自张开涛的<跟我学Shiro>原文地址:http://jinnianshilongnian.iteye.com/blog/2018936 我并没有全部看完,只是选择了一部分 ...

  2. 30分钟学会如何使用Shiro(转自:http://www.cnblogs.com/learnhow/p/5694876.html)

    本篇内容大多总结自张开涛的<跟我学Shiro>原文地址:http://jinnianshilongnian.iteye.com/blog/2018936 我并没有全部看完,只是选择了一部分 ...

  3. 30分钟学会如何使用Shiro(转)

    本文转自http://www.cnblogs.com/learnhow/p/5694876.html 感谢作者 本篇内容大多总结自张开涛的<跟我学Shiro>原文地址:http://jin ...

  4. 30分钟学会如何使用Shiro(转)

    本篇内容大多总结自张开涛的<跟我学Shiro>原文地址:http://jinnianshilongnian.iteye.com/blog/2018936 我并没有全部看完,只是选择了一部分 ...

  5. 转:30分钟了解Springboot整合Shiro

    引自:30分钟了解Springboot整合Shiro 前言:06年7月的某日,不才创作了一篇题为<30分钟学会如何使用Shiro>的文章.不在意之间居然斩获了22万的阅读量,许多人因此加了 ...

  6. 【grunt第二弹】30分钟学会使用grunt打包前端代码(02)

    前言 上一篇博客,我们简单的介绍了grunt的使用,一些基础点没能覆盖,我们今天有必要看看一些基础知识 [grunt第一弹]30分钟学会使用grunt打包前端代码 配置任务/grunt.initCon ...

  7. 30分钟学会使用Spring Web Services基础开发

    时隔一年终于又推出了一篇30分钟系列,上一篇<30分钟学会反向Ajax>是2016年7月的事情了.时光荏苒,岁月穿梭.虽然一直还在从事Java方面的开发工作,但是私下其实更喜欢使用C++. ...

  8. 30 分钟学会 Flex 布局

    30 分钟学会 Flex 布局 有酒   617 人赞同了该文章 为什么我要写这一篇关于 Flex 布局的教程? 因为它十分简单灵活,区区简单几行代码就可以实现各种页面的的布局,以前我在学习页面布局的 ...

  9. 30分钟学会XAML

    1.狂妄的WPF 相对传统的Windows图形编程,需要做很多复杂的工作,引用许多不同的API.例如:WinForm(带控件表单).GDI+(2D图形).DirectX API(3D图形)以及流媒体和 ...

随机推荐

  1. TE7下的创建组件AxHost失败

    问题比较诡异.时好时坏的.网上的办法试过了,没用. 最后的解决办法是,把项目属性切换到Any CPU,然后勾选 32位优先,切换到界面设计状态,拖放控件,OK:运行,会出现红色提示:再次切换到项目属性 ...

  2. KNN 与 K - Means 算法比较

    KNN K-Means 1.分类算法 聚类算法 2.监督学习 非监督学习 3.数据类型:喂给它的数据集是带label的数据,已经是完全正确的数据 喂给它的数据集是无label的数据,是杂乱无章的,经过 ...

  3. 任务九:使用HTML/CSS实现一个复杂页面

    任务目的 通过实现一个较为复杂的页面,加深对于HTML,CSS的实战能力 实践代码的复用.优化 任务描述 通过HTML及CSS实现设计稿 设计稿PSD文件(点击下载),效果如 效果图(点击打开) 整个 ...

  4. 如何一次性下载某个类库依赖的所有jar包"

    ** 经常碰到这种事情: ** 在一些非maven工程中(由于某种原因这种工程还是手工添加依赖的),需要用到某个新的类库(假设这个类库发布在maven库中),而这个类库又间接依赖很多其他类库,如果依赖 ...

  5. git revert .vs. git reset .vs. git rebase

    1. git rervert的工作方式是:将一个老的commit的改动完全找出来,并且在新的tip处运行反操作,最终清除老commit的改动: git revert的应用场景多在对public rep ...

  6. 【Leetcode】【Medium】Linked List Cycle II

    Given a linked list, return the node where the cycle begins. If there is no cycle, return null. 解题: ...

  7. 【转】Linux中/etc/profile,/etc/bashrc,~/.profile,~/.bashrcd的区别

    //因为在原文章中博主说以下内容是网友说的,所以我就只加个转了,找不到原作者 /etc/profile,/etc/bashrc 是系统全局环境变量设定 ~/.profile,~/.bashrc用户家目 ...

  8. PLSQL使用绑定变量

    想对一个sql做10046trace,结果因为10g数据库无法对sql_id做,只能使用绑定变量的方法,下面sql是如何使用绑定变量运行sql的语句 declare  v_sql  VARCHAR2( ...

  9. htm5 手机自适应问题 文本框被激活(获取焦点)时,页面会放大至原来尺寸。

    加上这句话就ok啦 <meta name="viewport" content="width=device-width, initial-scale=1.0, mi ...

  10. TIA Portal 和 scout 之间的驱动器地址分配

    TIA Portal集成了scout.在使用simotion控制器时,分配驱动装置的地址可能会碰到问题. 解决方法: 1)在配置驱动时,TIA Portal软件的语言需要选择为应为中文 2)unico ...