8.6 KiB
统一的账户管理体系
在商城的项目中,有一个统一认证的服务(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
有几点需要注意的是:
- 在商城的项目中,并没有使用
jwt
进行登录授权的流程,使用的是传统的token
形式,token
存储在redis中 - 由于掌握
spring security
的人员较少,而我们只要做校验token,获取token的工作,所以是自己编写的token相关代码
对于一个有网关的微服务来说,将网关服务暴露到公网,其他服务都在内网,这是一个合理的做法,然而实际上很多公司的运维人员不懂。就导致了本应该在内网的东西(因为很多人都认为在内网的服务不需要通过校验),这就导致了nacos
著名的漏洞(百度一下nacos漏洞
即可找到相关资料),有很多人将nacos
放到公网上,而实际上数据库的账号密码、配置文件的信息,都被暴露了,黑客用工具扫描8848
端口,找到对应的服务就开始进行攻击了...当然这个问题在2021年得以修复...
所以商城的授权校验并不是在网关进行的授权校验,而是在每个服务之间都有校验的代码。
我们查看 AuthFilter
这个类,可以看到
// 获取前端传入的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等参数
// 可以在全局获取登录的用户id
Long userId = AuthUserContext.get().getUserId();
在上面校验token的时候,使用的是tokenFeignClient
进行校验,从名字来看就是一个feign
请求,那这个请求连接的是哪个服务呢?实际上连接的是 tmerclub-auth
这个服务。因为统一的登录认证服务已经被抽取出来了~
除了用户通过网关直接连接服务器以外,服务器与服务器之间也要连接,比如用户下单的时候,不仅要经过订单服务,还要经过优惠券服务查看有没有优惠券,feign
请求默认是不会将http请求头信息转发到另外一个服务的,需要我们手动拦截转发。参考FeignBasicAuthRequestInterceptor
@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的方法
tokenStore.storeAndGetVo(data);
这里面一次登录,保存了3个数据到redis
当中token
,我们看下这三个值的作用
- 根据
AccessToken
获取用户信息的key,这个key对应的value存储了用户id、用户openid等信息,详情请看UserInfoInTokenBO
public String getAccessKey(String accessToken) {
return CacheNames.ACCESS + accessToken;
}
- 根据
RefreshToken
获取AccessToken
的key,可以通过这个key可以获取token,然后删除旧的token,并创建一个新的token
public String getRefreshToAccessKey(String refreshToken) {
return CacheNames.REFRESH_TO_ACCESS + refreshToken;
}
- 根据
uid
获取存储过的token信息,可以通过这个key,获取已经创建过的RefreshToken
和AccessToken
,进行删除,将用户直接退出登录
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
开头的请求:
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
// feign的内部请求,往请求头放入key 和 secret进行校验
template.header(feignInsideAuthConfig.getKey(), feignInsideAuthConfig.getSecret());
这里面的key和secret可以在nacos
的application-xxx.yml
进行配置
feign:
inside:
key: tmerclub-feign-inside-key
secret: tmerclub-feign-inside-secret
# ip白名单,如果有需要的话,用小写逗号分割
ips: