## 统一的账户管理体系 在商城的项目中,有一个统一认证的服务(tmerclub-auth),用于统一的认证登录。 ![](./img/账户体系/账户体系.png) 我们抽取了一个统一的账户体系,如果查看`tmerclub_auth` 这个数据库,里面有张表`auth_account` 这里面有几个值得注意的字段: - `sys_type`:0.普通用户系统 1.商家端 2平台端,代表不同系统的用户体系 - `user_id`:这里的用户id,是根据系统不用关联的内容也不同 - 用户系统代表的是 `tmerclub-user`数据库`user`表`user_id` 字段 - 商家端代表的是 `tmerclub_multishop`数据库`shop_user`表`shop_user_id` 字段 - 平台端代表的是 `tmerclub_platform`数据库`sys_user`表`sys_user_id` 字段 - `uid`:全平台用户唯一id - `tenant_id`:所属租户,在商家端代表`shop_id`,其他地方暂时无意义 - 因为是统一的登录平台,所以登录使用的用户名、密码、手机号、邮箱等都是存储在这个服务里面 除了账号密码登录以外,还有第三方登录,比如微信小程序、微信公众号的`open_id`,这些就存在 `tmerclub-auth`数据库`auth_social`表`biz_user_id`字段上 除了登录以外,系统还会有用户角色权限的模型,我们单独为这个模型抽取了个 `rbac`的服务。如果公司还有自己的统一登录服务,那么无需二次剥离。 ## 校验token 有几点需要注意的是: 1. 在商城的项目中,并没有使用 `jwt` 进行登录授权的流程,使用的是传统的 `token` 形式,`token` 存储在redis中 2. 由于掌握`spring security`的人员较少,而我们只要做校验token,获取token的工作,所以是自己编写的token相关代码 对于一个有网关的微服务来说,将网关服务暴露到公网,其他服务都在内网,这是一个合理的做法,然而实际上很多公司的运维人员不懂。就导致了本应该在内网的东西(因为很多人都认为在内网的服务不需要通过校验),这就导致了`nacos` 著名的漏洞(百度一下`nacos漏洞`即可找到相关资料),有很多人将`nacos` 放到公网上,而实际上数据库的账号密码、配置文件的信息,都被暴露了,黑客用工具扫描`8848`端口,找到对应的服务就开始进行攻击了...当然这个问题在2021年得以修复... 所以商城的授权校验并不是在网关进行的授权校验,而是在每个服务之间都有校验的代码。 我们查看 `AuthFilter` 这个类,可以看到 ```java // 获取前端传入的token String accessToken = req.getHeader("Authorization"); // 校验token ServerResponseEntity userInfoInTokenVoServerResponseEntity = tokenFeignClient .checkToken(accessToken); UserInfoInTokenBO userInfoInToken = userInfoInTokenVoServerResponseEntity.getData(); try { // 保存token上下文 AuthUserContext.set(userInfoInToken); chain.doFilter(req, resp); } finally { AuthUserContext.clean(); } ``` 这样不仅保证了服务器的安全,还方便后面的方法获取用户id,店铺id等参数 ```java // 可以在全局获取登录的用户id Long userId = AuthUserContext.get().getUserId(); ``` 在上面校验token的时候,使用的是`tokenFeignClient` 进行校验,从名字来看就是一个`feign` 请求,那这个请求连接的是哪个服务呢?实际上连接的是 `tmerclub-auth` 这个服务。因为统一的登录认证服务已经被抽取出来了~ 除了用户通过网关直接连接服务器以外,服务器与服务器之间也要连接,比如用户下单的时候,不仅要经过订单服务,还要经过优惠券服务查看有没有优惠券,`feign`请求默认是不会将http请求头信息转发到另外一个服务的,需要我们手动拦截转发。参考`FeignBasicAuthRequestInterceptor` ```java @Override public void apply(RequestTemplate template) { HttpServletRequest request = attributes.getRequest(); String authorization = request.getHeader("Authorization"); if (StrUtil.isNotBlank(authorization)) { template.header("Authorization", authorization); } } ``` ## 登录生成token > 当然既然需要验证,那么验证的`AccessToken`又来自哪里呢? > > 答:其实还是来自`tmerclub-auth`服务,因为这里是做统一授权认证的。 我们看下`LoginController` 这个类,有个保存token的方法 ```java tokenStore.storeAndGetVo(data); ``` 这里面一次登录,保存了3个数据到`redis`当中`token`,我们看下这三个值的作用 - 根据`AccessToken`获取用户信息的key,这个key对应的value存储了用户id、用户openid等信息,详情请看`UserInfoInTokenBO` ```java public String getAccessKey(String accessToken) { return CacheNames.ACCESS + accessToken; } ``` - 根据`RefreshToken`获取`AccessToken`的key,可以通过这个key可以获取token,然后删除旧的token,并创建一个新的token ```java public String getRefreshToAccessKey(String refreshToken) { return CacheNames.REFRESH_TO_ACCESS + refreshToken; } ``` - 根据`uid`获取存储过的token信息,可以通过这个key,获取已经创建过的`RefreshToken`和`AccessToken`,进行删除,将用户直接退出登录 ```java public String getUidToAccessKey(String approvalKey) { return CacheNames.UID_TO_ACCESS + approvalKey; } ``` ## 第三方登录 > 除了账号密码登录,我们还需要第三方登录,比如进入到微信小程序、公众号的时候就自动登录了。 我们来看那么一个流程图: ![](./img/账户体系/社交登录.png) 我们期望的是一进入系统就马上登录成功,比如通过微信打开的网页,一进去就直接登录了。这是该如何做到呢?如果了解`oauth2.0`的协议,应该清楚从`code`是可以拿到用户的信息,像`openid`。但code是有时间限制的,如果前端一开始就拿到了`code` 但是用户尚未进行注册或者绑定过账户信息,那么当用户填写完注册需要填写的资料,code已经过期了怎么办? 在商城系统中,当前端获取到code的时候,会直接传到后台服务器,后台回去微信服务器换取`openid`等信息并保存到数据库,返回一个变量(这个变量就没必要过期了),绑定账号的时候,直接查询数据库,就不会过期了,具体可以查看`SocialLoginController` ## 服务之间调用的安全问题 当有`AccessToken`进行操作的时候,会通过授权服务进行校验,可以确保安全。如果订单服务收到一条MQ消息,让订单变成确认收货,并且往商家的钱包进行增加余额的时候。MQ的消息并不会携带`token` 那该怎么办?如何确保商家服务收到的消息一定是来自我们商城的,而不是恶意消息。 一个最简单的办法就是除了网关服务,其他服务都在内网。而实际上难免运维人员会出错。 还有一个方法,就是我们参考用户的账号密码登录,每次获取到消息的时候,我们都校验一下服务器的“用户名密码”是否正确。 在`AuthFilter` 中有一个校验方法,校验`/feign`开头的请求: ```java private boolean feignRequestCheck(HttpServletRequest req) { // 不是feign请求,不用校验 if (!req.getRequestURI().startsWith(FeignInsideAuthConfig.FEIGN_INSIDE_URL_PREFIX)) { return true; } String feignInsideSecret = req.getHeader(feignInsideAuthConfig.getKey()); // 校验feign 请求携带的key 和 value是否正确 if (StrUtil.isBlank(feignInsideSecret) || !Objects.equals(feignInsideSecret,feignInsideAuthConfig.getSecret())) { return false; } // ip白名单 List ips = feignInsideAuthConfig.getIps(); // 移除无用的空ip ips.removeIf(StrUtil::isBlank); // 有ip白名单,且ip不在白名单内,校验失败 if (CollectionUtil.isNotEmpty(ips) && !ips.contains(IpHelper.getIpAddr())) { logger.error("ip not in ip White list: {}, ip, {}", ips, IpHelper.getIpAddr()); return false; } return true; } ``` 有地方要校验,那么是什么时候传进去的呢: 我们回头看下`FeignBasicAuthRequestInterceptor` ```java // feign的内部请求,往请求头放入key 和 secret进行校验 template.header(feignInsideAuthConfig.getKey(), feignInsideAuthConfig.getSecret()); ``` 这里面的key和secret可以在`nacos`的`application-xxx.yml`进行配置 ```yaml feign: inside: key: tmerclub-feign-inside-key secret: tmerclub-feign-inside-secret # ip白名单,如果有需要的话,用小写逗号分割 ips: ```