代码地址与接口看总目录:【学习笔记】记录冷冷-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();
}
}