代码地址与接口看总目录:【学习笔记】记录冷冷-pig项目的学习过程,大概包括Authorization Server、springcloud、Mybatis Plus~~~_清晨敲代码的博客-CSDN博客
之前只零碎的学习过spring-cloud-alibaba,并没有全面了解过,这次学习pig框架时,想着可以根据这个项目学习一下,练练手,于是断断续续的用了几天时间搭建了一下基础框架。目前就先重点记录一下遇到的问题吧,毕竟流程也不是特别复杂,就是有的东西没遇到过了解的也不深~
由于微服务包括认证这里内容太多,所以分了好几篇~
第一篇文章:No6.从零搭建spring-cloud-alibaba微服务框架,实现fegin、gateway、springevent等(一)_清晨敲代码的博客-CSDN博客
文章包括:
1.将服务系统注册到nacos注册中心;
2.通过nacos实现配置动态更新;
3.添加fegin服务,实现服务之间调用;
4.添加网关(学会使用webflux,学会添加过滤器);
5.添加log服务,通过springevent实现,并使用注解使用(使用AOP);
第二篇文章:
No6.从零搭建spring-cloud-alibaba微服务框架,实现数据库调用、用户认证与授权等(二,no6-2)_清晨敲代码的博客-CSDN博客
文章包括:
6.添加 mysql 数据库调用,并使用mybatis-plus操作;
7.在认证模块添加用户认证,基于oauth2的自定义密码模式(已认证用户是基于自定义token加redis持久化,不是session);
本篇文章包括:
8.在资源端模块添加用户认证,基于授权端的认证token添加逻辑(但是没有处理微服务间的不鉴权调用,微服务间的调用接口都是白名单呢!);
剩余包括(会有变动):
9.解决微服务间的不鉴权调用(优化外部访问鉴权逻辑~)
10.添加用户权限校验等逻辑;
目录
A8.在资源端模块添加用户认证,基于授权端的认证token添加逻辑;
再给 pig-upms模块添加 security 鉴权前,突然意识到 pig-auth 模块认证的时候是会远程调用它的接口的,如果这个接口需要鉴权后才能调用,这样就需要 pig-auth 提供认证信息了。可pig-auth 调用接口本身就是要进行认证的,这不就成死循环了吗?
而且抛开这个问题,后续开发中会有多个微服务之间进行远程调用,难道都需要携带认证信息吗?
于是在理解了 pig 项目里面的微服务之间的调用逻辑后,可以参考他的文档:feign调用及服务间鉴权 · 语雀和Inner注解使用说明 · 语雀,反正我有些没懂,然后看了源码后才理解,于是写了篇笔记防止自己忘掉:【pig-cloud项目】关于@Inner和@PreAuthorize的理解,以及微服务内外部间的调用认证鉴权理解
在开始前,首先明确目标,先实现资源端的用户认证,微服务间的调用在下一个A9里面实现,那么这里就需要注意要先将A7里面用户认证所需要的接口添加到白名单里面,不然是没有认证权限,就永无法调用的!
开始步骤:
1.不用导包,因为authorization-server包里面带有resource-server包;
2.在pig-common-security模块添加security白名单PermitAllUrlProperties,对外暴露URL,可以添加到nacos配置中心;
3.在pig-common-security模块添加请求认证的决策器BearerTokenResolver,对于白名单不进行认证,其余路径必须经过token认证,不认证则不能访问;
4.在pig-common-security模块添加自定义令牌自省类OpaqueTokenIntrospector,通过token拿到授权端用户认证信息,然后进行用户认证(也可以直接认证成功,添加是为了防止用户信息更改);
5.在pig-common-security模块添加认证异常处理类AuthenticationEntryPoint,处理token失效、无token、token异常等情况;
6.在pig-common-security模块将需要bean的类通过PigResourceServerAutoConfiguration注入容器;
7.在pig-common-security模块在PigResourceServerConfiguration添加资源端的安全过滤链,要将oauth2ResourceServer配置成我们自定义的;
8.最后在pig-common-security模块将资源端认证配置类封装成了注解,然后加到需要的资源端启动类上!
注意:由于之前upms模块不需要security,所以在启动类上exclude = SecurityAutoConfiguration.class,现在记得要去掉这个~
步骤代码:
- //2. 源服务器对外直接暴露URL
- package com.pig4cloud.pig.common.security.component;
-
- @Slf4j
- @ConfigurationProperties(prefix = "security.oauth2.ignore")
- public class PermitAllUrlProperties {
-
- @Getter
- @Setter
- private List
urls = new ArrayList<>(); -
- }
- //----------------------------
- //在nacos的application-dev.yml里面添加配置
-
- # 自定义的 spring security 配置,目前只对资源端认证有效(也就是只有资源端认证配置有用到);注意,如果有{##}的可能会匹配多个路径,需要对路径添加规则,并检查哦~
-
- security:
- oauth2:
- # 通用放行URL,服务个性化,请在对应配置文件覆盖
- ignore:
- urls:
- - /user/info/*
- - /client/getClientDetailsById/*
- //3.请求认证的决策器,白名单不进行认证
-
- public class PigBearerTokenExtractorResolver implements BearerTokenResolver {
-
- /**
- * 可以设置为灵活的配置项
- */
- private boolean allowFormEncodedBodyParameter = false;
-
- private boolean allowUriQueryParameter = false;
-
- /**
- * 路径白名单
- */
- private final PermitAllUrlProperties permitAllUrlProperties;
-
- /**
- * Pattern可以看作是一个正则表达式的匹配模式,Matcher可以看作是管理匹配结果
- */
- private static final Pattern authenticationPattern = Pattern.compile("^Bearer (?
[a-zA-Z0-9-:._~+/]+=*)$" , Pattern.CASE_INSENSITIVE); -
- private final PathMatcher pathMatcher = new AntPathMatcher();
-
- public PigBearerTokenExtractorResolver(PermitAllUrlProperties urlProperties) {
- this.permitAllUrlProperties = urlProperties;
- }
-
-
- @Override
- public String resolve(HttpServletRequest request) {
- //校验请求路径,校验是否是白名单,是直接返回null
- boolean match = permitAllUrlProperties.getUrls().stream().anyMatch(url -> pathMatcher.match(url, request.getRequestURI()));
- if(match){
- return null;
- }
-
- //校验hearder里的accesstoken格式是否匹配,并返回token
- final String headerToken =resolveFromHeader(request);
- //校验请求方式是否是GET/POST,是就从参数中获取accesstoken,并返回token
- final String parameterToken = isParameterSupportedForRequest(request) == true ? resolveFromParameters(request) : null ;
- //判断如果headertoekn不是空
- if(StringUtils.hasText(headerToken)){
- //如果parametertoekn不是空则抛出多个accesstoken异常
- if(StringUtils.hasText(parameterToken)){
- final BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
- throw new OAuth2AuthenticationException(error);
- }
- //如果parametertoekn是空则返回headertoekn
- return headerToken;
- }
-
- //如果parametertoekn不是空并且支持参数的token,则返回parameteken
- if(StringUtils.hasText(parameterToken) && isParameterEnableForRequest(request)){
- return parameterToken;
- }
-
- //都是空则返回null
- return null;
- }
-
-
- private boolean isParameterEnableForRequest(HttpServletRequest request) {
- return (this.allowFormEncodedBodyParameter && HttpMethod.POST.equals(request.getMethod()) && MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType())
- || this.allowUriQueryParameter && HttpMethod.GET.equals(request.getMethod()));
- }
-
- private boolean isParameterSupportedForRequest(HttpServletRequest request) {
- return (HttpMethod.POST.equals(request.getMethod()) && MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType())
- || HttpMethod.GET.equals(request.getMethod()));
- }
-
- private String resolveFromHeader(HttpServletRequest request) {
- String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
- //判断是否是 bearer 开头
- if(!StringUtils.startsWithIgnoreCase(authorization, "bearer")){
- return null;
- }
- Matcher matcher = authenticationPattern.matcher(authorization);
- //判断是否能匹配上
- if(!matcher.matches()){
- BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
- throw new OAuth2AuthenticationException(error);
- }
- return matcher.group("token");
- }
-
- private String resolveFromParameters(HttpServletRequest request) {
- String[] values = request.getParameterValues("access_token");
- if (values == null || values.length == 0) {
- return null;
- }
- if (values.length == 1) {
- return values[0];
- }
- BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
- throw new OAuth2AuthenticationException(error);
- }
-
- }
- //4.令牌自省
- //由于用户认证的信息都存到了 redis 里面,所以所有服务都可以通过 token 从 redis 里面拿到用户认证信息
-
-
- @Slf4j
- @RequiredArgsConstructor
- public class PigCustomOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
-
- private final OAuth2AuthorizationService authorizationService;
-
- @Override
- public OAuth2AuthenticatedPrincipal introspect(String token) {
- //根据 token 从 redis 里面拿到用户认证信息
- OAuth2Authorization authAuthorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
- if (Objects.isNull(authAuthorization)) {
- throw new InvalidBearerTokenException(token);
- }
-
- //从容器中获取到 UserDetailsService bean
- Map
userDetailsServiceMap = SpringUtil - .getBeansOfType(PigUserDetailsServiceImpl.class);
-
- Optional
optional = userDetailsServiceMap.values().stream() - .max(Comparator.comparingInt(Ordered::getOrder));
-
- UserDetails userDetails = null;
- try{
- //由于在授权端认证过程中会给 OAuth2Authorization 的 attributes 添加
,所以直接拿 - Object principal = Objects.requireNonNull(authAuthorization.getAttributes().get(Principal.class.getName()));
- UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) principal;
- Object tokenPrincipal = usernamePasswordAuthenticationToken.getPrincipal();
- //拿到授权端认证的用户信息后,可以在这里再认证一遍
- userDetails = optional.get().loadUserByUser((PigUser) tokenPrincipal);
- }catch (UsernameNotFoundException notFoundException) {
- log.warn("用户不不存在 {}", notFoundException.getLocalizedMessage());
- throw notFoundException;
- }catch (Exception ex) {
- log.error("资源服务器 introspect Token error {}", ex.getLocalizedMessage());
- }
- return (PigUser) userDetails;
- }
- }
- //5.客户端异常处理 AuthenticationException 不同细化异常处理,匿名用户访问无权限资源时的异常
-
- @RequiredArgsConstructor
- public class ResourceAuthExceptionEntryPoint implements AuthenticationEntryPoint {
- private final ObjectMapper objectMapper;
-
- @Override
- @SneakyThrows
- public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
- response.setCharacterEncoding(CommonConstants.UTF8);
- response.setContentType(CommonConstants.CONTENT_TYPE);
- R
result = new R<>(); - result.setCode(CommonConstants.FAIL);
- response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
- if (authException != null) {
- result.setMsg("error");
- result.setData(authException.getMessage());
- }
-
- // 针对令牌过期返回特殊的 424
- if (authException instanceof InvalidBearerTokenException) {
- response.setStatus(org.springframework.http.HttpStatus.FAILED_DEPENDENCY.value());
- result.setMsg("token expire");
- }
- PrintWriter printWriter = response.getWriter();
- printWriter.append(objectMapper.writeValueAsString(result));
- }
-
- }
- //6.资源端的自动配置项
-
- @RequiredArgsConstructor
- @EnableConfigurationProperties(PermitAllUrlProperties.class)
- public class PigResourceServerAutoConfiguration {
-
- /**
- * @Description: 请求认证的决策器,白名单不进行认证
- * @param urlProperties 对外暴露的接口列表
- * @Return: com.pig4cloud.pig.common.security.component.PigBearerTokenExtractorResolver
- */
- @Bean
- public PigBearerTokenExtractorResolver pigBearerTokenExtractor(PermitAllUrlProperties urlProperties) {
- return new PigBearerTokenExtractorResolver(urlProperties);
- }
-
- /**
- * 资源端认证异常
- * @param objectMapper jackson 输出对象
- * @return ResourceAuthExceptionEntryPoint
- */
- @Bean
- public ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint(ObjectMapper objectMapper) {
- return new ResourceAuthExceptionEntryPoint(objectMapper);
- }
-
- /**
- * 资源服务器toke内省处理器
- * @param authorizationService token 存储实现
- * @return TokenIntrospector
- */
- @Bean
- public OpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2AuthorizationService authorizationService) {
- return new PigCustomOpaqueTokenIntrospector(authorizationService);
- }
-
- }
- //7.资源服务器认证授权配置
-
- @Slf4j
- @EnableWebSecurity
- @RequiredArgsConstructor
- public class PigResourceServerConfiguration {
-
- private final PigBearerTokenExtractorResolver bearerTokenExtractorResolver;
-
- private final PigCustomOpaqueTokenIntrospector opaqueTokenIntrospector;
-
- protected final ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint;
-
- private final PermitAllUrlProperties permitAllUrlProperties;
-
- @Bean
- @Order(Ordered.HIGHEST_PRECEDENCE)
- SecurityFilterChain securityFilterChain(HttpSecurity http)throws Exception {
-
- http.authorizeRequests(auththorized ->
- auththorized.antMatchers(ArrayUtil.toArray(permitAllUrlProperties.getUrls(), String.class)).permitAll()
- .anyRequest().authenticated()
- ).oauth2ResourceServer(oauth ->
- oauth.bearerTokenResolver(bearerTokenExtractorResolver)
- .opaqueToken(opaqueTokenConfigurer -> opaqueTokenConfigurer.introspector(opaqueTokenIntrospector))
- .authenticationEntryPoint(resourceAuthExceptionEntryPoint)
- );
- http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
-
- return http.build();
-
- }
- }
- //8.资源服务注解
-
- @Documented
- @Inherited //@Inherited修饰的注解的@Retention是RetentionPolicy.RUNTIME,则增强了继承性,在反射中可以获取得到
- @Target({ ElementType.TYPE }) //@Target注解的作用目标,接口、类、枚举、注解
- @Retention(RetentionPolicy.RUNTIME) //注解的保留位置
- @EnableGlobalMethodSecurity(prePostEnabled = true)
- @Import({ PigResourceServerAutoConfiguration.class, PigResourceServerConfiguration.class})
- public @interface EnablePigResourceServer {
- }
-
-
- //--------------------
-
- @EnablePigResourceServer
- @EnableFeignClients(basePackages = "com.pig4cloud.pig")
- @EnableDiscoveryClient
- //@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
- @SpringBootApplication
- public class PigAdminApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(PigAdminApplication.class, args);
- }
-
- }
启动程序,先从auth里面拿到 token ,然后去用户模块测试,不添加token访问,会提示错误,添加正确的token冯文就会成功~

防止忘记

拿到token访问 upms 模块接口时,已经根据token拿到用户信息了,但是在OpaqueTokenAuthenticationProvider里面报错了!!!
问题就是写 PigUser 类时,实现了 OAuth2AuthenticatedPrincipal 接口,会重写他的getAttributes()方法,而 OpaqueTokenAuthenticationProvider 里面拿的就是 OAuth2AuthenticatedPrincipal#getAttributes() !由于我写这个类的时候没有修改,直接就是 null ,于是报错了~ 需要返回个对象,否则会报错!

- /**
- * @author QingChen
- * @Description 扩展用户认证时的信息
- * @date 2022-10-25 11:59
- * @Version 1.0
- */
-
- public class PigUser extends User implements OAuth2AuthenticatedPrincipal {
-
- ...
-
- @Override
- public Map
getAttributes() { - return new HashMap<>();
- }
-
- @Override
- public String getName() {
- return this.getUsername();
- }
- }