该项目基于 SpringCloud 搭建的一个电商系统,采用前后端分离微服务架构,实现了基于 RBAC/JWT 权限认证的解决方案,集成了各种微服务治理和监控功能。模块包括:业务逻辑层管理、 管理员角色权限系统、应用监控、Druid 数据库监控、日志系统(ELK)、配置中心、Swagger 文档、任务调度等。
用户端目前只做了小程序,安卓、IOS端APP可以直接使用接口来开发。
架构图如下:
小程序端
Nacos配置中心
Grafana监控系统
Kibana日志系统
Druid监控系统
Swagger文档
系统模块
leaf-admin:总后台管理系统
leaf-business:商家管理系统
leaf-client:用户端,小程序、app的使用接口
leaf-common:公共模块系统,里面放了一些公告模块和公共类比如:swagger、lombok、分页工具、公共返回结果类、错误代码、公共异常处理等
leaf-gateway:网关
leaf-mbg:使用mybatis和generator自动生成的Entity实体和mapper数据库访问层
leaf-order:订单服务
leaf-score:积分服务
leaf-security:使用spring security封装的一个验证、鉴权模块,实现了使用jwt验证的和RBAC基于角色的访问控制功能
使用 Security 结合 JWT 实现 url 级的权限控制系统,使用 Generator/Mybatis 自动生成数据库操作 的基本方法,数据库连接池使用 Druid 配置多个数据源,并实现了读写分离。日志系统通过 Logback 写到 Redis 队列中供 Logstash 读取,使用 Prometheus、Grafana 对 Redis、Mysql 和微服务进行监控。使用 Redis 实现了分布式锁和分布式事务(TX-LCN/Seata/RocketMQ), 使用 Sentinel 实现服务降级、熔断、限流和监控。
一、leaf-common模块
pom文件依赖了swagger2、lombok这些公共模块,不用重复依赖。
CommonPage类:分页的公共处理对象,封装了pagehelper的总页数、当前页等信息
CommonResult类:公共的结果返回对象,定义了返回的code、message、data字段和封装了一些常用的静态返回方法,比如在控制器中直接使用return CommonResult.success(data);返回结果。
ApiException:定义了我们自己的异常处理类,继承自RuntimeException类,然后创建一个类GlobalExceptionHandler添加全局异常处理类使用注解@GlobalExceptionHandler代码如下:
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = ApiException.class)
public CommonResult handle(ApiException e) {
if (e.getErrorCode() != null) {
return CommonResult.failed(e.getErrorCode());
}
return CommonResult.failed(e.getMessage());
}
}
这样只要在项目中抛出ApiException异常都会被handle捕获,返回通用的CommonResult结果。
还定义了断言类Asserts、返回错误码ResultCode等。
二、leaf-security
这个模块使用spring security封装的一个验证、鉴权的功能,它使用了spring security库。关于security我之前写过一篇介绍原理的博客分析Spring Security实现方式,在这里我只说一下它的应用,在日常的用户登录有以下场景:
一、用户发送username和password到服务器,控制器调用service层去数据库进行账号密码的验证;
二、如果验证成功,把用户信息封装成一个UserDetails使用jwt工具类生成一个token返还给客户端,类似这样eyJhbGciOiJIUzUxMiJ9.eyJuYW1lIjoi6a2P5Lqa5oGSIiwiZXhwIjoxNTg5Nzc2ODMzLCJhZ2UiOjEyM30.J0WvlpSvuS0OIyEpS4uEMqqO2PCWWkWLkFmQKX2l4vl-vVNTpdbiTcNTEj8qX3EFBZUYOIBLfgacaKMH4dCmAQ红色部分是JWT的头部,它定义这个token的加密算法类型,之后使用base64进行编码。黄色部分记录了信息比如用户名、过期时间等然后进行base64编码,最后绿色是对前面两部分内容进行sha256算法签名,秘钥在服务器可以有效防止数据被篡改。关于JWT的介绍可以看这篇文章JSON Web Token;
三、用户端这时候拿到token可以放到cookie中也可以放到本地存储器中,在访问服务器时把它放在HTTP的Header头中。
四、项目使用了security,所以请求到达控制器之前会先通过UsernamePasswordAuthenticationFilter过滤器,我们在UsernamePasswordAuthenticationFilter过滤器之前添加一个自己的过滤器jwtAuthenticationTokenFilter,在这个过滤器中我们验证这个JWT的签名是否合法,并且拿出里面的username,如果这个username在数据库中存在(因为操作数据库频率高,所以使用redis做了缓存),我们拿到用户UserDetails,然后创建一个UsernamePasswordAuthenticationToken实例authentication,再把authentication放到SecurityContextHolder中,这时候security就会从SecurityContext中拿到这个有效的authentication进行验证了。
五、之后就是角色鉴权的流程了,本文主要介绍系统的整体架构,由于篇幅原因关于角色鉴权具体实现可以看我之前的文章分析Spring Security实现方式进行了解。
这里贴一下重要类的实现及配置,关于SecurityConfig的配置类,代码如下:
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired(required = false)
private DynamicSecurityService dynamicSecurityService;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// authorizeRequests方法 定义哪些URL需要被保护、哪些不需要被保护
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
.authorizeRequests();
//不需要保护的资源路径允许访问
for (String url : ignoreUrlsConfig().getUrls()) {
registry.antMatchers(url).permitAll();
}
//允许跨域请求的OPTIONS请求
registry.antMatchers(HttpMethod.OPTIONS)
.permitAll();
// 任何请求需要身份认证
registry.and()
.authorizeRequests()
.anyRequest()
.authenticated()
// 关闭跨站请求防护及不使用session
.and()
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 自定义权限拒绝处理类 添加自定义未授权和未登录结果返回
.and()
.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler())
.authenticationEntryPoint(restAuthenticationEntryPoint())
// 自定义权限拦截器JWT过滤器
.and()
.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//有动态权限配置时添加动态权限校验过滤器
if(dynamicSecurityService!=null){
registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
}
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {