一、什么是SpringSecurity?

SpringSecurity是Spring家族的成员之一,SpringSecurity基于Spring框架,提供了一整套Web应用安全性的解决方案。

一般情况来说,Web应用的安全性包括两部分,分别是:用户认证(Authentication)和用户授权(Authorization),这两点也是SpringSecurity的重要核心功能。

  • 用户认证(Authentication):通俗的讲,就是验证用户是否可以成功登陆系统。
  • 用户授权(Authorization):通俗的讲,就是验证用户是否具有访问某些资源的权限。

二、SpringSecurity的一些基本概念

2.1、用户认证(Authentication)

--什么是认证?
        进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝等,下边拿微信来举例子说明认证相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码登录微信的过程就是认证。

--为什么需要认证
        认证的作用是为了保护系统的隐私数据与资源的安全性,用户的身份合法才可访问该系统的资源,比如您现在购买完商品付钱的时候,“钱”属于自己隐私就需要认证。

2.2.什么是授权(Authorization)

就拿支付宝来举例子,支付宝登陆成功后用户就可以使用转账、发红包、花呗、添加好友等功能,没有绑定银行卡是不可以转账的,花呗属于支付宝的资源,你需要向支付宝申请此功能才能去使用,当你申请成功才能拥有花呗功能,这个根据用户使用资源就是授权。

--为什么需要授权?
        认证是保证了用户的合法性,而授权是为了细粒度的控制用户使用的“资源”,控制不同的用户使用不同的资源。

2.3. SpringSecurity的授权模型

授权模型RBAC(Role-Based Access Control:基于角色的访问控制),基于角色来进行访问权限控制。

三、SpringSecurity和Shiro的区别

Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之 前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直 是 Shiro 的天下。

相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。

自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方 案,可以使用更少的配置来使用 Spring Security。

SpringSecurity Shiro
组织 Spring成员之一,可以和Spring框架无缝整合 Apache旗下的权限控制框架
通用性
  • 旧版不能脱离Web环境使用。
  • 新版本对整个框架进行了分层抽取,分成了核心模块和Web模块。单独引入核心模块就可以脱离Web环境使用。
不局限于Web环境,可以脱离Web环境使用。
重量级框架 轻量级框架
整合方案 SpringBoot/SpringCloud SSM

Ps:关于整合方案,只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。

四、Hello SpringSecurity

4.1、创建一个web项目,并且集成SpringSecurity

4.2、添加一个配置类

package com.example.demo01.configuration;import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin() //表单登陆.and().authorizeRequests() //认证配置.anyRequest() //任何请求.authenticated(); //都需要身份验证}
}

4.3、添加一个测试方法

package com.example.demo01;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@SpringBootApplication
@RequestMapping("/test")
@RestController
public class Demo01Application {public static void main(String[] args) {SpringApplication.run(Demo01Application.class, args);}@GetMapping("/hello")public String helloWorld(){return "Hello World !";}
}

4.4、测试

项目运行起来之后,控制台会生成一个密码,我们用这个密码登陆,用户名默认是user

浏览器访问http://localhost:8080/test/hello,会自动跳转到登陆页面进行登陆

登陆成功后(用户通过认证),可以正常请求接口地址

五、用户认证(Authentication)

SpringSecurity设置用户名和密码,提供了两种常用的配置方式,分别是:

  • 第一种:通过配置文件
  • 第二种:自定义编写实现类

5.1、通过配置文件

在application.yml中进行配置

spring:security:user:# 设置用户名name: qin# 设置密码password: 123123

5.2、通过自定义编写实现类

当什么都没有配置的时候,账号和密码是由SpringSecurity自动生成的。而在实际的项目开发中,账号和密码都是从数据库中查询出来的。所以,我们可以通过自定义逻辑控制认证。

如果需要自定义逻辑时,只需要实现UserDetailsService接口即可。

package com.example.demo01.configuration;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin() //表单登陆.and().authorizeRequests() //认证配置.anyRequest() //任何请求.authenticated(); //都需要身份验证}@BeanBCryptPasswordEncoder password(){return new BCryptPasswordEncoder();}
}

定义MyUserDetailsService类,通过@Service注解,按照名称将SecurityConfig类中的UserDetailsService对象的名称注入。

package com.example.demo01.configuration;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;import java.util.List;@Service
public class MyUserDetailsService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");return new User("qin", new BCryptPasswordEncoder().encode("123123"), auths);}
}

实际开发中,我们一般会通过数据库来认证用户信息,大概实现思路是在自定义UserDetailsService类中的loadUserByUsername方法中根据用户名去数据库中查询用户信息,如果满足要求,则用户通过认证,如果不满足要求,则抛出一个UsernameNotFoundException异常,该异常时SpringSecurity内部定义的用于抛出用户不存在的异常。

六、用户授权(Authorization)

6.1、基于权限进行访问控制

  • hasAuthority方法:如果当前的主体具有指定的权限,则返回true,否则返回false。
  • hasAnyAuthority方法:如果当前的主体有任何提供的权限的话,则返回true,否则返回false。

Ps:如果返回false,则页面提示http状态码为403,表示请求被拒绝

在SecurityConfig配置类中设置访问资源的权限的逻辑

package com.example.demo01.configuration;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin() //表单登陆.and().authorizeRequests() //认证配置//设置哪些路径可以访问,,不需要认证.antMatchers("/", "/test/hello").permitAll()//当前登录的用户,只有具有admin权限,才可以访问这个路径
//                .antMatchers("/test/index").hasAuthority("admin")//当前登录的用户,只有具有admin和manager任一权限,就可以访问这个路径.antMatchers("/test/index").hasAnyAuthority("admin","manager").anyRequest() //任何请求.authenticated(); //都需要身份验证}@BeanBCryptPasswordEncoder password(){return new BCryptPasswordEncoder();}}

在MyUserDetailsService类中添加授权的逻辑(用户被授予的权限)

package com.example.demo01.configuration;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;import java.util.List;@Service
public class MyUserDetailsService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//设置的authorityString必须和SecurityConfig中设置的hasAuthority字符串一致//当前用户具有admin的权限List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");return new User("qin", new BCryptPasswordEncoder().encode("123123"), auths);}
}

6.2、基于角色进行访问控制

  • hasRole方法:如果用户具有指定角色,则返回true,否则false。
  • hasAnyRole方法:如果用户具有指定的任意角色,则返回true,否则返回false。

Ps:如果返回false,则页面提示http状态码为403,表示请求被拒绝

在SecurityConfig配置类中设置访问资源的角色的逻辑

package com.example.demo01.configuration;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin() //表单登陆.and().authorizeRequests() //认证配置//设置哪些路径可以访问,,不需要认证.antMatchers("/", "/test/hello").permitAll()//当前登录的用户,只有具有admin权限,才可以访问这个路径
//                .antMatchers("/test/index").hasAuthority("admin")//当前登录的用户,只要具有admin和manager任一权限,就可以访问这个路径
//                .antMatchers("/test/index").hasAnyAuthority("admin","manager")//当前登录的用户,只有具有了sale的角色,才可以访问这个路径
//                .antMatchers("/test/index").hasRole("sale")//当前登录的用户,只要具有sale和common任一角色,就可以访问这个路径.antMatchers("/test/index").hasAnyRole("sale", "common").anyRequest() //任何请求.authenticated(); //都需要身份验证}@BeanBCryptPasswordEncoder password(){return new BCryptPasswordEncoder();}}

在MyUserDetailsService类中添加授权的逻辑(用户被授予的角色)

package com.example.demo01.configuration;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;import java.util.List;@Service
public class MyUserDetailsService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//设置的authorityString必须和SecurityConfig中设置的hasAuthority字符串一致//当前用户具有admin的权限//当前用户具有sale的角色,角色必须加ROLE_的前缀List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");return new User("qin", new BCryptPasswordEncoder().encode("123123"), auths);}
}

七、自定义页面

7.1、自定义403页面

在resources/static目录下定义一个访问被拒的页面unauth.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<h3>访问被拒,您没有权限浏览该页面</h3>
</body>
</html>

在SecurityConfig配置文件中进行配置

package com.example.demo01.configuration;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {//配置没有权限访问时跳转的自定义页面http.exceptionHandling().accessDeniedPage("/unauth.html");...}@BeanBCryptPasswordEncoder password(){return new BCryptPasswordEncoder();}}

效果:

7.2、自定义登陆页面

在resources/static目录下定义一个访问被拒的页面login.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><form action="/user/login" method="post"><!-- 这里务必注意,用户名和密码对应的name属性值必须是username和password-->用户名:<input type="text" name="username"><br/>密码:<input type="password" name="password"><br/><input type="submit" value="登录"></form>
</body>
</html>

在SecurityConfig配置文件中进行配置

package com.example.demo01.configuration;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {//配置没有权限访问时跳转的自定义页面http.exceptionHandling().accessDeniedPage("/unauth.html");http.formLogin() //表单登陆.loginPage("/login.html") //登录页面设置.loginProcessingUrl("/user/login") //登录访问路径.defaultSuccessUrl("/test/index").permitAll() //登录成功之后跳转路径.and()...}@BeanBCryptPasswordEncoder password(){return new BCryptPasswordEncoder();}}

7.3、用户注销

在resources/static目录下定义一个访问被拒的页面success.html,当用户登陆成功后进入到这个页面,该页面提供一个用户注销的按钮。

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<h3>登录成功</h3>
<br/>
<a href="/logout">退出登录</a>
</body>
</html>

在SecurityConfig配置文件中进行配置

package com.example.demo01.configuration;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {//退出登录http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();...http.formLogin() //表单登陆.loginPage("/login.html") //登录页面设置.loginProcessingUrl("/user/login") //登录访问路径
//                .defaultSuccessUrl("/test/index").permitAll() //登录成功之后跳转路径.defaultSuccessUrl("/success.html").permitAll() //登录成功之后跳转成功页面,配合退出登录.and()...}@BeanBCryptPasswordEncoder password(){return new BCryptPasswordEncoder();}}

测试时,直接浏览器访问登陆页面http://localhost:8080/login.html,输入正确的用户名和密码进行登陆,登录成功后跳转到success.html页面,此时打开新的页面输入http://localhost:8080/test/index发现是可以正常访问内容的,但是在success.html点击退出登录,此时再次访问刚才test/index接口,系统则提示重新登陆。

八、扩展 :SpringSecurity的三个重要过滤器

Spring Security本质是一个过滤器链,有三个重要过滤器,分别是:

  • FilterSecurityInterceptor
  • ExceptionTranslationFilter
  • UsernamePasswordAuthenticationFilter

 --FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部。

    public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());} else {if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);}InterceptorStatusToken token = super.beforeInvocation(filterInvocation);try {filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());} finally {super.finallyInvocation(token);}super.afterInvocation(token, (Object)null);}}

--ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常。

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {try {chain.doFilter(request, response);} catch (IOException var7) {throw var7;} catch (Exception var8) {Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var8);RuntimeException securityException = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);if (securityException == null) {securityException = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);}if (securityException == null) {this.rethrow(var8);}if (response.isCommitted()) {throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var8);}this.handleSpringSecurityException(request, response, chain, (RuntimeException)securityException);}}

--UsernamePasswordAuthenticationFilter:对/login 的 POST 请求做拦截,校验表单中用户,密码。

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {if (this.postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());} else {String username = this.obtainUsername(request);username = username != null ? username : "";username = username.trim();String password = this.obtainPassword(request);password = password != null ? password : "";UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);this.setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}}

九、扩展:PasswordEncoder接口:用来加密数据

BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,是对bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10。

SpringBoot第 15 讲:SpringSecurity相关推荐

  1. Java项目:在线淘房系统(租房、购房)(java+SpringBoot+Redis+MySQL+Vue+SpringSecurity+JWT+ElasticSearch+WebSocket)

    源码获取:博客首页 "资源" 里下载! 该系统有三个角色,分别是:普通用户.房屋中介.管理员.普通用户的功能:浏览房屋信息.预约看房.和中介聊天.申请成为中介等等.房屋中介的功能: ...

  2. 斯坦福NLP名课带学详解 | CS224n 第15讲 - NLP文本生成任务(NLP通关指南·完结)

    作者:韩信子@ShowMeAI,路遥@ShowMeAI,奇异果@ShowMeAI 教程地址:https://www.showmeai.tech/tutorials/36 本文地址:https://ww ...

  3. 概率论与数理统计 浙江大学 第9-15讲单元测验

    单元测验 期末考试 扩展测验(不计分) 第1-8讲单元测验 期末考试-2019冬 模拟测验 第9-15讲单元测验 测验1 第16-26讲单元测验 测验2 第27-34讲单元测验 测验3 第35-37讲 ...

  4. 【独家直播】 德哥PG系列课程15讲—PostgreSQL 多场景 沙箱实验从入门到精通

    简介 标签:PostgreSQL , 沙箱 , 实验 背景:为了能够让用户可以更快的上手PostgreSQL,德哥与云栖团队的小伙伴制作了一系列阿里云RDS PostgreSQL沙箱实验.所有实验内容 ...

  5. SpringBoot第 5 讲:SpringBoot+properties配置文件读取

    一.创建Maven项目 参考:SpringBoot第 1 讲:HelloWorld_秦毅翔的专栏-CSDN博客 二.修改pom.xml pom.xml中只需要添加springboot依赖即可 < ...

  6. STM32初学者补足基础知识(第15讲-软件如何硬件)

    STM32初学者补足基础知识(第15讲-软件如何硬件) 一.第15讲-软件如何硬件 来自 一.第15讲-软件如何硬件

  7. 小啊呜产品读书笔记001:《邱岳的产品手记-08》第15讲 产品案例分析:Mimo与LearnPython的导学之趣 第16讲 在内部产品中找到产品经理的价值

    小啊呜产品读书笔记001:<邱岳的产品手记-08>第15讲 产品案例分析:Mimo与LearnPython的导学之趣 & 第16讲 在内部产品中找到产品经理的价值 一.今日阅读计划 ...

  8. 阿里巴巴云原生实践 15 讲

    阿里巴巴从 2011 年开始通过容器实践云原生技术体系,在整个业界都还没有任何范例可供参考的大背境下,从最初独自摸索到拥抱开源回馈社区,阿里巴巴逐渐摸索出了一套比肩全球一线技术公司并且服务于整个阿里集 ...

  9. springBoot 2.0.3 + SpringSecurity 5.0.6 + thymeleaf + boostrap 權限管理案例

    1. 工於成其實,必先搭建springboot工程,配置我們pom.xml 所需的jar依賴 <!-- thymeleaf 模板依赖 --> <dependency> < ...

最新文章

  1. silverlight学习布局之:布局stackpanel
  2. 电磁悬浮控制系统仿真设计
  3. java double 小数点后两位小数_Java中double类型的数据精确到小数点后两位
  4. jetty NoSuchFieldError: MAX_INACTIVE_MINUTES
  5. php 设置excel格式,php 操作excel文件的方法小结
  6. java dos 菜单栏_学习java之电脑的常用快捷键和DOS窗口下的常用命令
  7. 2021-07-06-Intellij IDEA新建项目时JDK以及模块语言等级(language level)默认为1.8或1.5,每次创建新项目都需要重新更改
  8. 鸿蒙(HarmonyOS)刷机指南
  9. 10.卷2(进程间通信)---Posix信号量
  10. Deconvolutional Networks论文解读
  11. java case 语句_Java switch case 语句
  12. Android之Canvas撕衣服
  13. js与朴php订单评价功能
  14. 网络socket编程--多路复用
  15. 用Python绘制条形图
  16. 【模拟·习题】[USACO18JAN]Lifeguards S
  17. Creo 5.0软件安装教程|兼容WIN10
  18. android 模拟器截屏 保存到模拟器,天天模拟器屏幕截图保存在哪里?天天模拟器图库位置介绍...
  19. OpenCV4 Viz模块使用学习(二)
  20. OpenCV Java入门五 结合摄像头识脸和拍照

热门文章

  1. 集结多种便捷功能,华为云会议让沟通简单化
  2. Centos7 / RedHat7中安装openldap
  3. python爬虫知识点三--解析豆瓣top250数据
  4. python -----------梯形
  5. mysql事务数 每秒_TPS(每秒处理事务数)和TPMC(每分钟处理交易量)
  6. ❤️❤️❤️前端成神之路必看学习资源(一),建议收藏起来,偷偷学习!!!❤️❤️❤️
  7. 70招助你提升用户转化率(下)
  8. 设计模式(三):生成器模式
  9. linux内核电源关机流程,linux开机与关机步骤
  10. DragonBones 骨骼动画引擎