Session和Cookie
由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session。 Session是保存在服务端的,有一个唯一标识。在大型的网站,一般会有专门的Session服务器集群,这个时候 Session 信息都是放在内存的。
思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。 实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端, 需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。 有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪, 即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。
Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办? 这个信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。 这也是Cookie名称的由来,给用户的一点甜头。
所以,总结一下:
- session 在服务器端,cookie 在客户端(浏览器)
- session 默认被存在在服务器的一个文件里(不是内存)
- session 的运行依赖 session id,而 session_id是存在cookie中的,也就是说,如果浏览器禁用了cookie, 同时session也会失效(但是可以通过其它方式实现,比如在 url 中传递 session_id)
- session 可以放在 文件、数据库、或内存中都可以。
- 用户验证这种场合一般会用 session 因此,维持一个会话的核心就是客户端的唯一标识,即 session id
种常见的实现web应用会话管理的方式:
- 基于server端session的管理方式
- 基于cookie的管理方式
- 基于token的管理方式
基于Server端session
在早期web应用中,通常使用服务端session来管理用户的会话。
1)服务端session是用户第一次访问应用时,服务器就会创建的对象,代表用户的一次会话过程,可以用来存放数据。 服务器为每一个session都分配一个唯一的sessionid,以保证每个用户都有一个不同的session对象。
2)服务器在创建完session后,会把sessionid通过cookie返回给用户所在的浏览器,这样当用户第二次及以后向服务器发送请求的时候, 就会通过cookie把sessionid传回给服务器,以便服务器能够根据sessionid找到与该用户对应的session对象。
3)session通常有失效时间的设定,比如2个小时。当失效时间到,服务器会销毁之前的session,并创建新的session返回给用户。 但是只要用户在失效时间内,有发送新的请求给服务器,通常服务器都会把他对应的session的失效时间根据当前的请求时间再延长2个小时。
4)session在一开始并不具备会话管理的作用。它只有在用户登录认证成功之后,并且往sesssion对象里面放入了用户登录成功的凭证, 才能用来管理会话。管理会话的逻辑也很简单,只要拿到用户的session对象,看它里面有没有登录成功的凭证,就能判断这个用户是否已经登录。 当用户主动退出的时候,会把它的session对象里的登录凭证清掉。 所以在用户登录前或退出后或者session对象失效时,肯定都是拿不到需要的登录凭证的。
可简单使用流程图描述如下:
image主流的web开发平台都原生支持这种会话管理的方式,而且开发起来很简单。它还有一个比较大的优点就是安全性好, 因为在浏览器端与服务器端保持会话状态的媒介始终只是一个sessionid串,只要这个串够随机,攻击者就不能轻易冒充他人的sessionid进行操作; 除非通过CSRF或http劫持的方式,才有可能冒充别人进行操作;即使冒充成功,也必须被冒充的用户session里面包含有效的登录凭证才行。
但是这种方式也有几个问题需要解决:
1)这种方式将会话信息存储在web服务器里面,所以在用户同时在线量比较多时,这些会话信息会占据比较多的内存;
2)当应用采用集群部署的时候,会遇到多台web服务器之间如何做session共享的问题。因为session是由单个服务器创建的, 但是处理用户请求的服务器不一定是那个创建session的服务器,这样他就拿不到之前已经放入到session中的登录凭证之类的信息了;
3)多个应用要共享session时,除了以上问题,还会遇到跨域问题,因为不同的应用可能部署的主机不一样,需要在各个应用做好cookie跨域的处理。
针对问题1和问题2,我见过的解决方案是采用redis/memcached这种中间服务器来管理session的增删改查, 一来减轻web服务器的负担,二来解决不同web服务器共享session的问题。
针对问题3,由于服务端的session依赖cookie来传递sessionid,所以在实际项目中,只要解决各个项目里面如何实现sessionid的cookie跨域访问即可, 这个是可以实现的,就是比较麻烦,前后端有可能都要做处理。
如果在一些小型的web应用中使用,可以不用考虑上面三个问题,所以很适合这种方式。
基于cookie
由于前一种方式会增加服务器的负担和架构的复杂性,所以后来就有人想出直接把用户的登录凭证直接存到客户端的方案, 当用户登录成功之后,把登录凭证写到cookie里面,并给cookie设置有效期,后续请求直接验证存有登录凭证的cookie是否存在以及凭证是否有效, 即可判断用户的登录状态。使用它来实现会话管理的整体流程如下:
1)用户发起登录请求,服务端根据传入的用户密码之类的身份信息,验证用户是否满足登录条件,如果满足,就根据用户信息创建一个登录凭证, 这个登录凭证简单来说就是一个对象,最简单的形式可以只包含用户id,凭证创建时间和过期时间三个值。
2)服务端把上一步创建好的登录凭证,先对它做数字签名,然后再用对称加密算法做加密处理,将签名、加密后的字串,写入cookie。 cookie的名字必须固定(如ticket),因为后面再获取的时候,还得根据这个名字来获取cookie值。 这一步添加数字签名的目的是防止登录凭证里的信息被篡改,因为一旦信息被篡改,那么下一步做签名验证的时候肯定会失败。 做加密的目的,是防止cookie被别人截取的时候,无法轻易读到其中的用户信息。
3)用户登录后发起后续请求,服务端根据上一步存登录凭证的cookie名字,获取到相关的cookie值。然后先做解密处理,再做数字签名的认证, 如果这两步都失败,说明这个登录凭证非法;如果这两步成功,接着就可以拿到原始存入的登录凭证了。然后用这个凭证的过期时间和当前时间做对比, 判断凭证是否过期,如果过期,就需要用户再重新登录;如果未过期,则允许请求继续。
可简单使用流程图描述如下:
image它的缺点也比较明显:
1)cookie有大小限制,存储不了太多数据
2)每次传送cookie,增加了请求的数量,对访问性能也有影响;
3)也有跨域问题,毕竟还是要用cookie。
相比起第一种方式,基于cookie方案明显还是要好一些,目前好多web开发平台或框架都默认使用这种方式来做会话管理。
跨域的问题可以用CORS(跨域资源共享)的方式来快速解决。
基于token
前面两种会话管理方式因为都用到cookie,不适合用在移动端native app里面,native app不好管理cookie,毕竟它不是浏览器。 这两种方案都不适合用来做纯api服务的登录认证,就要考虑第三种会话管理方式,也就是token认证。
这种方式从流程和实现上来说,跟cookie-based的方式没有太多区别,只不过cookie-based里面写到cookie里面的ticket在这种方式下称为token, 这个token在返回给客户端之后,后续请求都必须通过url参数或者是http header的形式,主动带上token, 这样服务端接收到请求之后就能直接从http header或者url里面取到token进行验证:
这种方式不通过cookie进行token的传递,而是每次请求的时候,主动把token加到http header里面或者url后面, 所以即使在native app里面也能使用它来调用我们通过web发布的api接口。app里面还要做两件事情:
1)有效存储token,得保证每次调接口的时候都能从同一个位置拿到同一个token;
2)每次调接口的的代码里都得把token加到header或者接口地址里面。
可简单使用流程图描述如下:
image这种方式同样适用于网页应用,token可以存于localStorage或者sessionStorage里面,然后每发ajax请求的时候, 都把token拿出来放到ajax请求的header里即可。不过如果是非接口的请求,比如直接通过点击链接请求一个页面这种, 是无法自动带上token的。所以这种方式也仅限于走纯接口的web应用。
基于token的标准实现: JWT
现在SPA应用,前后端完全分离,基于API接口的应用越来越多,这时候基于token的认证就是最好的选择方式了。 好在这个方式的技术其实早就有很多实现了,而且还有现成的标准可用,这个标准就是JWT(json-web-token)。
JWT本身并没有做任何技术实现,它只是定义了token-based的管理方式该如何实现, 它规定了token的应该包含的标准内容以及token的生成过程和方法。目前实现了这个标准的技术已经有非常多:
官方网站:https://jwt.io/#libraries-io
Git主页:https://github.com/auth0/java-jwt
SpringSession
生产环境我们的应用示例不可能是单节点部署, 通常都是多结点部署, 结点上层会进行域映射, 实例之间负载响应请求. 比如常见的Nginx + Tomcat负载均衡场景中。常用的均衡算法有IP_Hash、轮训、根据权重、随机等。不管对于哪一种负载均衡算法,由于Nginx对不同的请求分发到某一个Tomcat,Tomcat在运行的时候分别是不同的容器里,因此会出现session不同步或者丢失的问题。
解决方案
IP_HASH
nginx可以根据客户端IP进行负载均衡,在upstream里设置ip_hash,就可以针对同一个C类地址段中的客户端选择同一个后端服务器,除非那个后端服务器宕了才会换一个. 这样如果该类QPS高会导致该台服务器的负载升高,负载不均.
通过容器插件
在容器层面扩展可共享存储的插件; 比如基于Tomcat的tomcat-redis-session-manager,基于Jetty的jetty-session-redis等等。好处是对项目来说是透明的,无需改动代码。该方案由于过于依赖容器,一旦容器升级或者更换意味着又得从新来过。并且代码不在项目中,对开发者来说维护也是个问题。
会话管理工具
自己写一套会话管理的工具类,包括Session管理和Cookie管理,在需要使用会话的时候都从自己的工具类中获取,而工具类后端存储可以放到Redis中。很显然这个方案灵活性最大,但开发需要一些额外的时间。并且系统中存在两套Session方案,很容易弄错而导致取不到数据。
开源解决方案
这里以开源框架Spring-Session为例,Spring-Session扩展了Servlet的会话管理(所有的request都会经过SessionRepositoryFilter,而 SessionRepositoryFilter是一个优先级最高的javax.servlet.Filter,它使用了一个SessionRepositoryRequestWrapper类接管了Http Session的创建和管理工作),既不依赖容器,又不需要改动代码. 可插拔, 轻量级. 支持多维度存储;诸如 Redis 、Pivotal GemFire、Jdbc、Mongo 、Hazelcast等
SpringSession应用
SpringMvc项目使用SpringSession
maven依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>1.3.1.RELEASE</version>
</dependency>
配置web.xml
<filter>
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
配置redis、以及redisHttpSession存储
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<!-- 将session放入redis -->
<bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<property name="maxInactiveIntervalInSeconds" value="1800" />
</bean>
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="100" />
<property name="maxIdle" value="10" />
</bean>
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
<property name="hostName" value="127.0.0.1"/>
<property name="port" value="6379"/>
<property name="password" value="" />
<property name="timeout" value="3000"/>
<property name="usePool" value="true"/>
<property name="poolConfig" ref="jedisPoolConfig"/>
</bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>
</beans>
测试代码
@RestController
@RequestMapping("/api/cloud")
public class ApiSessionController extends BaseMultiController {
private static final Logger LOG = LoggerFactory.getLogger(ApiSessionController.class);
@Autowired
protected HttpSession httpSession;
@GetMapping("/session/put")
public APIResult sessionPut(){
httpSession.setAttribute("cloud", JSON.toJSONString(new User("Elonsu", "123456")));
String userString = (String)httpSession.getAttribute("cloud");
LOG.info("[session][set]:" + userString);
return APIResult.success(true);
}
@GetMapping("/session/get")
public APIResult sessionGet(){
String userString = (String)httpSession.getAttribute("cloud");
LOG.info("[session][get]:" + userString);
User user = JSONObject.parseObject(userString, User.class);
return APIResult.success(user);
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class User implements Serializable {
private String username;
private String password;
}
}
测试输出
启两个容器实例,端口分别使用8080和8081进行访问
实例1访问结果
image实例2访问结果
image查看redis中存储的session
image应用SpringSession
maven依赖
<dependencies>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
</dependencies>
启动主类增加注解
启动类上增加注解@EnableRedisHttpSession
@Configuration
@SpringBootApplication
@EnableAutoConfiguration
@EnableRedisHttpSession
public class Application extends WebMvcStrap {
protected final static Logger LOG = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
应用配置文件配置
应用配置文件application.properties
增加如下配置
# spring redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
# spring session
spring.session.store-type=redis
server.session.timeout=5
网友评论