最近刚好有小伙伴在微信上问到这个问题,松哥就来和大家聊一聊,本文主要和小伙伴们聊一聊思路,不写代码,小伙伴们可以结合松哥之前的文章,应该能够自己写出来本文的代码。当然,思路也只是我自己的一点实践经验,不一定是最完美的方案,欢迎小伙伴们在留言中一起探讨。
首先小伙伴们知道,无论我们学习 Shiro 还是 Spring Security,里边的功能无论有哪些,核心都是两个:
所以,我们在微服务中处理鉴权问题,也可以从这两个方面来考虑。
认证,说白了就是登录。传统的 Web 登录是 Cookie+Session 的方案,这种方案依赖于服务器本地内存,在微服务中,由于服务众多,这种方案显然不再合适。
可能会有小伙伴说用 Redis+SpringSession 做 Session 共享,这是个办法,但是不是最佳方案,因为这种方案的性能以及可扩展性都比较差。
所以,微服务中的认证,还是建议使用令牌的方式,可以选择 JWT 令牌,这也是目前使用较多的一种方案。但是熟悉 JWT 的小伙伴都知道,纯粹的无状态登录无法实现注销,这就很头大,所以在实际应用中,单纯的使用 JWT 是不行的,一般还是要结合 Redis 一起,将生成的 JWT 字符串在 Redis 上也保存一份,并设置过期时间,判断用户是否登录时,需要先去 Redis 上查看 JWT 字符串是否存在,存在的话再对 JWT 字符串做解析操作,如果能成功解析,就没问题,如果不能成功解析,就说明令牌不合法。
这样有状态登录+无状态登录混在一起的方式,虽然看起来有点不伦不类,但是就当下来说,这个折衷的办法算是一个可行的方案了。
其实,上面的方案,说白了,跟传统的 Cookie+Session 没什么两样,思路几乎都是完全 copy 的:传统的 Session 用 Redis 代替了;传统穿梭于服务端和浏览器之间的 jsessionId 被 JWT 字符串代替了;传统的 jsessionId 通过 Cookie 来传输,现在的 JWT 则通过开发者手动设置后通过请求头来传输;传统的 Session 可以自动续签,现在用 JWT 就是手动续签,每次请求到达服务端的时候,就去看下 Redis 上令牌的过期时间,快过期了,就重新设置一下,其他都一模一样。
这是认证方案的选择。
微服务中授权,也可以使用 Shiro 或者 Spring Security 框架来做,省事一些。考虑到微服务技术栈都是 Spring 家族的产品,所以在权限框架这块也是建议大家首选 Spring Security(如果有小伙伴对 Spring Security 还不熟悉的话,可以在微信公众号后台回复 ss,有教程)。
当然,如果觉得 Spring Security 比较复杂想自己搞的话,也是可以的。自己搞的话,也是可以借助于 Spring Security 的思路的,松哥最近的一个项目就是这样:
请求到达微服务之后,先找到当前用户的各种信息,包括当前用户所拥有的角色和权限等信息,然后存入到和当前线程绑定的 ThreadLocal 对象中。另一方面自定义权限注解和角色注解,在切面中对这些注解进行解析,检查当前用户是否具备所需要的角色/权限等。
当然,如果你使用了 Spring Security 的话,上面这个就不需要自定义注解了,直接使用 Spring Security 中自带的即可,还可以体验 Spring Security 中更多的丰富的安全功能。
那么认证和授权在哪里做?
先来说认证,认证我们可以简单分为两个步骤:
一般来说,登录我们可以单独做一个认证服务。当登录请求到达网关之后,我们将之转发到认证服务上,完成认证操作。
在认证服务上,我们就去检查用户名/密码是否 OK,用户状态是否都 OK,都没问题的话,生成 JWT 字符串,同时再把数据存入到 Redis 上,然后把 JWT 字符串返回。
如果系统有注册功能的话,注册功能也是放在这个微服务上来完成。
校验是指每一个请求到达的时候,校验用户是否已经登录。
这个当然可以和 2.1 放到一起去做,但是松哥不建议。问题在于,假如是一个创建订单的请求,这个请求原本是要经过网关转发到订单服务上的,但是,此时就得先在网关上调用 2.1 小节的服务进行登录校验,没问题再转发到订单服务上,这样做很明显很费事,也不合理。
一个比较好的办法是直接在网关上去校验请求的令牌是否合法,这个校验本身也比较容易,校验令牌是否合法,我们只需要看 Redis 上是否存在这个令牌,并且这个 JWT 令牌能够被顺利解析就行,这个操作完全可以在网关上做。
以 Gateway 网关为例,我们可以自定义全局过滤器,在全局过滤器中校验每一个请求的令牌,校验通过了,再进行请求的转发,否则就不转发。
校验通过之后,在转发到具体的微服务之后,我们可以将解析出来的用户 id 以及用户名等信息放到请求头中,然后再转发,这样到达各个具体的微服务之后,就知道这个请求是谁发来的,这人都有哪些角色/权限,方便做下一步的权限校验。
松哥的做法是定义了一个公共模块,所有的微服务都依赖这个公共模块,这个公共模块中定义了一个拦截器,会拦截下来每一个请求,从请求头中取出用户 ID,然后从 Redis 中拿到具体的用户信息,存入到 ThreadLocal 中,这样在后续的方法调用中,如果需要判断用户是否具备某一个权限,就可以通过 ThreadLocal 去获取了。
大致上就是这样一个流程。
授权没法放到网关上做,还是得在各个微服务上去完成。
微服务上的授权我们又可以将之大致上分为两类:
对于外部请求来说,就按正常的权限校验对待就行了,自定义注解亦或者使用 Spring Security 等框架都是可以的,如果是自定义注解的话,就结合 AOP 一起,定义切面自己去处理权限注解,当然,这些功能基本上每一个微服务都是需要的,所以可以将之抽取成为一个公共的模块,在不同的微服务中依赖即可。
对于内部的请求来说,正常是不需要鉴权的,内部请求可以直接处理。问题是如果使用了 OpenFeign,数据都是通过接口暴露出去的,不鉴权的话,又会担心从外部来的请求调用这个接口,对于这个问题,我们也可以自定义注解+AOP,然后在内部请求调用的时候,额外加一个头字段加以区分。