1. 基于传统Session方式 (HttpSession+Spring MVC HandlerInterceptor+本地缓存)
- 用户在前台填写表单,通过HTTP POST的方式将账号密码传送给服务器,服务器验证密码和数据库中的密码是否匹配
- 如果匹配,创建1个HttpSession对象,将用户信息存入session,Session会将sessionId以Cookie的形式传送给客户端,之后客户端的每次请求都会在Cookie中传送sessionId
基于session的方式,对于所有的方法,系统都需要验证用户登录状态。如果用户未登录,不允许调用后台@Controller中方法。针对这个需求,我们需要在执行每个方法前验证用户是否登录,最笨的方法是:@Controller中的每个方法在被调用前,都执行验证登录逻辑,但我们肯定不会使用这种方法。还有2种方法可选
- 使用Aop,将验证登录逻辑封装成切面,再使用Aop注入进来,让每个方法执行前都先调用验证逻辑。需要为每个方法设置1个Aop拦截规则
- 使用Spring MVC的拦截器HandlerInterceptor,拦截所有需要验证的Http请求,如果验证成功,才可以继续操作;否则直接返回未登录
我们选择的是Spring MVC拦截器这种方案,因为Aop本质上还是在每个方法执行前,调用验证方法,只不过Aop为我们简化了代码开发
[Spring MVC处理请求流程]
基于传统Session的方式有1个缺点
Session占用Web容器的内存空间,如果Session过多,会使Web容器内存溢出
Session的默认生存时间是30min,如果登录用户的个数太多,Session会占用大量的内存空间,且必须等到30min后超时才会被删除。针对这种情况,我们不使用setAttribute()将用户信息放入Session对象中,而是放在本地缓存。我们没有采用手动编写ConcurrentHashMap的方式来实现缓存,而是使用Google Guava类库中的缓存类LoadingCache
- ConcurrentHashMap只能手动移除缓存,而LoadingCache可以在缓存大小超过指定大小或缓存超时时自动被删除
- ConcurrentHashMap和LoadingCache都是线程安全的,但是LoadingCache能够提供更加细粒度的操作,可以设置同时访问缓存的线程个数
- LoadingCache提供了1个回调函数,能够在缓存被移除时发送通知
2. 基于Token方式 (Token+Redis+Spring Cloud Zuul 过滤器ZuulFilter)
在单体应用中,所有的业务模块都部署在1个进程中,可以使用Session或缓存来存储用户信息,它们都保存在Tomcat内存中,被所有业务模块共享
在微服务系统中,系统按照功能拆分成不同的应用,每个应用都是单独运行的进程。完成1次业务请求需要访问多个子应用,不可能像传统session模式那样,在Web容器中保存用户信息用于后续请求,因为它们属于不同的进程,部署在不同的物理机上
这里我们有2个需求:
- 用户在系统的子应用A进行账户密码验证后,再去访问系统中的其他子应用时,不需要再次验证,类似于单点登录SSO
- 和单体应用一样,对所有请求做统一验证,而不是所有方法执行前都执行一次验证逻辑。在传统Seesion模式中,因为应用是单体应用,使用Spring MVC的HandlerInterceptor对所有请求进行拦截,然后做统一的验证。但是在微服务系统中,应用单独部署,无法使用HandlerInterceptor做统一验证,我们使用的是Spring Cloud的组件Zuul
Zuul是Spring Cloud微服务系统的网关,所有请求都需要先经过Zuul,由Zuul进行路由。我们的身份验证功能就是使用Zuul提供的过滤器ZuulFilter实现的,这样所有访问微服务系统的请求,只需要验证1次账户密码
ZuulFilter分为4类
(1)PRE
(2)ROUTING
(3)POST
(4)ERROR
客户端请求经过的顺序是PRE-ROUTING-POST,ROUTING负责路由,将请求路由到微服务系统的对应子系统;PRE在ROUTING路由前生效,主要用来实现身份验证,我们就是采用这种方式;POST用来处理微服务子系统返回的响应,POST执行完会将响应返回给客户端,一般用于为HTTP响应添加额外信息,收集统计信息等;ERROR在发送错误时被触发
于是这种模式下的流程是:
- 用户1st访问,cookie中无token,请求被引导到验证系统;根据账户密码进行验证,如果通过验证,生成token,以token为key,用户信息为value存入Redis;将token存入cookie传送给客户端
- 当用户再次访问时,发送带有token的cookie,查询Redis,如果存在,验证通过;否则会被引导到登录页面进行验证
3. 基于JWT方式
基于Token+Zuul+Redis的方式已经可以满足微服务系统的验证需求了,但是还可以改进。使用JWT来替代Token和Redis可以做到更加轻量
JWT是Json Web Token,将用户信息加密到Token中,服务器不再保存用户信息,这样我们就可以不再使用Redis保存用户信息,使得系统更加轻量
JWT分为3部分,头部header、载荷数据payload和签证signature,这3部分组成1个token,用.连接
header中设置JWT使用的加密算法
payload存放有效信息,分为标准数据和自定义数据,标准数据包括JWT签发者和接受者、过期时间等,自定义数据一般存放用户信息
signature是签证,使用BASE64算法对header和payload进行加密,使用.连接后,再使用header中设置的加密方式对结果和加盐字符串进行组合加密
base64UrlEncode(header) + "." + base64UrlEncode(payload)+your-256-bit-secret
- 生成JWT
使用jjwt包下Jwts类的builder()获得JwtBuilder对象,通过setClaims()设置JWT的payload,通过setExpiration()设置过期时间,通过signWith()设置signature和加盐加密用的盐,来生成JWT
String token = Jwts.builder().setClaims(claims).setExpiration(expiresDate)
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
- 解析JWT
使用Jwts类的parser()获得JwtParser对象,设置加盐加密用到的盐,解析JWT
Jws<Claims> jws = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
String signature = jws.getSignature();
Map<String, String> header = jws.getHeader();
Claims claims = jws.getBody();
加盐加密
加密是指使用已经公开的、不可逆的Hash加密算法对密码进行加密,这会出现2个问题:
- 对于相同的密码,加密得到的密文是一样的。如果存储密码的数据库泄漏,黑客可以破解具有相同密码的用户
- 如果密码设置的比较简单,破解起来也会比较容易
因此,设置1个盐值SALT,加密时,将密码和盐值一起进行Hash,提高了破解难度
注册
- 用户在前台填写表单,表单验证通过后,使用MD5对密码进行加盐加密
之后将注册信息存储到DB,不过记录为未激活状态false - 生成1个UUID与当前注册用户绑定,通过Spring Boot Mail组件异步的将UUID发送到用户邮箱,用于激活。以UUID为key,DB记录id为value作为缓存,放入Guava实现的内存缓存中
- 用户点击邮箱中的链接,链接将调用后台对应@Controller方法,这个方法会去缓存中取UUID对应的DB记录id,将该记录设置为已激活状态
登出
默认情况cookie生命周期为浏览器会话时间,浏览器关闭,cookie,这种cookie保存在内存中。如果设置cookie生存时间,cookie保存在硬盘中,即使关闭浏览器,cookie也不会消失
默认情况下,session存活时间为30min,用户手动登出时,使用HttpSession.invalidate()将session销毁
鉴权
目的:
- 验证用户是否登录,如果用户知道页面url,在没有登录的情况下,不能请求后台@Controller
- 每个用户都有自己的角色,每个角色可以看到的页面,数据范围,前台页面的按钮是不一样的。这方面使用的是联通自己做的权限系统,角色,角色可见的页面,数据范围和按钮以人工的方式配置在DB中,鉴权的时候,根据用户id查询,该用户权限的相关信息
对于所有的前台请求,都需要做鉴权处理,如果将鉴权逻辑写到各个@Controller方法中,所有方法中都存在冗余的鉴权逻辑
为了防止鉴权代码冗余,使用Spring MVC的拦截器,通过自定义HandlerInterceptor的实现类完成鉴权。当HTTP请求到达Spring MVC的DispatcherServlet后,Spring MVC会根据请求中的uri,使用HandlerMapping找到匹配的Handler,返回1个HandlerExecutionChain对象,这个对象包括1个Handler对象和若干个拦截器对象HandlerInterceptor,Spring MVC会先执行拦截器,再执行Handler,因此所有的请求在使用Handler处理前,都会先由HandlerInterceptor处理
HandlerInterceptor接口有3个方法,preHandle()、postHandle()和afterCompletion()
(1)请求到达Handler前,先执行拦截器的前置处理方法preHandle(),若该方法返回false,请求直接返回给客户端,返回true才会向下传递
(2)执行完Handler方法,请求会按调用链的反方向返回,执行拦截器的后置处理方法postHandle()
网友评论