tmerclub-doc/账户和鉴权.md
2025-03-19 15:04:57 +08:00

220 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 统一的账户管理体系
在商城的项目中,有一个统一认证的服务(mall4cloud-auth),用于统一的认证登录。
![](./img/账户体系/账户体系.png)
我们抽取了一个统一的账户体系,如果查看`mall4cloud_auth` 这个数据库,里面有张表`auth_account` 这里面有几个值得注意的字段:
- `sys_type`0.普通用户系统 1.商家端 2平台端代表不同系统的用户体系
- `user_id`这里的用户id是根据系统不用关联的内容也不同
- 用户系统代表的是 `mall4cloud-user`数据库`user``user_id` 字段
- 商家端代表的是 `mall4cloud_multishop`数据库`shop_user``shop_user_id` 字段
- 平台端代表的是 `mall4cloud_platform`数据库`sys_user``sys_user_id` 字段
- `uid`全平台用户唯一id
- `tenant_id`:所属租户,在商家端代表`shop_id`,其他地方暂时无意义
- 因为是统一的登录平台,所以登录使用的用户名、密码、手机号、邮箱等都是存储在这个服务里面
除了账号密码登录以外,还有第三方登录,比如微信小程序、微信公众号的`open_id`,这些就存在 `mall4cloud-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` 请求,那这个请求连接的是哪个服务呢?实际上连接的是 `mall4cloud-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`又来自哪里呢?
>
> 答:其实还是来自`mall4cloud-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<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: mall4cloud-feign-inside-key
secret: mall4cloud-feign-inside-secret
# ip白名单如果有需要的话用小写逗号分割
ips:
```