由于本人更偏向于后端开发,所以本项目的前端部分在此省略,涉及到的一些坑会在后续指出。
目录
一、技术栈
- 前端:Vue、ElementUI、Axios
- 后端:SpringCloud(Nacos、Seata、Feign)、SpringSecurity(Oauth2.0、Jwt)、Mybatis
- 第三方服务:腾讯云SMS、腾讯云COS、163邮箱、支付宝沙箱
- 中间件:MySQL、Redis、RabbitMQ
- 网关:Nginx、Gateway
二、技术描述
- 后端使用SpringCloud搭建,划分为网关、用户、旅游、认证、消息五大服务,使用的相关组件为Nacos、Seata、Feign等。
- 基于Oauth2.0协议,搭建UAA并使用SpringSecurity+Jwt实现分布式认证授权,采用RBAC模型动态分配资源列表。
- 使用Redis缓存旅游景点等热点数据,合理利用HyperLogLog、Geo缓存网页UV值和城市、景点地理位置。
- 使用SpringSchedule定时清除垃圾图片、刷新库存和UV值。使用SpringAOP+自定义注解记录相关操作日志。
- 使用RabbitMQ实现订单延时取消,通过发布订阅模式异步处理订单的附加操作。通过token、本地消息表保证消息幂等性。
- 整合多个第三方服务:腾讯云COS、腾讯云SMS、163邮箱、支付宝沙箱。
- 使用Vue+ElementUI+Axios快速搭建前端页面。
三、运行环境
腾讯云CentOS 7.6(1核2G、2核4G)、jdk8、erlang2.2、gcc4.8.5
四、详细实现方案和技术讲解
1.基于Oauth2.0的分布式权限认证方案
1.1.整体方案
什么是Oauth2.0?简单的讲,就是我们在A应用上可以直接用B应用的账号进行登录,避免了在A应用上再去单独注册一个账号,同时也可以通过授权码模式(后续会讲到)防止A应用直接获取到B应用的密码等,比较常见的例子就是我们可以在csdn上用qq、vx进行登录,qq和vx只需要提供给csdn用户id、头像这些不敏感的信息即可。但在此项目中其实A应用和B应用都是智慧出行这个应用,只是引用了Oauth2.0这个思想。
下图为智慧出行网站基于Oauth2.0的分布式认证授权方案,第一步,接入方(其实就是我们自己的网站)请求UAA;第二步,UAA内部进行用户密码、权限的判断和基于RBAC分配资源(后续讲到);第三步,UAA返回给client一个jwt(后续会讲到)格式的token;第四步,前端访问资源时需要在Headers里添加token进行访问;第五步,资源服务器内部进行token的解析,判断token是否被篡改和过期。
1.2.Oauth2.0的四种授权模式
UAA(User Access authorization)是授权服务器,主要用于注册客户端(client)、验证用户身份、向用户发放令牌(token)和刷新令牌。Oauth2.0涉及到四种授权模式,接下来我们一一讲解:
简单模式
这种模式client只需要向UAA请求token然后通过此token进行资源访问即可,但它本身已经失去了授权的意义,这种模式更适用于一种程序间内部调用的情景。
密码模式
密码模式就比简单模式多了一个步骤,就是需要client携带用户名和密码请求token,但弊端就是我们的用户名和密码可能会因此泄露给client,假设一种情况,此时你需要在智慧出行网站上通过支付宝登录,但这个登录页面是智慧出行网站提供的,你会放心大胆地在这个页面上输入你的支付宝账号和密码吗?
隐式模式
相比于密码模式,隐式模式在用户进行登录验证时,它会重定向到验证服务器,也就是我们用支付宝登录,那么就会redirect到支付宝的验证服务器,此时你就可以很放心地输入密码了,但缺点就是返回的token可能被盗取。在智慧出行网站中,我是采用了隐式模式模拟的第三方登录,为了防止token被盗取后篡改,我使用了jwt格式的token,jwt防止篡改的原理我会在后续讲到。
授权码模式 最安全的模式,也是需要步骤最多的模式,具体的步骤为:client在第三方(支付宝)提供的应用服务器上进行登陆验证后,验证服务器会返回给client一个授权码(code),此时这个code已经在验证服务器中完成了注册,我们访问其他资源的时候就需要携带这个code进行访问,此时就算code被盗取,那也毫无意义,因为想要获取token,是需要code+secret一起去生成token的,而这个secret是应用服务器和验证服务器内部之间已经约定好的一个密钥,外界是无法感知的。
1.3.通过RBAC模型动态分配资源
RBAC(Resource Basic Access Control)资源访问控制是一种模型,此模型将用户和权限之间进行了分离,减少了耦合,接下来我以智慧出行网站为例,讲述如何实现了RBAC以及RBAC的好处。
RBAC中包含用户(User)、角色(Role)、权限(Resource)这几种实体,一个用户可以对应一个角色或者多个角色,一个角色又对应多种权限。
那么此时可能会有一些疑问,搞这么些实体干什么,有什么用? 我这里通过反向假设来进行验证。假如没有这三个实体,那么肯定至少有两个实体,其中一个是用户,一个是角色或者权限。
如果是用户+角色来进行验证,假如此时智慧出行的后台管理只有拥有超级管理员这个role的用户才可以访问,那么有一天来了一个需求,需要新加一个角色,名为后台游客(就是可以访问后台,但不可以进行修改),那么我们就需要改动代码,在访问接口时,判断是否为超级管理员的时候还要判断是否是后台游客(如下图)。这肯定是不行的啊!那么多接口,你仅用一句话说加一个角色,我就要改一整天啊!所以这样的可维护性是极低的。
如果是用户+权限来进行验证,这个就更不用说了,智慧出行网站目前有30多项权限,你要我都写到这个过滤器判断里一个一个判断?这显然不合理,况且我们在存储用户和权限的关系表时也是很费力的,光一个用户就对应那么多权限,维护的时候效率也极低而且容易出错。假如有这么一种情况,在智慧出行中,一般的用户是可以查询所有的城市的,假如说此时甲方说,不行,一般用户不可以获取我们全部的城市,那这个时候你是不是崩溃了?你不得一个一个去改用户的权限哈哈。所以这种也是不合理的。
最后,如果是用户+角色+权限来进行验证,这就舒服多了,甲方你随便加角色,我只需要新建一个角色,然后给这个角色绑定上应有的权限,最后给这个用户分配角色就好了。而且维护的时候也很简单,修改权限只需要修改角色和权限的绑定关系即可。
1.4.jwt格式的token
JWT(Json Web Token)是一种token的格式,它内部含有一些编码格式和加密算法。它分为三部分,分别是:头部(header)、负载(payload)、签名(signature)。
头部:存储声明类型(type=jwt)和加密算法(RS256、HS256等)。
负载:存储用户的基本信息(这里不可以出现密码等敏感信息,否则会泄露密码),如用户名、邮箱、地址、角色、权限等。
签名:签名的话是把上述的头部和负载进行Base64编码后再通过加密算法生成,其中secret存储在服务器,注意,这里就是jwt防止篡改的重点,因为client是不知道secret的,所以我们无法解密signature中的信息,当我们携带此token请求后端服务时,服务器通过secret把signature解密后获得的sinature中的header和payload与整个jwt中的header和payload进行比较,如果两者不一样,说明此token已被篡改,直接报错。
下图为智慧出行网站的token,可以看出jwt格式的token还是很长的,payload的信息越多就越长。 jwt极大的提高了token的安全性,唯一的缺点就是太长,不过这也不算什么大事。
1.5.核心配置文件
在SpringSecurity原有的过滤器链基础上加入Oauth2.0的一些额外的过滤器,所以有两个配置文件,一个是security基础的,一个是oauth2.0额外的。他们的配置规则几乎差不多,差别就是Oauth2.0需要额外配置客户端、端点和令牌的一些东西。
SecurityConfig.java
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true) //启用方法级别的权限认证注解PreAuthorization
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//BCrypt加盐加密
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//加入我们自己的userDetailsService
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {