文章目录

  • 1. 自定义 SpringBoot Starter
    • 1. 统一的dependency管理
    • 2. 对外暴露 properties
    • 3. 实现自动装配
    • 4. 指定自动配置类的路径 META-INF\spring.factories
    • 5. 总结
  • 2. 使用自定义 SpringBoot Starter
    • 1. 在新项目中引入自定义Starter依赖配置
    • 2. 编写属性配置文件
  • 3. 业务场景
    • 1. 自定义Starter下的 ViewAutoConfiguration
    • 2. 自定义Starter下的 ViewTemplateInitializer 类
    • 3. 自定义注解 @SystemRequest

1. 自定义 SpringBoot Starter

在实际开发中,对于一些通用业务和公共组件,我们可能想将其做成一个Spring Boot Starter便于所有系统使用,这就需要我们定义自己的Spring Boot Starter。一个Spring Boot Starter都需要具备哪些能力:

① 提供了统一的dependency版本管理:仅需要导入对应的Starter依赖,相关的library,甚至是中间件,都一次性被引入了,而且要保证各dependency之间是不冲突的。例如当我们引入mybatis-spring-boot-starter依赖,mybatis和mybatis-spring等相关依赖也顺带被导入了。

② 提供自动装配的能力:Starter可以自动的向Spring容器中注入需要的Bean,并且完成对应的配置。

③ 对外暴露恰当的properties:Starter不可能提前知道全部的配置信息,有些配置信息只有在应用集成这个Starter的时候才能明确。例如对于mybatis,configLocation、mapperLocation这些参数在每个项目中都可能不同,所以只有应用自己知道这些参数的值该是什么。mybatis-spring-boot-starter对外暴露了一组properties,例如如果我们想指定mapper文件的存放位置,只需要在application.properties中添加mybatis.mapperLocations=classpath:mapping/*.xml即可

1. 统一的dependency管理

创建 view-spring-boot-starter 项目并导入依赖,利用maven的间接依赖特性,在Starter的maven pom.xml中声明所有需要的dependency,这样在项目工程导入这个Starter时,相关的依赖就都被一起导入了。下面是 view-spring-boot-starter 的 pom.xml 。

<artifactId>view-spring-boot-starter</artifactId><dependencies><!-- 省略很多公司项目中自定义的 SpringBoot Starter 依赖--><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-netflix-eureka-client</artifactId></dependency><dependency><groupId>com.netflix.eureka</groupId><artifactId>eureka-client</artifactId></dependency>
</dependencies>

2. 对外暴露 properties

在包 config 下创建类 ViewProperties。实现属性配置:

@ConfigurationProperties(prefix = "hh.view")
@Data
public class ViewProperties {private final static String DEFAULT_VIEW_TEMPLATE_PATH = "classpath:/view-template";/*** 统一化模板保存路径,默认在 classpath:/view-template 目录下*/private String templatePath = DEFAULT_VIEW_TEMPLATE_PATH;
}

3. 实现自动装配

在包config下创建类ViewAutoConfiguration。实现自动配置,把服务注入到Spring中

@Configuration
@Slf4j
@ComponentScan(value = "com.hh.view")
@EnableConfigurationProperties(ViewProperties.class)
public class ViewAutoConfiguration {// 向 Spring 容器中注册 ServiceRegisterClient@Beanpublic ServiceRegisterClient serviceRegisterClient(ApplicationContext applicationContext) {return new FeignClientBuilder(applicationContext).forType(ServiceRegisterClient.class, "view").build();}// 向 Spring 容器中注册 ViewTemplateRegisterClient@Beanpublic ViewTemplateRegisterClient viewTemplateRegisterClient(ApplicationContext applicationContext) {return new FeignClientBuilder(applicationContext).forType(ViewTemplateRegisterClient.class, "view").build();}
}

@ComponentScan 注解注解时用于配置类上的,一般和 @Configuration 注解一起使用,主要的作用就是定义包扫描的规则,Spring会去自动扫描 base-package 指定的包及其子包下的带有@Service,@Component,@Repository,@Controller注解的类,并将这些类自动装配到Spring容器内,然后交由Spring容器进行统一管理。

@EnableConfigurationProperties 获取读取配置文件的属性并注入到ViewProperties属性配置类中。

@Bean 向Spring容器中注入 ServiceRegisterClient 实例。

4. 指定自动配置类的路径 META-INF\spring.factories

在资源目录下,创建文件 META-INF\spring.factories,指定自动配置类的路径:

# AutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hh.view.config.ViewAutoConfiguration

5. 总结

创建自定义的 Spring Boot Starter 并不是什么难事,自定义 starter 的步骤:

① 确保在 pom.xml 文件中声明了使用该组件所需要的全部 dependency

② 利用 @ConfigurationProperties 注解对外暴露恰当的 properties

③ 利用条件注解 @ConditionalXXX编写XXXAutoConfiguration 类

④ 把写好的 XXXAutoConfiguration 类加到 META-INF/spring.factories 文件的 EnableAutoConfiguration 配置中,这样在应用启动时就会自动加载并执行 XXXAutoConfiguration。

2. 使用自定义 SpringBoot Starter

当其他项目需要使用 view-spring-boot-starter 时,就需要使用导入该依赖。

1. 在新项目中引入自定义Starter依赖配置

创建一个新的SpringBoot项目 incident ,在项目的pom.xml文件中引入自定义SpringBoot Starter的依赖配置如下:

<dependency><groupId>com.hh</groupId><artifactId>ngsoc-view-spring-boot-starter</artifactId><version>3.0.1</version>
</dependency>

2. 编写属性配置文件

    hh:view:templatePath: view-templatengsoc:security:api-key: 78464adf485cddf6c18615341fd4ebf813f02e06722b913721009134bee3a548

在 resource/view-template 目录下创建:incident.json,incidentTemplate.json、incidentRecordTemplate.json,这些文件就是统一化模板的配置文件,读取这些文件进行统一化模板的配置。

当 incident 项目启动的时候,会去加载 view-spring-boot-starter 中的自动配置类 ViewAutoConfiguration。

3. 业务场景

1. 自定义Starter下的 ViewAutoConfiguration

① 当 incident 项目启动的时候,会去加载 view-spring-boot-starter 中的自动配置类 ViewAutoConfiguration:

@Configuration
@Slf4j
@ComponentScan(value = "com.hh.view")
@EnableConfigurationProperties(ViewProperties.class)
public class ViewAutoConfiguration {// 向 Spring 容器中注册 ServiceRegisterClient@Beanpublic ServiceRegisterClient serviceRegisterClient(ApplicationContext applicationContext) {return new FeignClientBuilder(applicationContext).forType(ServiceRegisterClient.class, "view").build();}// 向 Spring 容器中注册 ViewTemplateRegisterClient@Beanpublic ViewTemplateRegisterClient viewTemplateRegisterClient(ApplicationContext applicationContext) {return new FeignClientBuilder(applicationContext).forType(ViewTemplateRegisterClient.class, "view").build();}
}

在该配置类上的注解 @ComponentScan(value = “com.hh.view”) 会去扫描 com.hh.view 包及其子包下所有带有@Service,@Component,@Repository,@Controller注解的类,并将这些类自动装配到Spring容器内,然后交由Spring容器进行统一管理。

2. 自定义Starter下的 ViewTemplateInitializer 类

② 在 com.hh.view.inialializer 包存在 ViewTemplateInitializer 类:

@Service
@Slf4j
@ComponentScan(value = "com.hh.view")
public class ViewTemplateInitializer extends AbstractRegister implements InitializingBean {// ...
}

可以看到 ViewTemplateInitializer 类加了 @Service 注解,表示该类的生命周期会交给 Spring 容器去管理,Spring容器在启动的时候会完成bean的初始化。ViewTemplateInitializer 类不仅继承了AbstractRegister 而且实现了InitializingBean:

@Slf4j
@RequiredArgsConstructor
@Data
public abstract class AbstractRegister implements InitializingBean {// 创建一个线程池,其中核心线程数10个,最大线程数20个,阻塞队列的大小为10,用来执行异步任务public static final ExecutorService REGISTER_POOL = new ThreadPoolExecutor(10, 20, 200L, TimeUnit.MICROSECONDS,new LinkedBlockingDeque<>(10),new ThreadFactoryBuilder().setNameFormat("view-register-runner-%d").build());// 服务是否已经可用private Boolean ready = false;// 已经重试次数private int retryTimes = 0;// 重试等待时间private final int waitSeconds;/***  Spring启动后,初始化Bean时,若该Bean实现 InitializingBean 接口,*  会自动调用 afterPropertiesSet()方法,完成一些自定义的初始化操作。*/@Overridepublic void afterPropertiesSet() {// 向线程池提交任务,当有任务到达线程池后,就会创建创建一个核心工作线程来执行线程池中的任务REGISTER_POOL.submit(this::execute);}public void execute() {while (!this.ready) {retryTimes++;try {// 注册服务register();onSuccess();this.ready = true;} catch (Throwable e) {onFailed(e);try {TimeUnit.SECONDS.sleep(waitSeconds);} catch (InterruptedException interruptedException) {// ignore}}}}@PreDestroypublic void destroy() {this.ready = true;// 关闭线程池,该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。REGISTER_POOL.shutdown();}/*** 注册*/public abstract void register();/*** 成功时 回调*/public abstract void onSuccess();/*** 失败时 回调** @param e 异常*/public abstract void onFailed(Throwable e);
}

我们知道Spring启动后,初始化Bean时,若该Bean实现 InitializingBean 接口,会自动调用 afterPropertiesSet()方法,完成一些自定义的初始化操作。

① 创建一个线程池,其中核心线程数10个,最大线程数20个,阻塞队列的大小为10,用来执行异步任务;

② 当Spring容器启动后初始化 AbstractRegister 后,会去执行 afterPropertiesSet() 方法,在该方法中向线程池中提交任务;

③ 线程池在收到任务后会创建一个工作线程来执行该任务。

④ 最终会执行 AbstractRegister 子类 ViewTemplateInitializer 的 registerRemote() 方法。

@Service
@Slf4j
@ComponentScan(value = "com.hh.view")
public class ViewTemplateInitializer extends AbstractRegister implements InitializingBean {@Setter(onMethod_ = @Autowired)private ViewTemplateRegisterClient viewTemplateRegisterClient;// 省略。。。@Overridepublic void register() {// 注册到view服务上registerRemote(viewTemplateProvider.provide());}// 远程注册统一化模板private void registerRemote(List<ViewTemplateRegistry> viewTemplates) {RegisterQo<List<ViewTemplateRegistry>> registerQo = new RegisterQo<>();registerQo.setRegisterBody(viewTemplates);registerQo.setAppName(this.appName);// 探讨的重点!!!!!!这里就是我今天引入的入口!!!!!!!!!!!!!!!!final ApiResponse<Void> result = viewTemplateRegisterClient.register(registerQo);log.info("注册统一化模板结果为:{}", result);if (result.getCode() != ApiResponse.CODE_OK) {throw new DataQueryException("注册统一化模板失败,原因:" + result.getData());}}
}

可以看到在该方法中会远程调用 view 服务下的 register() 方法:

@FeignClient(value = "view")
public interface ViewTemplateRegisterClient {/*** 注册服务** @param registerQo 注册内容* @return 注册结果*/@RequestMapping(value = "/VIEW/api/v1/view/template/register", method = RequestMethod.POST)@SystemRequestApiResponse<Void> register(@RequestBody RegisterQo<List<ViewTemplateRegistry>> registerQo);
}
@RestController
@RequestMapping("/api/v1/view/template")
@ResponseResult
@Api(tags = "页面模板管理")
@Validated
public class ViewTemplateController {@Setter(onMethod_ = @Autowired)private ViewTemplateService viewTemplateService;@ApiOperation("注册页面模板")@PostMapping("/register")@PreAuthorize("hasAnyAuthority('superAdmin')")public void register(@RequestBody @Validated RegisterQo<List<ViewTemplateRegistry>> registerQo) {viewTemplateService.register(registerQo.getAppName(), registerQo.getRegisterBody());}
}

那么问题来了,在 incident 项目启动时,需要加载自定义的 Spring Boot Starter,而在我们自定义的 view-spring-boot-starter 项目下的 ViewTemplateInitializer 中远程调用了 view 项目中的统一化模板注册接口,由于项目使用 SpringSecurity Oauth2 搭建了认证服务器和授权服务器,因此访问 view 项目中的受限资源时,需要带着 accessToken,而此时由于系统仍然在后台启动中未处于登录状态,所以访问 view 项目下的请求时并未携带 accessToken 导致无权限访问。

即当访问 /api/v1/view/template/register 路径下的受限资源时,需要携带 access_token ,此时就需要自定义认证方式来实现获取 accessToken 来访问系统受限资源。

我们可以看到 ViewTemplateRegisterClient 的 register() 方法上加了一个自定义注解 @SystemRequest,该自定义注解 @SystemRequest 中实现了通过自定义认证方式获取 access_token 并访问受限资源的功能,下面重点来看这个注解吧。

3. 自定义注解 @SystemRequest

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemRequest {}
@ConfigurationProperties("ngsoc.security")
@Data
public class FeignRequestProperties {/*** apikey*/private String apiKey;/*** token类型 默认是bearer*/private String tokenType;/*** 登录地址*/private String loginUrl;
}
@Slf4j
public class SystemRequestHandler {public final RedisTemplate<String, String> redisTemplate;public final ReentrantLock lock = new ReentrantLock();/*** 当前登录的token*/public volatile String token;/*** feign请求的属性*/public final FeignRequestProperties properties;public SystemRequestHandler(RedisTemplate<String, String> redisTemplate, FeignRequestProperties properties) {this.redisTemplate = redisTemplate;this.properties = properties;}/*** 尝试通过ApiKey登录** @param requestTemplate 请求模板*/public void tryLoginByApiKey(RequestTemplate requestTemplate) {lock.lock();try {// token 超时则尝试登录if (StringUtils.isBlank(this.token) || !redisTokenExists(this.token)) {login();}requestTemplate.header("Authorization", properties.getTokenType() + " " + this.token);} catch (Exception e) {log.error("API 登陆失败", e);} finally {lock.unlock();}}/*** 校验token是否存在** @param token token* @return 校验结果*/private boolean redisTokenExists(final String token) {return ngsocAuthTokenExists(token) && oAuthTokenExists(token);}/*** redis中token是否存在** @param token token* @return redis中是否存在这个token*/public boolean ngsocAuthTokenExists(final String token) {if (StringUtils.isBlank(token)) {return false;}final Boolean isTokenExits = redisTemplate.hasKey(RedisKeyUtil.getAuthAccessTokenKey(token));return Objects.nonNull(isTokenExits) && isTokenExits;}/*** redis中token是否存在** @param token token* @return redis中是否存在这个token*/public boolean oAuthTokenExists(String token) {if (StringUtils.isBlank(token)) {return false;}final Boolean isTokenExits = redisTemplate.hasKey(RedisKeyUtil.getOauthTokenKey(token));return Objects.nonNull(isTokenExits) && isTokenExits;}/*** 判断当前feign方法是否需要登录apiKey*** @param methodMetadata 方法元数据* @return 是否需要登录apiKey*/public boolean isSystemRequest(MethodMetadata methodMetadata) {final String apiKey = properties.getApiKey();final Method method = methodMetadata.method();return method.isAnnotationPresent(SystemRequest.class) && Objects.nonNull(apiKey);}/*** 尝试登录并初始化*/public void login() throws AuthenticationException {RestTemplate restTemplate = new RestTemplate();// 构造接口登录参数(body)Map<String, String> loginParams = new HashMap<>(1);loginParams.put("apiKey", properties.getApiKey());// 发送接口请求String tokenUrl = properties.getLoginUrl();log.info("尝试使用API-KEY登陆 :{}, 参数 :{} ", tokenUrl, loginParams);HttpEntity<Map<String, String>> request = new HttpEntity<>(loginParams);final ParameterizedTypeReference<ApiResponse<ApiKeyToken>> typeReference = new ParameterizedTypeReference<>() {};// 在这里会调用 SpringSecurity Oauth2 的 自定义认证方式final ResponseEntity<ApiResponse<ApiKeyToken>> result = restTemplate.exchange(tokenUrl, HttpMethod.POST, request, typeReference);log.info("API-KEY登陆结果 :{}", result);Objects.requireNonNull(result.getBody());Objects.requireNonNull(result.getBody().getData());if (result.getBody().getCode() != ApiResponse.CODE_OK) {throw new AuthenticationException(result.getBody().getMessage());}this.token = result.getBody().getData().getAccessToken();log.info("成功刷新token缓存, token={}", this.token);}
}

具体如何实现自定义认证方式,我们下篇文章再看吧。

SpringSecurity Oauth2 - 自定义 SpringBoot Starter 远程访问受限资源相关推荐

  1. twitter自定义api_为Twitter4j创建自定义SpringBoot Starter

    twitter自定义api SpringBoot提供了许多启动器模块来快速启动和运行. SpringBoot的自动配置机制负责根据各种标准代表我们配置SpringBean. 除了Core Spring ...

  2. 自定义SpringBoot Starter实现

    文章目录 自定义stater pom文件 配置文件类properties 使用配置类 创建AutoConfiguration 项目结构 自定义stater pom文件 引入自动配置类spring-bo ...

  3. 为Twitter4j创建自定义SpringBoot Starter

    SpringBoot提供了许多启动器模块来快速启动和运行. SpringBoot的自动配置机制负责根据各种标准代表我们配置SpringBean. 除了Core Spring Team提供的现成的spr ...

  4. 零基础学习SpringSecurity OAuth2 四种授权模式(理论+实战)(配套视频讲解)

    配套视频直达 背景 前段时间有同学私信我,让我讲下Oauth2授权模式,并且还强调是零基础的那种,我也不太理解这个零基础到底是什么程度,但是我觉得任何阶段的同学看完我这个视频,对OAuth2的理解将会 ...

  5. 玩转springboot:默认静态资源和自定义静态资源实战

    点个赞,看一看,好习惯!本文 GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了3个月总结的一线大厂Java面试总结,本人已拿腾 ...

  6. 简述SpringBoot Starter原理及自定义实现

    简述SpringBoot Starter原理及自定义实现 一.简述 二.结合SpringBoot启动原理看容器如何实现自动装配 三.解析mybatis-spring-boot-starter包看myb ...

  7. 自定义一个SpringBoot Starter

    文章目录 简介 使用Spring Initializr创建一个项目 定义一个配置信息映射类 定义一个Service 定义一个配置类自动装配Service 在spring.factories中指定自动装 ...

  8. springsecurity oauth2使用jwt实现单点登录

    Jwt方式已经分享在文章结尾处的百度网盘链接中,redis方式可以看我以前发表的文章. 文章目录 前言 一.springsecurity oauth2 + redis方式的缺点 二.oauth2认证的 ...

  9. 六、SpringSecurity OAuth2 + SpringCloud Gateway实现统一鉴权管理

    代码 代码仓库:地址 代码分支:lesson6 博客:地址 简介 在先前文章中,我们使用SpringSecurity OAuth2搭建了一套基于OAuth2协议的授权系统,并扩展了手机验证码授权模式. ...

最新文章

  1. Microsoft .NET Framework 4.6.1
  2. Windows客户机脱域问题及解决办法
  3. python获取图片的颜色信息
  4. 腾讯云的ubuntu虚拟主机上再安装VirtualBox遇到的一些错误
  5. 【开源程序(C++)】获取bing图片并自动设置为电脑桌面背景
  6. “约见”面试官系列之常见面试题第二十一篇之函数防抖和节流(建议收藏)
  7. 服务器日志文件中包含堆栈跟踪,日志框架 Logback 官方手册(第三章:Configuration)...
  8. 算法与程序设计_算法与程序设计入门-简单计算题1
  9. 增值税发票的种类_以及税率---财务知识工作笔记001
  10. 随想录(udp经验总结)
  11. 洛谷P1120【小木棍】(搜索+剪枝)
  12. 《Linux内核修炼之道》精华版之方法论
  13. Syzmlw 蜗居大结局
  14. junit5_JUnit 5测试中的临时目录
  15. php的seeder是什么,Laravel框架使用Seeder实现自动填充数据功能
  16. 假设检验 python_假的解释|假的意思|汉典“假”字的基本解释
  17. 求一个只包含0、1的矩阵中只包含1的最大子矩阵大小
  18. 搭建Maven私服Nexus
  19. 特斯拉降价也无法阻挡国内新能源汽车厂商前进的步伐
  20. 散列表及散列冲突解决方案

热门文章

  1. 判断字符串是否是email格式 正则表达式
  2. Fortran语言学习记录
  3. 汉诺塔(河内塔)算法与心得
  4. oommf 提示 mmArchive 保存场文件失败
  5. Unity中实现UI描边
  6. 点击页面上的按钮后更新TextView的内容,谈谈你的理解?(阿里面试题 参照Alvin笔记 Handler源码解析)
  7. 研究生比本科生还多,我的学历成一张废纸了吗
  8. 高可用性架构:云计算和高可用性
  9. 基于Holt-Winters方法对资源进行预测
  10. msq_table's methods