大家好,我是苍何呀。
现在出去找工作,简历上不写上微服务的技术,仿佛自己跟不上时代了,面试官更是喜欢盯着微服务项目来提问。
但其实虽说微服务是主流,随着云原生架构的发展,微服务也是趋势,但很多公司特别是中小公司的中小业务,根本用不上微服务,你说一个简单的内部管理系统,非得上微服务,有点为了微服务而微服务的感觉了😂。
为了大家能有个可放在简历上「吹逼」的微服务项目,我们在单体版本的基础上又增加了微服务版本,并在接下来的一段时间内,持续分享单体应用微服务化的经验。
今天分享的是,在微服务架构中,如何做好鉴权。
微服务架构
微服务简而言之就是单个独立的服务,可以独立开发、部署和维护。而微服务架构是指的多个微服务聚合起来的系统,这个系统涵盖多个微服务,服务与服务之间的通讯、服务监控、服务熔断降级、服务注册、分布式配置、分布式事务等各种解决方案聚合而成的架构体系。
微服务架构有如下优点:
- 提高开发效率:团队可以并行开发不同的微服务,减少了开发和发布的时间。
- 增强可维护性:小而专注的代码库更易于理解和维护,降低了技术债务。
- 灵活的技术选型:不同的微服务可以根据需要使用最合适的技术栈,而无需在整个系统中保持一致。
- 持续交付和部署:微服务架构支持持续集成和持续交付(CI/CD),使得新功能和修复能够快速上线。
- 更好的故障隔离:一个微服务的故障不会影响其他微服务的正常运行,从而提高系统的可靠性。
- 按需扩展:可以独立地扩展需要高负载处理的微服务,优化资源使用和成本。
鉴权基础
鉴权顾名思义就是需要进行权限认证和授权控制,你写好的系统不希望谁都可以访问吧?你写的牛逼的接口也不希望哪个人都可以来蹭一下访问吧?那就需要认证和授权。
专业做这块的有 Spring Security 和 Shiro 这两哥们,当然还有一些其他的框架也是可以做的,但无非核心都在做两件事:
- 认证
- 授权
认证,说白了就是登录,传统 web 登录是通过用户名和密码用 Cookie+Session 的方式,这种依赖于服务器本地内存,微服务中,显然不合适。
常见的鉴权方式有以下几种:
用户名和密码
是最传统和常见的鉴权方式,用户通过输入预先设置的用户名和密码进行登录,需要注意密码的存储和传输安全,如使用加盐哈希存储和HTTPS 传输。
多因素认证(MFA)
这是一种增强安全性的方法,通过要求多种不同类型的验证因素来确认用户身份,常见的因素包括:知识因子(密码)、拥有因子(手机验证码)、生物因子(指纹、面部识别)。
OAuth(开放授权)
这是一种一种授权协议,允许第三方应用以有限的权限访问用户资源,而无需暴露用户的凭证。常用于社交登录和API访问控制。
JWT(JSON Web Token)
一种基于 JSON 的开放标准(RFC 7519),用于在各方之间传递声明。JWT包含用户信息和签名,可用于鉴权和授权。我们这次也是采用的这种方式进行的鉴权。
微服务中鉴权
微服务架构中,通常有多个独立服务组成,这些服务可能部署在不同的服务器或数据中心, 鉴权机制需要在分布式环境中有效运作,确保各个服务能安全通信,且需要有统一认证中心,我们先来看一张微服务架构图:
通常会有一个单独的微服务来做认证,也就是认证服务,对于微服务系统而言,请求一般分为 2 种:
- 通过 API 网关的请求
- 微服务内部请求
对于这两种请求,都需要进行鉴权,但方式是不一样的,
微服务中如何做认证?
微服务中的认证最多的方式是通过 JWT 令牌的方式,但 JWT 实际上是无状态的,也就是没法确定登录的用户啥时候过期,所以大部分情况下会需要结合 Redis 来设置状态。
将生成的 JWT 字符串在 Redis 上也保存一份,并设置过期时间,判断用户是否登录时,需要先去 Redis 上查看 JWT 字符串是否存在,存在的话再对 JWT 字符串做解析操作,如果能成功解析,就没问题,如果不能成功解析,就说明令牌不合法。
这是一个简单的流程图:
我的做法是,在认证服务中,检查用户名密码的正确性,正确的话就生成 JWT 字符串,同时再把数据存入到 Redis 上,然后返回 token 信息。登录请求先经过网关,网关再转发到认证服务,下面是一个具体的流程:
微服务中如何做授权?
授权是请求到达每个微服务后,需要对请求进行权限判定,看是否有权限访问,通常不会放在网关中来做。还是在微服务中自己来做。
我们说过,在微服务架构中,请求主要分为 2 种,外部请求和内部请求,下面是不同的授权思路。
外部请求
我的做法是:请求到达网关后,通过微服务的自定义请求头拦截器(可以放在公共包下面,每个服务都可以引用),配合自定义注解和 AOP,拦截请求头,获取用户和权限信息,然后进行比对,有权限则放行,没权限则抛出异常。
内部请求
对于内部的请求来说,正常是不需要鉴权的,内部请求可以直接处理。问题是如果使用了 OpenFeign,数据都是通过接口暴露出去的,不鉴权的话,又会担心从外部来的请求调用这个接口,对于这个问题,我们也可以自定义注解+AOP,然后在内部请求调用的时候,额外加一个头字段加以区分。
我采用的是自定义内部请求注解,然后 AOP 控制拦截。
/**
* 内部认证注解
*
* @author canghe
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InnerAuth
{
/**
* 是否校验用户信息
*/
boolean isUser() default false;
}
AOP 的切面控制请求是否携带有内部请求的标识:
/**
* 内部服务调用验证处理
*
* @author canghe
*/
@Aspect
@Component
public class InnerAuthAspect implements Ordered {
@Around("@annotation(innerAuth)")
public Object innerAround(ProceedingJoinPoint point, InnerAuth innerAuth) throws Throwable {
String source = ServletUtils.getRequest().getHeader(SecurityConstants.FROM_SOURCE);
// 内部请求验证
if (!StringUtils.equals(SecurityConstants.INNER, source)) {
throw new InnerAuthException("没有内部访问权限,不允许访问");
}
String userid = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USER_ID);
String username = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USERNAME);
// 用户信息验证
if (innerAuth.isUser() && (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username))) {
throw new InnerAuthException("没有设置用户信息,不允许访问 ");
}
return point.proceed();
}
因为使用的是 OpenFeign,请求通过 OpenFeign 调用也需要鉴权,所以我实现了 feign.RequestInterceptor 接口来定义一个 OpenFeign 的请求拦截器,在拦截器中,统一为 OpenFeign 请求设置请求头信息。
/**
* feign 请求拦截器
*
* @author canghe
*/
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
HttpServletRequest httpServletRequest = ServletUtils.getRequest();
if (StringUtils.isNotNull(httpServletRequest)) {
Map<String, String> headers = ServletUtils.getHeaders(httpServletRequest);
// 传递用户信息请求头,防止丢失
String userId = headers.get(SecurityConstants.DETAILS_USER_ID);
if (StringUtils.isNotEmpty(userId)) {
requestTemplate.header(SecurityConstants.DETAILS_USER_ID, userId);
}
String userKey = headers.get(SecurityConstants.USER_KEY);
if (StringUtils.isNotEmpty(userKey)) {
requestTemplate.header(SecurityConstants.USER_KEY, userKey);
}
String userName = headers.get(SecurityConstants.DETAILS_USERNAME);
if (StringUtils.isNotEmpty(userName)) {
requestTemplate.header(SecurityConstants.DETAILS_USERNAME, userName);
}
String authentication = headers.get(SecurityConstants.AUTHORIZATION_HEADER);
if (StringUtils.isNotEmpty(authentication)) {
requestTemplate.header(SecurityConstants.AUTHORIZATION_HEADER, authentication);
}
// 配置客户端IP
requestTemplate.header("X-Forwarded-For", IpUtils.getIpAddr());
}
}
总结
微服务中的鉴权是很关键的,也是系统安全和完整性的最为核心的一步,微服务架构通过其独立性、模块化和灵活性,使得开发、部署和维护大型复杂系统变得更加高效和可控。然而,这种分布式系统带来了新的安全挑战,需要在系统设计中充分考虑和实现有效的鉴权机制。
以上是今天的分享,遗忘对你有帮助!