220 lines
8.6 KiB
Markdown
220 lines
8.6 KiB
Markdown
## 统一的账户管理体系
|
||
|
||
|
||
|
||
在商城的项目中,有一个统一认证的服务(tmerclub-auth),用于统一的认证登录。
|
||
|
||

|
||
|
||
|
||
|
||
我们抽取了一个统一的账户体系,如果查看`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<UserInfoInTokenBO> 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;
|
||
}
|
||
```
|
||
|
||
|
||
|
||
|
||
|
||
## 第三方登录
|
||
|
||
> 除了账号密码登录,我们还需要第三方登录,比如进入到微信小程序、公众号的时候就自动登录了。
|
||
|
||
|
||
|
||
我们来看那么一个流程图:
|
||
|
||

|
||
|
||
我们期望的是一进入系统就马上登录成功,比如通过微信打开的网页,一进去就直接登录了。这是该如何做到呢?如果了解`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<String> 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:
|
||
```
|
||
|