SpringSecurity Oauth2 - 自定义 SpringBoot Starter 远程访问受限资源
文章目录
- 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 远程访问受限资源相关推荐
- twitter自定义api_为Twitter4j创建自定义SpringBoot Starter
twitter自定义api SpringBoot提供了许多启动器模块来快速启动和运行. SpringBoot的自动配置机制负责根据各种标准代表我们配置SpringBean. 除了Core Spring ...
- 自定义SpringBoot Starter实现
文章目录 自定义stater pom文件 配置文件类properties 使用配置类 创建AutoConfiguration 项目结构 自定义stater pom文件 引入自动配置类spring-bo ...
- 为Twitter4j创建自定义SpringBoot Starter
SpringBoot提供了许多启动器模块来快速启动和运行. SpringBoot的自动配置机制负责根据各种标准代表我们配置SpringBean. 除了Core Spring Team提供的现成的spr ...
- 零基础学习SpringSecurity OAuth2 四种授权模式(理论+实战)(配套视频讲解)
配套视频直达 背景 前段时间有同学私信我,让我讲下Oauth2授权模式,并且还强调是零基础的那种,我也不太理解这个零基础到底是什么程度,但是我觉得任何阶段的同学看完我这个视频,对OAuth2的理解将会 ...
- 玩转springboot:默认静态资源和自定义静态资源实战
点个赞,看一看,好习惯!本文 GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了3个月总结的一线大厂Java面试总结,本人已拿腾 ...
- 简述SpringBoot Starter原理及自定义实现
简述SpringBoot Starter原理及自定义实现 一.简述 二.结合SpringBoot启动原理看容器如何实现自动装配 三.解析mybatis-spring-boot-starter包看myb ...
- 自定义一个SpringBoot Starter
文章目录 简介 使用Spring Initializr创建一个项目 定义一个配置信息映射类 定义一个Service 定义一个配置类自动装配Service 在spring.factories中指定自动装 ...
- springsecurity oauth2使用jwt实现单点登录
Jwt方式已经分享在文章结尾处的百度网盘链接中,redis方式可以看我以前发表的文章. 文章目录 前言 一.springsecurity oauth2 + redis方式的缺点 二.oauth2认证的 ...
- 六、SpringSecurity OAuth2 + SpringCloud Gateway实现统一鉴权管理
代码 代码仓库:地址 代码分支:lesson6 博客:地址 简介 在先前文章中,我们使用SpringSecurity OAuth2搭建了一套基于OAuth2协议的授权系统,并扩展了手机验证码授权模式. ...
最新文章
- Microsoft .NET Framework 4.6.1
- Windows客户机脱域问题及解决办法
- python获取图片的颜色信息
- 腾讯云的ubuntu虚拟主机上再安装VirtualBox遇到的一些错误
- 【开源程序(C++)】获取bing图片并自动设置为电脑桌面背景
- “约见”面试官系列之常见面试题第二十一篇之函数防抖和节流(建议收藏)
- 服务器日志文件中包含堆栈跟踪,日志框架 Logback 官方手册(第三章:Configuration)...
- 算法与程序设计_算法与程序设计入门-简单计算题1
- 增值税发票的种类_以及税率---财务知识工作笔记001
- 随想录(udp经验总结)
- 洛谷P1120【小木棍】(搜索+剪枝)
- 《Linux内核修炼之道》精华版之方法论
- Syzmlw 蜗居大结局
- junit5_JUnit 5测试中的临时目录
- php的seeder是什么,Laravel框架使用Seeder实现自动填充数据功能
- 假设检验 python_假的解释|假的意思|汉典“假”字的基本解释
- 求一个只包含0、1的矩阵中只包含1的最大子矩阵大小
- 搭建Maven私服Nexus
- 特斯拉降价也无法阻挡国内新能源汽车厂商前进的步伐
- 散列表及散列冲突解决方案