小Hub领读:

讲解视频也同步发布啦,记得去看哈,一键三连哇。

视频讲解:https://www.bilibili.com/video/bv1SD4y1o7cN

这系统,一定要学会用户-服务认证,服务-服务鉴权的那一套,这才算学会。


简介

Cloud-Platform是国内首个基于Spring Cloud微服务化开发平台,具有统一授权、认证后台管理系统,其中包含具备用户管理、资源权限管理、网关API 管理等多个模块,支持多业务系统并行开发,可以作为后端服务的开发脚手架。代码简洁,架构清晰,适合学习和直接项目中使用。核心技术采用Spring Boot 2.1.2以及Spring Cloud (Greenwich.RELEASE) 相关核心组件,采用Nacos注册和配置中心,集成流量卫兵Sentinel,前端采用vue-element-admin组件,Elastic Search自行集成。

B站视频讲解:https://www.bilibili.com/video/BV1SD4y1o7cN/

ps:注意本文讲解是基于Cloud-Platform v2.5版本,不是最新版本!

技术选型

前端:vue-element-admin

后端:springcloud(eureka、gateway、admin、sidecar、Hystrix、feign、ribbon、zipkin)、tk+mybatis、lucene、jwt、rest

项目结构

ace-security
  ace-modules--------------公共服务模块(基础系统、搜索、OSS)
  ace-auth-----------------服务鉴权中心
  ace-gate-----------------网关负载中心
  ace-common---------------通用脚手架
  ace-control--------------运维中心(监控、链路)
  ace-sidebar--------------调用第三方语言服务

项目启动

须知: 因为Cloud-Platform是一个前后端分离的项目,所以后端的服务必须先启动,在后端服务启动完成后,再启动前端的工程。

环境

  • mysql,redis,maven

  • jdk1.8

  • IDE插件一个,lombok插件,具体百度即可

  • node

前端

git链接:https://gitee.com/geek_qi/cloud-platform-ui

  • 先clone到本地,并进入cloud-platform-ui目录打开命令行窗口(cmd)

  • 因为涉及node.js,所以需要安装npm,node等环境

node.js安装教程:http://nodejs.cn/download/下载msi版本安装。

安装之后,命令行窗口,表示安装成功。

给项目打包依赖

# 1、安装淘宝镜像依赖
npm install -g cnpm --registry=https://registry.npm.taobao.org
# 2、安装项目依赖
cnpm install
# 启动服务
npm run dev

启动成功后会自动打开链接:http://localhost:9527/

后端

首先clone项目下来(v2.5版本):https://gitee.com/geek_qi/cloud-platform/tree/v2.5/

导入到idea中,然后导入数据库sql:

修改数据库的账号密码(直接ctrl+shirt+r,搜索datasource:,可以很方便修改):

接下来启动redis,然后安装顺序启动我们的服务

# 启动顺序
CenterBootstrap -> AuthBootstrap -> AdminBootstrap -> GatewayServerBootstrap

其他监控或服务可以先不启动。

服务说明

ace-auth-server

关键类:

  • AuthController

    • /jwt开头的控制器,登录、刷新、校验jwt

    • 网关不拦截这个链接的请求

  • ClientController

    • 对外暴露的接口,可通过客户端的id和密钥获取到对应的授权相关资源

  • ServiceController

    • 后台管理系统服务管理模块接口

  • ClientTokenInterceptor

    • 拦截feign接口发起的请求,并自动添加请求头token

  • ServiceAuthRestInterceptor

    • 拦截的url为/service/**(WebConfiguration)

    • 服务之间的调用鉴权

    • 一般feign、或者restTemplate方式调用

  • UserAuthRestInterceptor

    • 拦截的url为/service/**(WebConfiguration)

    • 获取用户的token并解析,为会话上下文添加用户信息(ThreadLocal)

  • OkHttpTokenInterceptor

    • 拦截所有的feign请求,OkHttp重写请求

    • OkHttp3一个强有力的机制,能够监控,重写以及重试(请求的)调用

模块分析

用户授权

首先我们来弄清楚一下登录的流程,登录中和之后发生了什么事,那么我们打开前段登录页面,按下F12,然后点击登录,可以查看到以下几个动作:

  • 点击登录,提交表单:http://localhost:9527/api/auth/jwt/token

#返回值
{"status":200,"data":"eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJJZCI6IjEiLCJuYW1lIjoiTXIuQUciLCJleHAiOjE1NjE0NDEyNTZ9.tXNw8nhAFmI4QIQDpKy4DzWtJSTpfwD4685JqbA2pGScdyfXt_5DDs_r1gVZA4CwQC4oZxBsmLKZGclTLGc4HKeXlP2PiVoHZfSWymFRLNfvFqOzKUETJ6WpyDqK55yjf1wddTBD3VzSFvY49uunvozEcb2oFjOs3M_I2sgxAAU","rel":false}

可以看到登录成功之后返回的是一个jwt的token值,应该就是标识用户身份用的token。

  • ajax发起请求获取用户信息以及菜单列表等:http://localhost:9527/api/admin/user/front/info?token=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJJZCI6IjEiLCJuYW1lIjoiTXIuQUciLCJleHAiOjE1NjE0NDEyNTZ9.tXNw8nhAFmI4QIQDpKy4DzWtJSTpfwD4685JqbA2pGScdyfXt_5DDs_r1gVZA4CwQC4oZxBsmLKZGclTLGc4HKeXlP2PiVoHZfSWymFRLNfvFqOzKUETJ6WpyDqK55yjf1wddTBD3VzSFvY49uunvozEcb2oFjOs3M_I2sgxAAU

# 返回值
{"id":"1","username":"admin","name":"Mr.AG","description":"","menus":[{"code":"userManager","type":"menu","uri":"/admin/user","method":"GET","name":"访问","menu":"用户管理"},{"code":"baseManager","type":"menu","uri":"/admin","method":"GET","name":"访问","menu":"基础配置管理"},
.....(此次删除了部分),{"code":"serviceManager:btn_clientManager","type":"button","uri":"/auth/service/{*}/client","method":"POST","name":"服务授权","menu":"服务管理"}]}

同时注意请求头的信息:带有刚才登录后的jwt token,名称叫Authorization,以后的所有请求都会带上这个Authorization用于标识用户身份。 

接下来我们来走下这个过程:

  • 客户端点击按钮发起登录请求http://localhost:9527/api/auth/jwt/token

    • 获取请求uri,method

    • 判断是否是不拦截地址

    • 刚好我们发现不拦截地址中有这个配置:gate: ignore: startWith: /auth/jwt

    • 所以网关过滤器直接将请求代理到ace-auth服务

    • 到达网关gate首先经过我们的过滤器AccessGatewayFilter

  • 网关ace-gateway-v2的代理以及过滤配置

 # 网关代理规则
 spring:
   cloud:
      gateway:
        locator:
          enabled: true
        routes:
        # =====================================
        - id: ace-auth
          uri: lb://ace-auth
          order: 8000
          predicates:
          - Path=/api/auth/**
          filters:
          - StripPrefix=2
        - id: ace-admin
          uri: lb://ace-admin
          order: 8001
          predicates:
          - Path=/api/admin/**
          filters:
          - StripPrefix=2
gate:
  ignore:
    startWith: /auth/jwt

那么我们进入ace-auth中找到对应controller 可以找到这里:

@RestController
@RequestMapping("jwt")
@Slf4j
public class AuthController {    @RequestMapping(value = "token", method = RequestMethod.POST)
    public ObjectRestResponse<String> createAuthenticationToken(
            @RequestBody JwtAuthenticationRequest authenticationRequest) throws Exception {
        log.info(authenticationRequest.getUsername()+" require logging...");
        final String token = authService.login(authenticationRequest);
        return new ObjectRestResponse<>().data(token);
    }
}

这个方法里面只有一个方法就是authService.login,点这个方法进去发现里面又有个方法是userService.validate,这是一个feign接口

@FeignClient(value = "ace-admin",configuration = FeignConfiguration.class)
public interface IUserService {  @RequestMapping(value = "/api/user/validate", method = RequestMethod.POST)
  public UserInfo validate(@RequestBody JwtAuthenticationRequest authenticationRequest);

}可以看到这是远程调用,那么接下来,我们看看远程调用的过程,这设计到服务间的相互鉴权

服务间鉴权

上面的userService.validate,类上有这样的注解

@FeignClient(value = "ace-admin",configuration = FeignConfiguration.class)
表示声明式调用ace-admin服务,接下来我们解析一下这个过程发生了什么事情,我们可以看到有个configuration = FeignConfiguration.class,我们打开FeignConfiguration
@Configuration
public class FeignConfiguration {
    @Bean
    ClientTokenInterceptor getClientTokenInterceptor(){
        return new ClientTokenInterceptor();
    }
}

再打开ClientTokenInterceptor :

public class ClientTokenInterceptor implements RequestInterceptor {
    private Logger logger = LoggerFactory.getLogger(ClientTokenInterceptor.class);
    @Autowired
    private ClientConfiguration clientConfiguration;
    @Autowired
    private AuthClientService authClientService;    @Override
    public void apply(RequestTemplate requestTemplate) {
        logger.info("----> 为feign调用添加token头");
        try {
            requestTemplate.header(clientConfiguration.getClientTokenHeader(), authClientService.apply(clientConfiguration.getClientId(), clientConfiguration.getClientSecret()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
从上面的配置我们知道,RequestInterceptor 是feign接口下的包装拦截器,从代码里面看到,其实意思就是在feign发起远程调用时候往请求里添加请求头信息(clientToken),所以发起的请求头中就有了当前服务的身份token,接受端的服务器就能根据token辨别来源服务器的身份。

接下来我们看看接收端怎么辨别的。ServiceAuthRestInterceptor

public class ServiceAuthRestInterceptor extends HandlerInterceptorAdapter {
    private Logger logger = LoggerFactory.getLogger(ServiceAuthRestInterceptor.class);    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        logger.info("------>判断服务A是否有权限访问当前服务B ~");
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        // 配置该注解,说明不进行服务拦截
        IgnoreClientToken annotation = handlerMethod.getBeanType().getAnnotation(IgnoreClientToken.class);
        if (annotation == null) {
            annotation = handlerMethod.getMethodAnnotation(IgnoreClientToken.class);
        }
        if(annotation!=null) {
            return super.preHandle(request, response, handler);
        }        String token = request.getHeader(serviceAuthConfig.getTokenHeader());
        IJWTInfo infoFromToken = serviceAuthUtil.getInfoFromToken(token);
        String uniqueName = infoFromToken.getUniqueName();
        for(String client:serviceAuthUtil.getAllowedClient()){ //ace-auth、ace-gate
            if(client.equals(uniqueName)){
                return super.preHandle(request, response, handler);
            }
        }
        throw new ClientForbiddenException("Client is Forbidden!");
    }
}

从代码看到首先看下有没IgnoreClientToken的注解,有的话就跳过。没有的话继续获取调用端服务器的token,然后再去获取当前服务器允许访问的lient(serviceAuthUtil.getAllowedClient()),然后匹配调用端的名称是否在允许的客户端内,如果允许就继续,不允许就forbidden。所以挺清晰的。那这套客户端和允许的访问的客户端这套关系是哪里维护的呢,其实有两张表

  • auth_client :客户端的id和名称等

  • auth_client_service:服务端允许调用的客户端关联表

从这张表中,我们就可以得出哪些允许被访问,哪些不能被访问了

cloud-platform鉴权逻辑.png

服务间调用优化.png

总结

第一件事

  • 新建所有需要用到的模块,基本搭建好框架

  • 做好增删改查的封装、接口规范

  • 全局异常捕捉封装

  • 单元测试

  • 通用工具类

第二件事

  • 决定服务的授权模式(jwt、oauth2等)

第三件事

  • 服务内部鉴权

  • 权限控制

视频讲解:https://www.bilibili.com/video/bv1SD4y1o7cN


(完)

MarkerHub文章索引:

https://github.com/MarkerHub/JavaIndex
【推荐阅读】
开源即责任

总在说SpringBoot内置了tomcat启动,那它的原理你说的清楚吗?

谈谈在Java中如何优雅地判空

SpringBoot 整合 Quartz 实现 JAVA 定时任务的动态配置

骚操作:不重启 JVM,如何替换掉已经加载的类?

记一次订单号重复的事故,快看看你的 UUID 在并发下还正确吗?

Spring如何实现AOP,请不要再说cglib了!

懵!啥是Java软引用、弱引用、虚引用?


好文章!点个在看!

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐