IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> No6-3.从零搭建spring-cloud-alibaba微服务框架,实现资源端用户认证与授权等(三,no6-3) -> 正文阅读

[Java知识库]No6-3.从零搭建spring-cloud-alibaba微服务框架,实现资源端用户认证与授权等(三,no6-3)

??代码地址与接口看总目录:【学习笔记】记录冷冷-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添加逻辑;

oauth2资源端自定义密码模式调用图:

遇到的问题:


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<String> 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 (?<token>[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<String, PigUserDetailsServiceImpl> userDetailsServiceMap = SpringUtil
                .getBeansOfType(PigUserDetailsServiceImpl.class);

        Optional<PigUserDetailsServiceImpl> optional = userDetailsServiceMap.values().stream()
                .max(Comparator.comparingInt(Ordered::getOrder));

        UserDetails userDetails = null;
        try{
            //由于在授权端认证过程中会给 OAuth2Authorization 的 attributes 添加 <Principal,upAuthenticationToken>,所以直接拿
            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<String> 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冯文就会成功~

oauth2资源端自定义密码模式调用图:

防止忘记

遇到的问题:

拿到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<String, Object> getAttributes() {
        return new HashMap<>();
    }

    @Override
    public String getName() {
        return this.getUsername();
    }
}

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-10-31 11:40:47  更:2022-10-31 11:44:20 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 4:49:36-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码