15.4 Spring控制反转容器的使用

本节主要介绍**Spring如何管理bean依赖关系**。

15.4.1 通过构造器创建一个bean实例

前面已经介绍,通过调用ApplicationContextgetBean方法可以获取到一个bean的实例。下面的配置文件中定义了一个名为productbean

一个简单的配置文件

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 通过默认无参的构造器来除注入Product类 -->
<bean name="product" class="app15a.bean.Product"/>
</beans>

bean的定义告诉Spring通过默认无参的构造器来初始化Product。如果不存在该构造器(因为类作者重载了构造器,且没有显式定义默认构造器),则Spring将抛出一个异常。
注意**,应采用id或者name属性标识一个bean**。为了让Spring创建一个Product实例,应将bean定义的name值“product”(具体实践中也可以是id值)和Product类型作为参数传递给ApplicationContextgetBean方法:

1
2
3
4
5
6
ApplicationContext context =
new ClassPathXmlApplicationContext(
new String[] {"spring-config.xml"});
Product product1 = context.getBean("product", Product.class);
product1.setName("Excellent snake oil");
System.out.println("product1: " + product1.getName());

15.4.2 通过工厂方法创建一个bean实例

除了通过类的构造器方式,**Spring还同样支持通过调用一个工厂的方法来初始化类**。下面的bean定义展示了通过工厂方法来实例化java.util.Calendar

1
2
3
<!-- 使用id属性来标识一个bean -->
<bean id="calendar" class="java.util.Calendar"
factory-method="getInstance"/>

本例中采用了id属性,而非name属性来标识bean,并采用了getBean方法来获取Calendar实例:

1
2
3
4
5
6
7
// 加载定义bean的xml文件
ApplicationContext context =
new ClassPathXmlApplicationContext(
new String[] {"spring-config.xml"});
//获取实例
Calendar calendar =
context.getBean("calendar", Calendar.class);

15.4.3 DestroyMethod的使用

有时,我们希望一些类在被销毁前能执行一些方法。Spring考虑到了这样的需求。可以在bean定义中配置destroy-method属性,来指定在销毁前要被执行的方法
下面的例子中,我们配置Spring通过java.util.concurrent.Executors的静态方法newCachedThreadPool来创建一个java.uitl.concurrent.ExecutorService实例,并指定了destroy-method属性值为shutdown方法。这样,Spring会在销毁ExecutorService实例前调用其shutdown方法:

1
2
3
<bean id="executorService" class="java.util.concurrent.Executors"
factory-method="newCachedThreadPool"
destroy-method="shutdown"/>

15.4.4 向构造器传递参数

Spring支持通过带参数的构造器来初始化类。

Product类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package app15a.bean;
import java.io.Serializable;
public class Product
implements
Serializable
{
private static final long serialVersionUID = 748392348L;
private String name;
private String description;
private float price;
public Product()
{}
public Product(String name,String description,float price)
{
this.name = name;
this.description = description;
this.price = price;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public String getDescription()
{
return description;
}
public void setDescription(String description)
{
this.description = description;
}
public float getPrice()
{
return price;
}
public void setPrice(float price)
{
this.price = price;
}
}

如下定义展示了如何通过参数名传递参数:

1
2
3
4
5
6
7
8
9
10
11
<!-- 调用:public Product(String name,String description,float price) -->
<bean name="featuredProduct" class="app15a.bean.Product">
<!-- 构造器使用构造器注入 -->
<!-- 给构造器的name参数赋值 -->
<constructor-arg name="name" value="终极橄榄油" />
<!-- 给构造器的description参数赋值 -->
<constructor-arg name="description"
value="市场上最纯的橄榄油" />
<!-- 给构造器的price参数赋值 -->
<constructor-arg name="price" value="9.95" />
</bean>

这样,在创建Product实例时,Spring会调用如下构造器:

1
2
3
4
5
public Product(String name, String description, float price) {
this.name = name;
this.description = description;
this.price = price;
}

除了通过名称传递参数外,Spring还支持通过下标方式传递参数,具体如下:

1
2
3
4
5
6
7
<bean name="featuredProduct2" class="app15a.bean.Product">
<constructor-arg index="0"
value="终极橄榄油" />
<constructor-arg index="1"
value="市场上最纯的橄榄油" />
<constructor-arg index="2" value="9.95" />
</bean>

需要说明的是,采用这种下标方式,对应构造器的所有参数必须传递,缺一不可。

15.4.5 setter方式依赖注入

下面以Employee类和Address类为例,介绍setter方式依赖注入。

Employee类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package app15a.bean;
public class Employee
{
private String firstName;
private String lastName;
private Address homeAddress;
public Employee()
{}
public Employee(String firstName,String lastName,Address homeAddress)
{
this.firstName = firstName;
this.lastName = lastName;
this.homeAddress = homeAddress;
}
@Override
public String toString()
{
return firstName + lastName + "," + homeAddress;
}
public String getFirstName()
{
return firstName;
}
public void setFirstName(String firstName)
{
this.firstName = firstName;
}
public String getLastName()
{
return lastName;
}
public void setLastName(String lastName)
{
this.lastName = lastName;
}
public Address getHomeAddress()
{
return homeAddress;
}
public void setHomeAddress(Address homeAddress)
{
this.homeAddress = homeAddress;
}
}

Address类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package app15a.bean;
public class Address
{
// 国家
private String country;
// 城市
private String city;
// 街道
private String state;
// 街道编号
private String stateId;
// 邮编
private String zipCode;
@Override
public String toString()
{
return country + "," + city + "," + state + "," + stateId + ","
+ zipCode;
}
public Address(String country,String city,String state,String stateId,
String zipCode)
{
this.country = country;
this.city = city;
this.state = state;
this.stateId = stateId;
this.zipCode = zipCode;
}
//setter和getter方法省略...
}

Employee依赖于Address类,可以通过如下配置来保证每个Employee实例都能包含Address实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 使用构造器方式注入依赖 -->
<bean name="simpleAddress" class="app15a.bean.Address">
<constructor-arg name="country" value="中国" />
<constructor-arg name="city" value="北京" />
<constructor-arg name="state" value="雾霾大道" />
<constructor-arg name="stateId"
value="996" />
<constructor-arg name="zipCode" value="1314" />
</bean>
<!-- 使用setter方法来给bean注入值 -->
<bean name="employee1" class="app15a.bean.Employee">
<property name="firstName" value="张" />
<property name="lastName" value="三" />
<!-- 引用另一个bean -->
<property name="homeAddress" ref="simpleAddress" />
</bean>

simpleAddress对象是Address类的一个实例,其通过构造器方式实例化。employee1对象则通过配置property元素来调用setter方法以设置值。需要注意的是,**homeAddress属性配置的是simpleAddress对象的引用**。
被引用对象A的配置定义不需要在引用该对象A的对象B之前定义。本例中,employee1对象可以出现在simpleAddress对象定义之前。

15.4.6 构造器方式依赖注入

上面的Employee类提供了一个可以传递参数的构造器,如下所示:

1
2
3
4
5
6
7
//可以通过构造器注入
public Employee(String firstName, String lastName,
Address homeAddress) {
this.firstName = firstName;
this.lastName = lastName;
this.homeAddress = homeAddress;
}

所以,我们还可以将Address对象通过构造器注入,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 使用构造器方法注入依赖 -->
<bean name="employee2" class="app15a.bean.Employee">
<constructor-arg name="firstName" value="李" />
<constructor-arg name="lastName" value="四" />
<!-- 引用另一个beans -->
<constructor-arg name="homeAddress"
ref="simpleAddress" />
</bean>
<!-- 使用构造器方式注入依赖 -->
<bean name="simpleAddress" class="app15a.bean.Address">
<constructor-arg name="country" value="中国" />
<constructor-arg name="city" value="北京" />
<constructor-arg name="state" value="雾霾大道" />
<constructor-arg name="stateId"
value="996" />
<constructor-arg name="zipCode" value="1314" />
</bean>

完整代码

项目结构

spring-config.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 定义一个name为product,类型为app15a.bean.Product的bean -->
<bean name="product" class="app15a.bean.Product" />
<bean name="featuredProduct" class="app15a.bean.Product">
<!-- public Product(String name,String description,float price) -->
<!-- 构造器使用构造器注入 -->
<!-- 给构造器的name参数赋值 -->
<constructor-arg name="name" value="终极橄榄油" />
<!-- 给构造器的description参数赋值 -->
<constructor-arg name="description"
value="市场上最纯的橄榄油" />
<!-- 给构造器的price参数赋值 -->
<constructor-arg name="price" value="9.95" />
</bean>
<bean name="featuredProduct2" class="app15a.bean.Product">
<constructor-arg index="0"
value="终极橄榄油" />
<constructor-arg index="1"
value="市场上最纯的橄榄油" />
<constructor-arg index="2" value="9.95" />
</bean>
<!-- 使用工厂方法来生成bean -->
<bean id="calendar" class="java.util.Calendar"
factory-method="getInstance" />
<!-- 使用setter方法来给bean注入值 -->
<bean name="employee1" class="app15a.bean.Employee">
<property name="firstName" value="张" />
<property name="lastName" value="三" />
<!-- 引用另一个bean -->
<property name="homeAddress" ref="simpleAddress" />
</bean>
<!-- 使用构造器方法注入依赖 -->
<bean name="employee2" class="app15a.bean.Employee">
<constructor-arg name="firstName" value="李" />
<constructor-arg name="lastName" value="四" />
<!-- 引用另一个beans -->
<constructor-arg name="homeAddress"
ref="simpleAddress" />
</bean>
<!-- 使用构造器方式注入依赖 -->
<bean name="simpleAddress" class="app15a.bean.Address">
<constructor-arg name="country" value="中国" />
<constructor-arg name="city" value="北京" />
<constructor-arg name="state" value="雾霾大道" />
<constructor-arg name="stateId"
value="996" />
<constructor-arg name="zipCode" value="1314" />
</bean>
</beans>

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package app15a;
import java.util.Calendar;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import app15a.bean.Employee;
import app15a.bean.Product;
public class Main
{
public static void main(String[] args)
{
@SuppressWarnings(
"resource"
)
// 从类的加载路径中,加载xml文件之中的beans
ApplicationContext context = new ClassPathXmlApplicationContext(
new String[]
{"spring-config.xml"});
// 获取xml中定义的的bean
// 将获得spring-config.xml中定义的bean:
// <bean name="product" class="app15a.bean.Product" />
Product product1 = context.getBean("product", Product.class);
// 调用这个对象的setName方法
product1.setName("真是个棒棒的产品");
System.out.println("product1: " + product1.getName());
// 还是获取name为product的bean,
// 这和第一次获取到的bean是同一bean,因为name属性一样
// 所以这里可以不用设置值就可以输出
Product product2 = context.getBean("product", Product.class);
// 因为是同一bean,所以打印内容一样
System.out.println("product2: " + product2.getName());
// 获取name属性为:featuredProduct的bean:
Product featuredProduct = context.getBean("featuredProduct",
Product.class);
// 因为已经在xml文件之中通过构造器注入值了,这里可以直接取出
System.out.println("featuredProduct: " + featuredProduct.getName()
+ ", " + featuredProduct.getDescription() + ", "
+ featuredProduct.getPrice());
// 获取id为calendar的bean
Calendar calendar = context.getBean("calendar",
java.util.Calendar.class);
// 调用其方法
System.out.println("calendar: " + calendar.getTime());
// 获取id为employee1的bean
Employee employee1 = context.getBean("employee1", Employee.class);
System.out.print(
"employee1: " + employee1.getFirstName() + employee1.getLastName());
System.out.println(",地址:" + employee1.getHomeAddress());
// 获取id或者name为employee2的bean
Employee employee2 = context.getBean("employee2", Employee.class);
System.out.print(
"employee2: " + employee2.getFirstName() + employee2.getLastName());
System.out.println(",地址:" + employee2.getHomeAddress());
}
}

15.3 XML配置文件

从1.0版本开始,Spring就支持基于XML的配置,从2.5版本开始,增加了通过注解的配置支持。

使用XML文件配置beans

下面介绍如何使用XML文件配置beansbeansxml配置文件的根元素通常为:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
...
</beans>

如果需要更强的Spring配置能力,可以在schemalocation属性中添加相应的schema

配置文件可以是一份,也可以分解为多份,以支持模块化配置。ApplicationContext的实现类支持读取多份配置文件。另一种选择是,通过一份主配置文件,将该文件导入到其他配置文件。

beans的xml文件导入其他被子beans的xml文件

下面是一个beans的配置文件导入其他beans的配置文件的示例:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<import resource="config1.xml"/>
<import resource="module2/config2.xml"/>
<import resource="/resources/config3.xml"/>
...
</beans>

bean元素的配置后面将会详细介绍。

15.2 依赖注入

在过去数年间,依赖注入技术作为代码可测试性的一个解决方案已经被广泛应用。实际上,Spring、谷歌Guice等伟大框架都采用了依赖注入技术。那么,什么是依赖注入技术?

什么是依赖注入

很多人在使用中并不区分依赖注入和控制反转IoC),尽管Martin Fowler在其文章中已分析了二者的不同。
http://martinfowler.com/articles/injection.html

实例

简单来说,依赖注入的情况如下。
有两个组件ABA依赖于B。假定A是一个类,且A有一个方法importantMethod使用到了B,如下:

1
2
3
4
5
6
7
8
public class A {
public void importantMethod() {
B b = ... // get an instance of B
b.usefulMethod();
...
}
...
}

要使用**B类,A类必须先获得组件B的实例的引用**。若B是一个具体类,则可通过new关键字直接创建组件B实例。但是,如果B是接口,且有多个实现,则问题就变得复杂了。我们固然可以任意选择接口B的一个实现类,但这也意味着A的可重用性大大降低了,因为无法采用B的其他实现。
依赖注入是这样处理此类情景的:接管对象的创建工作,并将该对象的引用注入需要该对象的组件
以上述例子为例,
依赖注入框架会分别创建对象A和对象B,将对象B注入到对象A

依赖注入方式

**为了能让框架进行依赖注入,程序员需要编写特定的set方法或者构造方法**。

setter方法注入

例如,为了能将B注入到A中,类A会被修改成如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class A {
private B b;
public void importantMethod() {
// no need to worry about creating B anymore
// B b = ... // get an instance of B
b.usefulMethod();
...
}
//提供setter方法
public void setB(B b) {
this.b = b;
}
}

修改后的类A新增了一个setter方法,setter方法将会被框架调用,用以注入一个B的实例。由于对象依赖由依赖注入,类AimportantMethod方法不再需要在调用BusefulMethod方法前去创建一个B的实例。

构造器注入

当然,也可以采用构造器方式注入,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class A {
private B b;
//提供构造器
public A(B b) {
this.b = b;
}
public void importantMethod() {
// no need to worry about creating B anymore
// B b = ... // get an instance of B
b.usefulMethod();
...
}
}

本例中,**Spring会先创建B的实例,再创建实例A,然后把B注入到实例A
注意
Spring管理的对象称为beans
通过提供一个
控制反转容器**(或者依赖注入容器),Spring为我们提供一种可以“聪明”地管理Java对象依赖关系的方法。其优雅之处在于,程序员无须了解Spring框架的存在,更不需要引入任何Spring类型。

版本支持

  • 从1.0版本开始,**Spring就同时支持setter构造器方式的依赖注入**。
  • 从2.5版本开始,通过Autowired注解,Spring支持基于field方式的依赖注入,但缺点是程序必须引入org.springframework.beans.factory.annotation.Autowired,这对Spring产生了依赖,这样,程序无法直接迁移到另一个依赖注入容器内。

使用Spring,程序几乎将所有重要对象的创建工作移交给Spring,并配置如何注入依赖

Spring支持XML和注解两种配置方式。此外,还需要创建一个ApplicationContext对象,ApplicationContext对象代表一个Spring控制反转容器,

ApplicationContext接口

org.springframework.context.ApplicationContext接口有多个实现,包括ClassPathXmlApplicationContextFileSystemXmlApplicationContext。这两个实现都需要至少一个包含beans信息的XML文件ClassPathXmlApplicationContext尝试在类加载路径中加载配置文件,而FileSystemXmlApplicationContext则从文件系统中加载。
下面为从类路径中加载config1.xmlconfig2.xmlApplicationContext创建的一个代码示例:

1
2
3
ApplicationContext context =
new ClassPathXmlApplicationContext(
new String[] {"config1.xml", "config2.xml"});

可以通过调用ApplicationContextgetBean方法获得对象:

1
Product product = context.getBean("product", Product.class);

getBean方法会查询idproduct且类型为Productbean对象

理想情况下,我们仅需在测试代码中创建一个ApplicationContext,应用程序本身无须处理。对于Spring MVC应用,可以通过一个Spring Servlet来处理ApplicationContext,而无须直接处理。

15.1 Spring入门

Spring模块都打包成JAR文件,其命名格式如下:

1
spring-maluleName-x.y.z.RELEASE.jar

其中module name是模块的名字,而x.y.zspring的版本号。例如:Spring4.1.12版本中的beans模块的包全名为:spring-beans-4.1.12.RELEASE.jar
推荐采用MavenGradle工具来下载Spring模块,具体操作步骤可以参见Spring官网:
http://projects.spring.io/spring-framework
采用类似Maven以及Gradle这样的工具有一个好处,即下载一个Spring模块时会自动下载其所依赖的模块。
如果不熟悉以上两种工具,则可以通过如下链接下载包括所有模块的压缩文件:
http://repo.spring.io/release/org/springframework/spring/

Spring MVC maven依赖

以下是Spring MVC用到的依赖,把下面的代码复制到pom.xml的<dependencies></dependencies>标签下即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!-- Spring MVC依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>

第15章 Spring框架

Spring框架是一个开源的企业应用开发框架,作为一个轻量级的解决方案,其包含20多个不同的模块。本书主要关注CoreBean,以及Spring MVC模块SpringMVCSpring的一个子框架,也是本书的主题。
本章介绍了**CoreBean两个模块,以及基于它们之上的依赖注入方案**。为了方便初学者,本章也会详细讨论依赖注入的概念。你将会在后续章节中应用本章所学习的技能来配置Spring MVC应用。

12.3.5 SSL是怎么工作的

HTTPS协议

从浏览器地址栏中可看到协议:例如https://www.amazon.cn/,有的浏览器也会在地址栏显示一个安全的图标(一个绿色的锁),如下图所示:
这里有一张图片

HTTP协议是明文传输的,并且不能验证对方的身份,而且不能保证数据的完整性。而当我们在网络上进行购物电子交易时,电子网银转账时,这种方式就显得很不安全了。如果黑客截取了我们和服务器端的通信数据,那么黑客就能获取我们的一些敏感信息了。所以,HTTPS应运而生!
HTTPS是在HTTP协议的基础上加入了SSL协议,**SSL协议加在了传输层和应用层之间**,如下图所示:
这里有一张图片
在加入了SSL协议后,HTTPS相比HTTP更加的安全,其对数据的传输进行了加密处理,并且能验证通信双方的身份,还保证了数据传输过程的完整性。这样,即使黑客截取了我们和服务器之间的通信数据,它也获取不到任何有用的信息。
需要说明的是,**SSL协议是独立于 HTTP 的协议**,所以不光是 HTTP 协议,其他运行在应用层的 SMTPTelnet 等协议均可配合 SSL 协议使用。可以说 SSL 是当今世界上应用最为广泛的网络安全技术。

申请SSL数字证书

SSL证书是需要向第三方CA机构申请的,现在大部分的SSL证书都是要付费申请的。

申请SSL证书流程

我们拿amazon.com为例:

  • 亚马逊首先生成一对公私钥,私钥自己保存。公钥用于申请SSL证书
  • 然后将公钥和amazon.com的一些身份信息发送给第三方可信任CA机构.
  • CA机构收到亚马逊的申请之后,将执行一些必要的步骤,以确信请求确实由亚马逊发送而来,并且这些信息是正确的。
  • 然后,认证中心亚马逊发来的公钥和亚马逊的身份信息进行数字签名,生成数字证书,然后发送给亚马逊。
  • 数字证书里面包含了亚马逊的公钥、亚马逊的**身份信息 **和 第三方CA机构的数字签名

SSL协议工作过程

当买家进入一个亚马逊的网站时,他的浏览器亚马逊的服务器在后台发生了如下事件:

  • 浏览器访问 https://www.amazon.cn/
  • 服务器将网站的证书信息发送给客户端
  • 浏览器通过证书上第三方可信任机构CA的签名,向该机构验证证书的真假
    • 如果该证书是真的,
      • 要求服务器证明是该证书的拥有者
  • 服务器向浏览器发送消息和使用服务器私钥加密的消息的摘要
  • 浏览器使用证书中服务器的公钥解密消息的摘要,
    • 浏览器生成另一份消息的摘要,比较两个摘要,
      • 如果两个摘要相同,则说明该服务器确实是该证书的拥有者,
        • 则浏览器生成会话秘钥对,把会话公钥作为消息,然后使用服务器公钥加密该消息,然后发送给服务器。
  • 服务器使用服务器的私钥解密该消息,就得到了浏览器会话的公钥
  • 最后,浏览器和服务器双方都拥有对方的公钥,相互通信时,只需要:
    • 使用对方的公钥加密要向对方发送的消息,
    • 使用自己的私钥解密从对方接收到的消息。

过程如下图所示:
这里有一张图片

参考资料

SSL协议的工作过程

12.3 安全套接层

Secure Socket Layer,是Netscape研发的用以保障在Internet上安全传输数据的技术,利用数据加密(Encryption)技术,可确保数据在网络上之传输过程中不会被截取及窃听。充分理解SSL是如何工作的,有很多你需要学习技术,从加密到私钥和公钥对,再到证书。本节讨论详细SSL及其组件。

12.3.1密码学

我们时不时需要一个安全信息通道,使得信息是安全的,即便外部可以访问信息,也不能篡改信息。
从历史上看,密码只关心加密和解密,在双方可以放心的交换信息,只有他们可以读取消息。在开始的时候,人们使用对称密码加密和解密消息。**对称密码使用相同的密钥来加密和解密消息**。这是一个非常简单的加密/解密技术。

对称加密例子

假设,加密方法是:循环前移字母表中每个字符n位数。

  • 因此,如果密码是2,加密版“ThisFriday”是“vjkuhtkfca”。当你到了字母表结尾的单词,循环前移两位将得到字母表开头的单词,因此y变成a
  • 相应的,如果知道密钥是2,则对密文循环右移2位即可解密消息。

对称加密不适合互联网

对称加密要求双方提前知道用于加密/解密的密钥。对称加密是不适合互联网的原因如下:

  • 两人交换消息往往不知道对方。例如,在亚马逊网站上购买一本书,你需要发送您的个人资料和信用卡信息。如果对称密码被使用,你必须调用亚马逊的交易之前必须同意这个密钥。
  • 每个人都希望能够与其他各方沟通。如果使用对称密码,每个人在不同的地方都会有不同的独特的钥匙
  • 信息在互联网上通过许多不同的计算机传播。这样很容易挖掘其他人的消息。对称密码体制并不能保证数据不被第三方篡改

因此,今天的安全通信在互联网上使用非对称加密,提供了这三个特点:

  • 加密/解密信息对第三方进行加密隐藏。只有预期的接收者才能解密
  • 身份验证。验证确保实体就是声称者。
  • 数据的完整性。许多计算机在互联网上发送的消息传递。它必须是确保发送的数据不变,完好无损。

公钥私钥

在非对称加密中,加密和解密的数据是通过使用一对非对称密钥(公钥和私钥)来实现的

  • 私钥是私有的。颁发者必须保持私钥放在一个安全的地方,不能落入任何另一方的手里。
  • 公钥用于加密,通常谁都可以下载公钥与颁发者进行沟通。

您可以使用工具来生成公钥和私钥。这些工具将在本章后面讨论。

公钥加密的优点

公钥加密的优点是:使用公共密钥加密的数据只能使用对应的私钥进行解密,同样的,使用私钥加密的数据只能使用对应的公钥解密
这优雅的**算法是基于大素数**,由Ron RivestAdi Shamir,和Len Adleman在麻省理工学院(MIT)在1977年发明的。它们简称为RSA算法,基于他们的姓氏的首字母。

RSA算法是在互联网上的被广泛使用,特别是电子商务领域。
爱丽丝(Alice)与鲍伯(Bob)是广泛地代入密码学和物理学领域的通用角色。这里我也会使用他们。

12.2.2 客户端证书认证

客户端证书认证也称为client-cert认证,客户端证书身份通过HTTPSHTTP通过SSL)认证,要求每个客户有一个客户端证书。这是一个非常强大的身份验证机制,但不适合在互联网上部署的应用程序,因为要求每个用户自己的数字证书是不切实际的。然而,这种身份验证方法可以用来访问组织内部的应用

12.2.1 基于表单的认证

基本访问认证和摘要访问认证不允许你使用一个定制的登录表单。如果你必须有一个自定义窗体,那么你可以使用基于表单的认证。由于基于表单的认证会发送明文,你应当与SSL配合使用

基于表单的身份认证要求

基于表单的身份验证,您需要创建一个登录页面和一个错误的页面,这可以是HTMLJSP页面。

基于表单的认证流程

第一次请求受保护的资源,servletJSP容器将显示登录页面。

  • 在成功登录时,所请求的资源将被发送。
  • 如果登录失败,用户会看到错误页。

部署描述符要求

使用form-based authentication(基于表单的身份验证),您的部署描述符的auth-method 元素的值必须是FORM(大写)。此外,login-config元素必须有form-login-config元素节点,该节点有两个子元素,form-login-pageform-error-page。如下是一个基于表单的身份验证登录 login-config元素的示例:

1
2
3
4
5
6
7
<login-config>
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>/login.html</form-login-page>
<form-error-page>/error.html</form-error-page>
</form-login-config>
</login-config>

实例

app12d的部署描述符,基于表单身份验证的例子。

项目结构

这里有一张图片

部署描述符web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<security-constraint>
<web-resource-collection>
<web-resource-name>JSP pages</web-resource-name>
<url-pattern>*.jsp</url-pattern>
</web-resource-collection>
<!-- 不允许认识角色访问这些受保护的资源 -->
<auth-constraint />
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Servlet1</web-resource-name>
<url-pattern>/servlet1</url-pattern>
</web-resource-collection>
<auth-constraint>
<!-- 允许下面两个角色访问该资源 -->
<role-name>member</role-name>
<role-name>manager</role-name>
</auth-constraint>
</security-constraint>
<!-- 验证设置 -->
<login-config>
<!-- 设置验证方法为表单验证 -->
<auth-method>FORM</auth-method>
<form-login-config>
<!-- 指定登录页面 -->
<form-login-page>/login.html</form-login-page>
<!-- 指定登录失败页面 -->
<form-error-page>/error.html</form-error-page>
</form-login-config>
</login-config>
</web-app>

form-login-page元素使用下面的login.html页面,formerror-page元素使用下面的Error.html

login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE HTML>
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Login Form</h1>
<form action='j_security_check' method='post'>
<table>
<tr>
<td align="right">User Name:</td>
<td><input name='j_username' /></td>
</tr>
<tr>
<td align="right">Password:</td>
<td><input type='password' name='j_password' /></td>
</tr>
</table>
<input type='submit' value='Login' />
</form>
</body>
</html>

error.html

1
2
3
4
5
6
7
8
<!DOCTYPE HTML>
<html>
<head>
<title>Login error</title>
</head>
<body>Login failed.
</body>
</html>

Servlet1.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package servlet;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.HttpConstraint;
import javax.servlet.annotation.ServletSecurity;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(
urlPatterns =
{"/servlet1"}
)
@ServletSecurity(
@HttpConstraint(
rolesAllowed = "manager"
)
)
public class Servlet1 extends HttpServlet
{
private static final long serialVersionUID = -11420L;
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException,IOException
{
RequestDispatcher dispatcher = request
.getRequestDispatcher("/jsp/1.jsp");
dispatcher.forward(request, response);
}
}

1.jsp

1
2
3
4
5
6
7
8
9
<!DOCTYPE HTML>
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h2>Hello World</h2>
</body>
</html>

运行效果

测序下app12d基于表单的认证,直接浏览器这个URL
http://localhost:8080/app12d/servlet1
login.html的用户登录页面如图下图所示:
这里有一张图片
输入正确的用户名tom,密码tom,将会显示所请求的servlet1页面,如下图所示:
这里有一张图片
关闭浏览器,重新访问上述链接,输入错误的用户名123,密码123,将会显示错误页面,如下图所示:
这里有一张图片
这里有一张图片