PIG框架学习1——密码模式登录认证获取Token流程

文章目录

      • O、前言
      • 一、总流程概括:
      • 二、具体流程分析
        • PIG提供的具体流程图:
          • 鉴权请求报文示例
          • 0、网关前置处理
          • 1、客户端认证处理
          • 2、正式接受登录请求
          • 3、组装认证对象
          • 4、认证管理器进行认证(授权认证调用)
          • 5、认证成功处理器

O、前言

pig框架获取Token流程中的主要部分进行分析和整理,方便日后的学习、复习。

一、总流程概括:

说明: 对pig框架通过用户名密码的形式生成token(认证)的总流程进行分析。

1、通过浏览器或者PostMan等发送请求报文

请求直接访问网关,通过网关去进行其他微服务项目的访问

报文示例&解析:

//请求方式 请求路径&请求参数  HTTP 1.1 协议
POST /auth/oauth2/token?grant_type=password&scope=server HTTP/1.1//HTTP 请求头部中的 Host 字段,用于指定要访问的主机和端口号
Host: pig-gateway:9999 /Authorization头部请求字段 Basic模式
//客户端id、客户端密码进行Base64加密
//格式:Basic base64(clientId:clientSecret)
Authorization: Basic dGVzdDp0ZXN0       //指定请求媒体类型(数据格式)
//当前数据格式用于表单提交
Content-Type: application/x-www-form-urlencoded//指示请求体的长度(字节)
Content-Length: 32//用户名称和密码
username=admin&password=YehdBPev

2、在网关中对验证码进行校验、对前端通过AES对称算法加密的用户密码进行解密处理

3、组装客户端认证的令牌对象(此时客户端认证的令牌对象中只有客户端id、客户端密码、客户端鉴权方式,并且认证结果为false,即未鉴权)

在这里插入图片描述

4、对客户端信息进行注册,将注册的客户端信息(RegisteredClient)存储到新的客户端认证的令牌对象中,并对客户端信息账号密码(调用SpringSecurity的密码验证)进行认证,认证成功后,将认证结果设置为true(注:在进行客户端信息注册的时候,会涉及到客户端信息的缓存,如果需要注册有修改的客户的信息,需要在redis中清理对应的客户端缓存)

在这里插入图片描述

5、通过认证成功处理器,将认证成功后的客户端认证的令牌对象放入到认证的安全上下文SecurityContext中进行存储

6、组装资源拥有者密码凭证授权模式的令牌对象,可以看到其中存放着认证成功的客户端认证的令牌对象信息

在这里插入图片描述

7、对密码凭证授权模式的令牌对象进行认证授权,在密码模式获取token中,其本质是通过创建UsernamePasswordAuthenticationToken,调用spring Security的密码认证进行的,其中pig对查询用户信息(原生的userDetailservices --> pigx提供的PigxUserDetailsService)、和返回的用户信息(pigxUser)进行了扩展,支持多用户体系等。

8、认证成功后,根据授权类型(在客户端中进行配置)创建对应的令牌信息,创建访问令牌对象OAuth2AccessTokenAuthenticationToken
在这里插入图片描述

9、调用认证成功处理器,输出登录成功的日志,记录登录信息到对应的数据表中,并输出token等信息给请求调用者

二、具体流程分析

PIG提供的具体流程图:

在这里插入图片描述

鉴权请求报文示例
POST /auth/oauth2/token?grant_type=password&scope=server HTTP/1.1
Host: pig-gateway:9999
Authorization: Basic dGVzdDp0ZXN0
Content-Type: application/x-www-form-urlencoded
Content-Length: 32
username=admin&password=YehdBPev
0、网关前置处理

对于获取token的请求,经过网关会进行两个前置处理,分别是验证码校验和前端已加密的密码的解密。

①验证码校验(待学习)

涉及到的类:ValidateCodeGatewayFilter

②前端已加密的用户密码进行解密

涉及到的类:PasswordDecoderFilter

说明:

在前端登录的请求报文中,前端会通过AES对称加密算法对登录的密码进行加密传输(具体过程不展开),如上鉴权请求报文中的password,示例:

username=admin&password=YehdBPev

后端对该密码进行解密的key配置在nacos中的网关配置文件nacos/pig-gateway-dev.yml中进行定义:

在这里插入图片描述

我们可以通过在线解密加密服务对登录密文进行解密,示例如下:

在这里插入图片描述

具体后端解密流程:

进入PasswprdDecoderFilter就可以直接看到一个成员变量对象private final GatewayConfigProperties gatewayConfig; ,其是一个配置文件,内容就是我们在网关配置文件nacos/pig-gateway-dev.yml中进行定义解密的key配置是

@Data
@Component
@RefreshScope
@ConfigurationProperties("gateway")
public class GatewayConfigProperties {/*** 网关解密登录前端密码 秘钥 {@link com.pig4cloud.pigx.gateway.filter.PasswordDecoderFilter}*/public String encodeKey;}

然后我们查看其通过继承自定义网关过滤器工厂创建的网关过滤器中的内容

首先其拿到http请求内容

ServerHttpRequest request = exchange.getRequest();

如果不是登录请求,或是刷新token的类型,就放行

// 1. 不是登录请求,直接放行(通过请求路径中/oauth2/token进行判断)
if (!StrUtil.containsAnyIgnoreCase(request.getURI().getPath(), SecurityConstants.OAUTH_TOKEN_URL)) {return chain.filter(exchange);
}// 2. 刷新token类型,直接放行(通过请求参数中的授权类型判断)
String grantType = request.getQueryParams().getFirst("grant_type");
if (StrUtil.equals(SecurityConstants.REFRESH_TOKEN, grantType)) {return chain.filter(exchange);
}

然后调用isEncClient判断当前的客户端请求是否需要解密

// 3. 判断客户端是否需要解密,明文传输直接向下执行
if (!isEncClient(request)) {return chain.filter(exchange);
}

具体的过程,我通过具体的例子去进行分析,当前我登录的报文内容如下

POST /auth/oauth2/token?grant_type=password&scope=server HTTP/1.1
Authorization: Basic dGVzdDp0ZXN0(Basic Auth: test/test)
username=banana&password=Bi6KFBD0(明文:123456)
/**
* 根据请求的clientId 查询客户端配置是否是加密传输
* @param request 请求上下文
* @return true 加密传输 、 false 原文传输
*/
private boolean isEncClient(ServerHttpRequest request) {//获得请求头Basic加密的内容(即客户端的信息username/password)String header = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);//调用工具类解析客户端信息,获取客户端Id(clientId)String clientId = WebUtils.extractClientId(header).orElse(null);//从请求头中获取租户拼接区分租户的keyString tenantId = request.getHeaders().getFirst(CommonConstants.TENANT_ID);//拼接获得在redis中存储的缓存key,如1392314001162240:client_config_flag:test/*这里key对应的value信息是sys_oauth_client_details表中的additional_information(附加信息字段),保存的value是当前客户端的密码是否加密,是否开启验证码、在线数量等信息如:{"enc_flag":"1","captcha_flag":"1","online_quantity":"1"}*/String key = String.format("%s:%s:%s", StrUtil.isBlank(tenantId) ? CommonConstants.TENANT_ID_1 : tenantId,CacheConstants.CLIENT_FLAG, clientId);/*创建了一个 redisTemplate 对象,然后设置了该对象的 key 的序列化方式为 StringRedisSerializer,也就是将 key 转换为字符串类型。这样在 Redis 中保存的 key 就会以字符串的形式存储*/redisTemplate.setKeySerializer(new StringRedisSerializer());/*redisTemplate.opsForValue().get(key) 方法从 Redis 中获取 key 对应的 value,并将其赋值给变量 val*/Object val = redisTemplate.opsForValue().get(key);// 当配置不存在时,默认需要解密if (val == null) {return true;}//将当前获得的val信息转化为JSONObject//如:{ "enc_flag":"1","captcha_flag":"0"}JSONObject information = JSONUtil.parseObj(val.toString());//ENC_FLAG:0关闭加密  1:打开加密if (StrUtil.equals(EncFlagTypeEnum.NO.getType(), information.getStr(CommonConstants.ENC_FLAG))) {return false;}return true;
}

如果返回false,则表示当前密码为明文,不需要加密,则放行,返回true,则表示当前密文为密文,需要加密。

后面就是解密的过程(△),将报文重写,转为新的报文(密码是明文):

// 4. 前端加密密文解密逻辑
Class inClass = String.class;
Class outClass = String.class;
ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);// 解密生成新的报文
Mono<?> modifiedBody = serverRequest.bodyToMono(inClass).flatMap(decryptAES());BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass);
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
headers.remove(HttpHeaders.CONTENT_LENGTH);headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {ServerHttpRequest decorator = decorate(exchange, headers, outputMessage);return chain.filter(exchange.mutate().request(decorator).build());
}));

将重写后的报文中的body内容进行查看,可以发现前端加密后的password已经变成明文了

StringBuilder sb = new StringBuilder();
decorator.getBody().subscribe(buffer -> {byte[] bytes = new byte[buffer.readableByteCount()];buffer.read(bytes);DataBufferUtils.release(buffer);sb.append(new String(bytes, StandardCharsets.UTF_8));
});

在这里插入图片描述

1、客户端认证处理

涉及的类:OAuth2ClientAuthenticationFilter ProviderManager、ClientSecretAuthenticationProvider、RegisteredClientRepository(具体实现类:PigxRemoteRegisteredClientRepository)

说明:

这一步主要对前端传入的客户端信息的正确性进行一个判断,我们可以看到报文中传了一个这么个东西Basic base64(clientId:clientSecret):

Authorization: Basic dGVzdDp0ZXN0

这个就是对Client客户端信息的ClientId和clientSecret进行加密后进行传出的结果,我们可以通过在线解密工具解密一下看一下

在这里插入图片描述

流程(关键步骤结点):

1、OAuth2ClientAuthenticationFilter

组装客户端认证转换器,返回客户端认证的令牌对象信息OAuth2ClientAuthenticationToken

此时OAuth2ClientAuthenticationToken对象中的authenticatedfalse,表示还未进行认证

Authentication authenticationRequest = this.authenticationConverter.convert(request);

2、OAuth2ClientAuthenticationFilter

这里调用的是2.1中ProviderManagerauthenticate方法,对客户端进行认证

Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);

2.1、ProviderManager

这里会通过迭代器遍历provider,找到适合的、对应的provider进行处理

最终这里的provider实现类是2.2ClientSecretAuthenticationProvider,调用2.2ClientSecretAuthenticationProviderauthenticate方法

result = provider.authenticate(authentication);

2.2、ClientSecretAuthenticationProvider

ClientSecretAuthenticationProviderauthenticate方法中,调用registeredClientRepository的实现类2.3PigxRemoteRegisteredClientRepositoryfindByClientId方法

RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);

并且对客户端账号密码进行检验

String clientSecret = clientAuthentication.getCredentials().toString();
if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);
}

2.3、PigxRemoteRegisteredClientRepository类的findByClientId方法

其具体实现类PigxRemoteRegisteredClientRepository是通过在com.pig4cloud.pigx.common.security中的resources.errors.META-INF.spring.org.springframework.boot.autoconfigure.AutoConfiguration.imports进行自动配置的

3、客户端信息认证成功

Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);

返回客户端认证的令牌对象信息OAuth2ClientAuthenticationTokenauthenticationResult

此时OAuth2ClientAuthenticationToken对象中的authenticatedtrue,表示已经进行认证

之后调用OAuth2ClientAuthenticationFilterdoFilterInternal方法中的如下方法,调用认证成功的处理器

this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);

此时的this.authenticationSuccessHandler就是OAuth2ClientAuthenticationFilter,即调用OAuth2ClientAuthenticationFilteronAuthenticationSuccess方法

下面就是将客户端授权token对象信息OAuth2ClientAuthenticationToken放入到SecurityContext上下文中进行存储

private void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) {SecurityContext securityContext = SecurityContextHolder.createEmptyContext();securityContext.setAuthentication(authentication);SecurityContextHolder.setContext(securityContext);if (this.logger.isDebugEnabled()) {this.logger.debug(LogMessage.format("Set SecurityContextHolder authentication to %s",authentication.getClass().getSimpleName()));}}

可以看下客户端认证的令牌对象认证前后的区别:

在这里插入图片描述

具体关键流程findByClientId说明:

通过PigxRemoteRegisteredClientRepository类的findByClientId方法,对客户的信息进行一个查询以及注册

根据客户端id(ClientId),先调用远程接口,获取客户端的信息

SysOauthClientDetails clientDetails = RetOps.of(clientDetailsService.getClientDetailsById(clientId, SecurityConstants.FROM_IN)).getData().orElseThrow(() -> new OAuth2AuthorizationCodeRequestAuthenticationException(new OAuth2Error("客户端查询异常,请检查数据库链接"), null));

创建返回类型RegisteredClient的内部类Builder,其使用建造者模式,通过建造者模式进行创建

RegisteredClient.Builder builder = //创建一个RegisteredClient.Builder对象return new Builder(id)RegisteredClient.withId(clientDetails.getClientId())//设置builder对象中的clientId为客户端id.clientId(clientDetails.getClientId())//设置builder对象中的客户端密码为{noop}密码,即明文密码.clientSecret(SecurityConstants.NOOP + clientDetails.getClientSecret())//设置builder的鉴权方式(通过函数式方程)添加到clientAuthenticationMethods成员变量中.clientAuthenticationMethods(clientAuthenticationMethods -> {clientAuthenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);clientAuthenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);});

将客户端信息(存储在数据表)中的授权模式添加到builder对象中

// 授权模式
Arrays.stream(clientDetails.getAuthorizedGrantTypes()).forEach(grant -> builder.authorizationGrantType(new AuthorizationGrantType(grant)));

将客户端信息中的回调信息添加到builder对象中

Optional.ofNullable(clientDetails.getWebServerRedirectUri()).ifPresent(redirectUri -> Arrays  .stream(redirectUri.split(StrUtil.COMMA)).filter(StrUtil::isNotBlank).forEach(builder::redirectUri));

将客户端信息中的授权范围添加到builder对象中

// scope
Optional.ofNullable(clientDetails.getScope()).ifPresent(scope -> Arrays.stream(scope.split(StrUtil.COMMA)).filter(StrUtil::isNotBlank).forEach(builder::scope));

将客户端信息中的扩展配置添加到builder对象中

// 注入扩展配置
Optional.ofNullable(clientDetails.getAdditionalInformation()).ifPresent(ext -> {Map map = JSONUtil.parseObj(ext).toBean(Map.class);builder.clientSettings(ClientSettings.withSettings(map).requireProofKey(false).requireAuthorizationConsent(!BooleanUtil.toBoolean(clientDetails.getAutoapprove())).build());});

创建通过builder创建RegisteredClient对象,并封装tokensetting的内容(一些token的时效等信息)

在这里插入图片描述

return builder.tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.REFERENCE).accessTokenTimeToLive(Duration.ofSeconds(Optional.ofNullable(clientDetails.getAccessTokenValidity()).orElse(accessTokenValiditySeconds))).refreshTokenTimeToLive(Duration.ofSeconds(Optional.ofNullable(clientDetails.getRefreshTokenValidity()).orElse(refreshTokenValiditySeconds))).build()).build();
2、正式接受登录请求

**涉及对象:**OAuth2TokenEndpointFilter

说明:

OAuth2TokenEndpointFilter 会接收通过上文 OAuth2ClientAuthenticationFilter 客户端认证的请求

流程:

① OAuth2TokenEndpointFilter

try {//获取当前请求参数中的授权模式/*即报文中的POST /auth/oauth2/token?grant_type=password&scope=server HTTP/1.1的grant_type=password*/String[] grantTypes = request.getParameterValues(OAuth2ParameterNames.GRANT_TYPE);//校验当前的授权模式,不存在或为空抛出异常if (grantTypes == null || grantTypes.length != 1) {throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.GRANT_TYPE);}//组装登录认证对象:详情见3Authentication authorizationGrantAuthentication = this.authenticationConverter.convert(request);//登录认证对象为null 抛出异常if (authorizationGrantAuthentication == null) {throwError(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, OAuth2ParameterNames.GRANT_TYPE);}//登录认证对象是AbstractAuthenticationToken的实例//将其转换为 AbstractAuthenticationToken 类型,并设置其详细信息/*remoteAddresssessionId等*/if (authorizationGrantAuthentication instanceof AbstractAuthenticationToken) {((AbstractAuthenticationToken) authorizationGrantAuthentication).setDetails(this.authenticationDetailsSource.buildDetails(request));}//认证管理器进行认证,详情见4OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =(OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication);//认证成功处理,详情见5this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, accessTokenAuthentication);} catch (OAuth2AuthenticationException ex) {SecurityContextHolder.clearContext();if (this.logger.isTraceEnabled()) {this.logger.trace(LogMessage.format("Token request failed: %s", ex.getError()), ex);}//认证失败处理this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);}
}
3、组装认证对象

AuthenticationConverter: 会根据请求中的参数和授权类型组装成对应的授权认证对象

在这里插入图片描述

登录认证对象:

在这里插入图片描述

在这里插入图片描述

组装登录认证对象方法解析:Authentication authorizationGrantAuthentication = this.authenticationConverter.convert(request);

组装登录认证对象的cover方法的pigx自己定义的实现类(自定义模式认证转换器)OAuth2ResourceOwnerBaseAuthenticationConverter

public Authentication convert(HttpServletRequest request) {// grant_type (REQUIRED)String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);//判断当前认证转换器是否支持该授权类型grantType,详情见3.1if (!support(grantType)) {return null;}/*获取OAuth2 端点工具获取请求参数,如:username:用户名    	password:密码(以在网关前置中解密)grant_type:授权类型scope:授权范围*/MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);// scope (OPTIONAL)//从请求参数parameters中获取名为 "scope" 的第一个值String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);//判断是否有授权范围,没有抛出异常if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE,OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);}//处理多个授权范围的情况(“ ”分割),存储为Set集合中Set<String> requestedScopes = null;if (StringUtils.hasText(scope)) {requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));}// 校验个性化参数//调用当前转换器的checkParams方法,详情见3.2checkParams(request);// 通过SecurityContextHolder获取当前已经认证的客户端信息(在客户端认证成功后已经将客户端信息放入到SecurityContext中)Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();//客户端信息为null,抛出响应的异常if (clientPrincipal == null) {OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ErrorCodes.INVALID_CLIENT,OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);}// 扩展信息//过滤grant_type和scope参数内容//以键值对的方式将剩下的参数存储到additionalParameters中Map<String, Object> additionalParameters = parameters.entrySet().stream().filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE)&& !e.getKey().equals(OAuth2ParameterNames.SCOPE)).collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));// 创建资源拥有者密码凭证授权模式的令牌对象,详情3.3return buildToken(clientPrincipal, requestedScopes, additionalParameters);}}

返回内容:

在这里插入图片描述

3.1 !support(grantType)

该方法位于自定义认证模式转化器的类OAuth2ResourceOwnerBaseAuthenticationConverter

该类是一个抽象类,并且其中包含一个抽象方法public abstract boolean support(String grantType);用于检测当前授权类型granType是否有对应支持的转换器

DelegatingAuthenticationConverter类中:

方法public Authentication convert(HttpServletRequest request)用于遍历所有的认证转换器

构造器public DelegatingAuthenticationConverter(List<AuthenticationConverter> converters)用于添加认证转换器保存到当前类中的converters成员变量中

public final class DelegatingAuthenticationConverter implements AuthenticationConverter {private final List<AuthenticationConverter> converters;/*** Constructs a {@code DelegatingAuthenticationConverter} using the provided parameters.** @param converters a {@code List} of {@link AuthenticationConverter}(s)*/public DelegatingAuthenticationConverter(List<AuthenticationConverter> converters) {Assert.notEmpty(converters, "converters cannot be empty");this.converters = Collections.unmodifiableList(new LinkedList<>(converters));}@Nullable@Overridepublic Authentication convert(HttpServletRequest request) {Assert.notNull(request, "request cannot be null");//遍历当前所有认证转换器,组装符合当前认证类型的转换器for (AuthenticationConverter converter : this.converters) {Authentication authentication = converter.convert(request);if (authentication != null) {return authentication;}}return null;}
}

在遍历转化器的时候,会调用转换器的covert方法:Authentication authentication = converter.convert(request);

这里pigx为我们提供了自定义模式的认证转换器OAuth2ResourceOwnerBaseAuthenticationConverter,其是一个抽象类,其具体的实现类有:

  • OAuth2ResourceOwnerDingTalkAuthenticationConverter 钉钉登录转换器
  • OAuth2ResourceOwnerPasswordAuthenticationConverter密码认证转换器
  • OAuth2ResourceOwnerSSOAuthenticationConverter 三方接入登录转换器
  • OAuth2ResourceOwnerSmsAuthenticationConverter 短信登录转换器

在调用covert的方法的时候,是调用父类OAuth2ResourceOwnerBaseAuthenticationConverter中的covert方法,而调用support(grantType)方法的时候,是其中具体实现类的方法

当然其在遍历转换器的时候也是遍历的具体实现类,只不过调用的covert方法是在抽象父类中统一进行处理的

在这里插入图片描述

3.2 检验参数 checkParams(request);

调用的是当前具体实现类转化器中的checkParams方法,这里以密码模式进行分析

可以看到,在密码模式下,其中主要对用户名和密码的参数进行了验证

@Override
public void checkParams(HttpServletRequest request) {/*获取OAuth2 端点工具获取请求参数,如:username:用户名    	password:密码(以在网关前置中解密)grant_type:授权类型scope:授权范围*/MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);// username (REQUIRED)//获得第一个username的值String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);//判断当前的username是否为空为null || 判断是否请求中携带username入参//不满足要求否则抛出异常if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.USERNAME,OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);}// password (REQUIRED)//同理用户名判断//判断当前的密码是否为空为null || 判断是否请求中携带密码password入参String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.PASSWORD,OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);}
}

3.3 创建资源拥有者密码凭证授权模式的令牌对象: buildToken(clientPrincipal, requestedScopes, additionalParameters);

调用的是当前具体实现类转化器中的buildToken方法,这里以密码模式OAuth2ResourceOwnerPasswordAuthenticationConverter进行分析

@Override
public OAuth2ResourceOwnerPasswordAuthenticationToken buildToken(Authentication clientPrincipal,Set requestedScopes, Map additionalParameters) {return new OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType.PASSWORD, clientPrincipal,requestedScopes, additionalParameters);
}

调用OAuth2ResourceOwnerPasswordAuthenticationToken构造器

OAuth2ResourceOwnerPasswordAuthenticationToken构造器调用已用父类OAuth2ResourceOwnerBaseAuthenticationToken构造器

public class OAuth2ResourceOwnerPasswordAuthenticationToken extends OAuth2ResourceOwnerBaseAuthenticationToken {public OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType authorizationGrantType,Authentication clientPrincipal, Set<String> scopes, Map<String, Object> additionalParameters) {super(authorizationGrantType, clientPrincipal, scopes, additionalParameters);}
}

OAuth2ResourceOwnerBaseAuthenticationToken构造器:

public OAuth2ResourceOwnerBaseAuthenticationToken(AuthorizationGrantType authorizationGrantType,Authentication clientPrincipal, @Nullable Set<String> scopes,@Nullable Map<String, Object> additionalParameters) {//调用父类`AbstractAuthenticationToken`构造器	super(Collections.emptyList());Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");//将请求的参数信息保存到当前对象的成员变量中this.authorizationGrantType = authorizationGrantType;this.clientPrincipal = clientPrincipal;this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());this.additionalParameters = Collections.unmodifiableMap(additionalParameters != null ? new HashMap<>(additionalParameters) : Collections.emptyMap());}

调用父类AbstractAuthenticationToken构造器

传的是Collections.emptyList(),因此调用的是最终的this.authorities = Collections.unmodifiableList(new ArrayList(authorities))

this.authorities = Collections.unmodifiableList(new ArrayList(Collections.emptyList()))

public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {if (authorities == null) {this.authorities = AuthorityUtils.NO_AUTHORITIES;} else {Iterator var2 = authorities.iterator();while(var2.hasNext()) {GrantedAuthority a = (GrantedAuthority)var2.next();Assert.notNull(a, "Authorities collection cannot contain any null elements");}this.authorities = Collections.unmodifiableList(new ArrayList(authorities));}
}
4、认证管理器进行认证(授权认证调用)

在调用OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication);

首先是调用ProviderManager类中的public Authentication authenticate(Authentication authentication) 方法,该方法中调用了

result = provider.authenticate(authentication);

这里的proder是pigx提供的处理自定义授权类OAuth2ResourceOwnerBaseAuthenticationProvider ,即调用OAuth2ResourceOwnerBaseAuthenticationProviderauthenticate方法,方法内容如下所示:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {//获取登录认证对象信息//对于密码登录这里是OAuth2ResourceOwnerPasswordAuthenticationTokenT resouceOwnerBaseAuthentication = (T) authentication;//获取经过身份验证的客户端信息clientPrincipal,详情见4.1OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(resouceOwnerBaseAuthentication);//从客户端登录认证对象信息中获取注册的客户端信息RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();//检查注册的客户端信息,这里主要对其授权类型进行一个判断,详情见4.2checkClient(registeredClient);//处理登录认证对象信息中的授权范围,存储到authorizedScopes中Set<String> authorizedScopes;// Default to configured scopesif (!CollectionUtils.isEmpty(resouceOwnerBaseAuthentication.getScopes())) {for (String requestedScope : resouceOwnerBaseAuthentication.getScopes()) {if (!registeredClient.getScopes().contains(requestedScope)) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);}}authorizedScopes = new LinkedHashSet<>(resouceOwnerBaseAuthentication.getScopes());}else {throw new ScopeException(OAuth2ErrorCodesExpand.SCOPE_IS_EMPTY);}//从登录认证对象信息中获取其他的入参信息(username、password)放入到reParameters中Map<String, Object> reqParameters = resouceOwnerBaseAuthentication.getAdditionalParameters();try {//生成UsernamePasswordAuthenticationToken,详情见4.3//目的是后面通过Spring security对其进行验证//UsernamePasswordAuthenticationToken属于Spring securityUsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = buildToken(reqParameters);//打印获得的UsernamePasswordAuthenticationTokenLOGGER.debug("got usernamePasswordAuthenticationToken=" + usernamePasswordAuthenticationToken);//交由Spring security进行验证,详情见4.4/*认证通过后,会返回用户信息和权限信息principal:用户信息credentials:null   认证前村密码明文的authorities:权限信息details: null     authenticated:true   表示认证通过*/Authentication usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);//从客户端登录认证对象信息获取可同时在线数量的信息Object onlineQuantity = registeredClient.getClientSettings().getSettings().get(CommonConstants.ONLINE_QUANTITY);// 没有设置并发控制走原有逻辑生成 || 设置同时在线为 trueif (Objects.isNull(onlineQuantity) || BooleanUtil.toBooleanObject((String) onlineQuantity)) {//构建请求令牌、刷新令牌 详情见4.6return generatAuthenticationToken(resouceOwnerBaseAuthentication, clientPrincipal, registeredClient,authorizedScopes, usernamePasswordAuthentication);}// 不允许同时在线,删除原有username 关联的所有tokenPigxRedisOAuth2AuthorizationService redisOAuth2AuthorizationService = (PigxRedisOAuth2AuthorizationService) this.authorizationService;//详情见4.5redisOAuth2AuthorizationService.removeByUsername(usernamePasswordAuthentication);//构建请求令牌return generatAuthenticationToken(resouceOwnerBaseAuthentication, clientPrincipal, registeredClient,authorizedScopes, usernamePasswordAuthentication);}catch (Exception ex) {throw oAuth2AuthenticationException(authentication, (AuthenticationException) ex);}}

4.1 获取经过身份验证的客户端,否则抛出无效客户端

其调用的是OAuth2ResourceOwnerBaseAuthenticationProvider方法中的getAuthenticatedClientElseThrowInvalidClient方法

private OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {//声明一个客户端认证的身份验证令牌//OAuth2ClientAuthenticationToken 封装了客户端的身份信息和授权服务器返回的访问令牌等相关信息,以便在应用程序中进行处理和使用OAuth2ClientAuthenticationToken clientPrincipal = null;//通过isAssignableFrom方法判断authentication.getPrincipal().getClass()是否是OAuth2ClientAuthenticationToken.class类型//如果是将其值赋值给clientPrincipalif (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();}//如果clientPrincipal有值并且已经认证过,那么就返回clientPrincipalif (clientPrincipal != null && clientPrincipal.isAuthenticated()) {return clientPrincipal;}//否则抛出异常throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}

getPrincipal()方法,用于获取用户名,其调用返回的变量clientPrincipal的值:

/**
* 获取用户名
*/
@Override
public Object getPrincipal() {return this.clientPrincipal;
}

4.2验证客户端信息checkClient(registeredClient);

其调用的是处理自定义授权的抽象类OAuth2ResourceOwnerBaseAuthenticationProvider中的抽象方法

public abstract void checkClient(RegisteredClient registeredClient);

我们当前是用户名密码授权,因此执行该方法的具体实现类是OAuth2ResourceOwnerPasswordAuthenticationProvider中的

@Override
public void checkClient(RegisteredClient registeredClient) {assert registeredClient != null;//判断当前注册的客户端信息的授权类型是否是密码类型//若不是则抛出错误异常if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);}
}

4.3 生成UsernamePasswordAuthenticationToken 账号密码认证令牌对象

UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = buildToken(reqParameters);

在这里调用的buildTokenOAuth2ResourceOwnerBaseAuthenticationProvider处理自定义授权抽象类,其中的UsernamePasswordAuthenticationToken是一个抽象方法

public abstract UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters);

由于当前是用户密码授权,因此其具体处理用户名密码授权的实现类是OAuth2ResourceOwnerPasswordAuthenticationProvider,其中实现的方法如下:

从登录认证对象信息中获取的其他入参信息中获取Username和Password信息,分别赋值给局部变量username和password

@Override
public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {String username = (String) reqParameters.get(OAuth2ParameterNames.USERNAME);String password = (String) reqParameters.get(OAuth2ParameterNames.PASSWORD);return new UsernamePasswordAuthenticationToken(username, password);
}

return new UsernamePasswordAuthenticationToken(username, password);调用UsernamePasswordAuthenticationToken的构造器

public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {super((Collection)null);this.principal = principal;this.credentials = credentials;this.setAuthenticated(false);
}

super((Collection)null)调用父类构造器

public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {if (authorities == null) {this.authorities = AuthorityUtils.NO_AUTHORITIES;} else {Iterator var2 = authorities.iterator();while(var2.hasNext()) {GrantedAuthority a = (GrantedAuthority)var2.next();Assert.notNull(a, "Authorities collection cannot contain any null elements");}this.authorities = Collections.unmodifiableList(new ArrayList(authorities));}
}

生成结果:

在这里插入图片描述

4.4 将UsernamePasswordAuthenticationToken交给Spring security进行验证

Authentication usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);

当前authenticationManagerProviderManager,调用其中的方法

其中providerAbstractUserDetailsAuthenticationProvider的具体实现类PigxDaoAuthenticationProvider

 result = provider.authenticate(authentication);

首先是调用AbstractUserDetailsAuthenticationProviderauthenticate方法

public Authentication authenticate(Authentication authentication) throws AuthenticationException {Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");});String username = this.determineUsername(authentication);boolean cacheWasUsed = true;UserDetails user = this.userCache.getUserFromCache(username);if (user == null) {cacheWasUsed = false;try {user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);} catch (UsernameNotFoundException var6) {this.logger.debug("Failed to find user '" + username + "'");if (!this.hideUserNotFoundExceptions) {throw var6;}throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));}Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");}try {this.preAuthenticationChecks.check(user);this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);} catch (AuthenticationException var7) {if (!cacheWasUsed) {throw var7;}cacheWasUsed = false;//详情见4.4.1user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);this.preAuthenticationChecks.check(user);//详情见4.4.2this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);}this.postAuthenticationChecks.check(user);if (!cacheWasUsed) {this.userCache.putUserInCache(user);}Object principalToReturn = user;if (this.forcePrincipalAsString) {principalToReturn = user.getUsername();}return this.createSuccessAuthentication(principalToReturn, authentication, user);
}

4.4.1 在上述的authenticate方法中通过具体实现类PigxDaoAuthenticationProvider调用retrieveUser的方法,通过userDetailservices查询用户信息(其中使用的是pigx自己扩展提供的userDetailservices实现类)

user = this. (username, (UsernamePasswordAuthenticationToken)authentication);

其调用实现类PigxDaoAuthenticationProviderretrieveUser方法,来获取用户信息(支持多用户体系)

@SneakyThrows
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) {prepareTimingAttackProtection();//获取授权类型String grantType = WebUtils.getRequest().getParameter(OAuth2ParameterNames.GRANT_TYPE);//获取客户端idString clientId = WebUtils.getRequest().getParameter(OAuth2ParameterNames.CLIENT_ID);//如果客户端id无法从请求中获取//就调用如下方法从basic authentication中去获取客户端id信息if (StrUtil.isBlank(clientId)) {clientId = basicConvert.convert(WebUtils.getRequest()).getName();}//SpringUtil 工具类获取所有类型为 PigxUserDetailsService 的 Bean//存储在userDetailsServiceMap中Map<String, PigxUserDetailsService> userDetailsServiceMap = SpringUtil.getBeansOfType(PigxUserDetailsService.class);//将clientId的值赋值给finalClientIdString finalClientId = clientId;//获取到支持当前授权类型grantType的PigxUserDetailsService//如果有多个就取order最大的PigxUserDetailsServiceOptional<PigxUserDetailsService> optional = userDetailsServiceMap.values().stream().filter(service -> service.support(finalClientId, grantType)).max(Comparator.comparingInt(Ordered::getOrder));//如果对应的PigxUserDetailsService不存在则抛出异常if (!optional.isPresent()) {throw new InternalAuthenticationServiceException("UserDetailsService error , not register");}try {//根据上面获取的PigxUserDetailsService去获取相信的用户信息//当前获取到的PigxUserDetailsService的实现类是PigxDefaultUserDetailsServiceImpl,详情见4.4.1.1UserDetails loadedUser = optional.get().loadUserByUsername(username);//获取用户详情信息为空则抛出异常if (loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");}return loadedUser;}catch (UsernameNotFoundException ex) {mitigateAgainstTimingAttack(authentication);throw ex;}catch (InternalAuthenticationServiceException ex) {throw ex;}catch (Exception ex) {throw new InternalAuthenticationServiceException(ex.getMessage(), ex);}
}

4.4.1.1 PigxDefaultUserDetailsServiceImplpublic UserDetails loadUserByUsername(String username)方法

/**
* 用户密码登录
* @param username 用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
@SneakyThrows
public UserDetails loadUserByUsername(String username) {//获取用户信息缓存 实例值:1392314001162240:user_details,详情见4.4.1.1.1Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);//如果有缓存直接从缓存中获取if (cache != null && cache.get(username) != null) {return cache.get(username, PigxUser.class);}//通过upms的远程接口,通过username去获取用户的名称R<UserInfo> result = remoteUserService.info(username, SecurityConstants.FROM_IN);//组装UserDetials类UserDetails userDetails = getUserDetails(result);//加入缓存, 详情见4.4.1.1.2cache.put(username, userDetails);//返回扩展厚的用户信息return userDetails;
}

4.4.1.1.1 对于cacheManager的实现类的说明:

其实现类是RedisAutoCacheManager

在这里插入图片描述

com.pig4cloud.pigx.common.data.cach下的RedisCacheAutoConfiguration配置类中,声明RedisCacheAutoConfiguration的Bean对象

并在org.springframework.boot.autoconfigure.AutoConfiguration.imports自动配置RedisCacheAutoConfiguration配置类

4.4.1.1.2 封装用户信息为UserDetails

default UserDetails getUserDetails(R<UserInfo> result) {// @formatter:off//通过RetOps进行远程调用的判空处理return RetOps.of(result).getData()//调用convertUserDetails方法对远程调用获取的UserInfo进行处理.map(this::convertUserDetails).orElseThrow(() -> new UsernameNotFoundException("用户不存在"));// @formatter:on
}default UserDetails convertUserDetails(UserInfo info) {Set<String> dbAuthsSet = new HashSet<>();if (ArrayUtil.isNotEmpty(info.getRoles())) {// 获取角色(ROLE_ + roleId)加入到dbAuthsSetArrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE + roleId));// 获取资源(权限)加入到dbAuthsSetdbAuthsSet.addAll(Arrays.asList(info.getPermissions()));}//调用AuthorityUtils的createAuthorityList方法//将dbAuthsSet中的信息存入到authorities中,类型是SimpleGrantedAuthorityCollection<? extends GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(dbAuthsSet.toArray(new String[0]));//获得SysUser 用户信息SysUser user = info.getSysUser();// 构造security用户(PigxUser)return new PigxUser(user.getUserId(), user.getUsername(), info.getDepts().stream().map(SysDept::getDeptId).collect(Collectors.toList()), user.getPhone(), user.getAvatar(),user.getNickname(), user.getName(), user.getEmail(), user.getTenantId(),SecurityConstants.BCRYPT + user.getPassword(), true, true, UserTypeEnum.TOB.getStatus(), user.getJobNumber(),user.getJobId(), true,!CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities);
}

4.4.2之后,在authenticate方法中通过具体实现类PigxDaoAuthenticationProvider调用additionalAuthenticationChecks的方法,检查用户信息包括密码、状态:

this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);

其具体实现方法如下:

@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {// app 和 code 模式不用校验密码String grantType = WebUtils.getRequest().getParameter(OAuth2ParameterNames.GRANT_TYPE);if (StrUtil.equals(SecurityConstants.APP, grantType) ||StrUtil.equals(SecurityConstants.DING_TALK_CODE, grantType)|| StrUtil.equals(SecurityConstants.THIRD_SSO, grantType)) {return;}//当前密码为null 抛出异常if (authentication.getCredentials() == null) {this.logger.debug("Failed to authenticate since no credentials provided");throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));}//获取当前密码给局部变量presentedPasswordString presentedPassword = authentication.getCredentials().toString();//调用spring security进行账号密码匹配,详细信息见4.4.2.1if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {this.logger.debug("Failed to authenticate since password does not match stored value");throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));}
}

4.4.2.1 密码匹配this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())

我们可以在PasswordEncoderFactories中看懂各种加密类型

public final class PasswordEncoderFactories {private PasswordEncoderFactories() {}public static PasswordEncoder createDelegatingPasswordEncoder() {String encodingId = "bcrypt";Map<String, PasswordEncoder> encoders = new HashMap();encoders.put(encodingId, new BCryptPasswordEncoder());encoders.put("ldap", new LdapShaPasswordEncoder());encoders.put("MD4", new Md4PasswordEncoder());encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));encoders.put("noop", NoOpPasswordEncoder.getInstance());encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());encoders.put("scrypt", new SCryptPasswordEncoder());encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));encoders.put("sha256", new StandardPasswordEncoder());encoders.put("argon2", new Argon2PasswordEncoder());return new DelegatingPasswordEncoder(encodingId, encoders);}
}

为了能够匹配加密类型,需要在UserDetails配置对应的密码加密类型,PasswordEncoder 会自动根据特征码匹配对应的加密算法

默认支持加密方式如下:

  • {noop}密码明文
  • {加密特征码}密码密文

具体的UserDetails配置就是在之前查询用户信息,并且封装UserDetails的时候,将如下的SecurityConstants.BCRYPT修改为对一个的加密方式即可

 return new PigxUser(user.getUserId(), user.getUsername(), info.getDepts().stream().map(SysDept::getDeptId).collect(Collectors.toList()), user.getPhone(), user.getAvatar(),user.getNickname(), user.getName(), user.getEmail(), user.getTenantId(),SecurityConstants.BCRYPT + user.getPassword(), true, true, UserTypeEnum.TOB.getStatus(), user.getJobNumber(),user.getJobId(), true,!CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities);

4.5 不允许同时在线,删除原有username 关联的所有toke

/*** 扩展方法根据 username 查询是否存在存储的* @param authentication* @return*/
public void removeByUsername(Authentication authentication) {// 根据 username查询对应access-tokenString authenticationName = authentication.getName();// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxxString tokenUsernameKey = String.format("%s::%s::%s::%s::*", tenantKeyStrResolver.key(), AUTHORIZATION,SecurityConstants.DETAILS_USERNAME, authenticationName);Set<String> keys = redisTemplate.keys(tokenUsernameKey);if (CollUtil.isEmpty(keys)) {return;}List<Object> tokenList = redisTemplate.opsForValue().multiGet(keys);for (Object token : tokenList) {// 根据token 查询存储的 OAuth2AuthorizationOAuth2Authorization authorization = this.findByToken((String) token, OAuth2TokenType.ACCESS_TOKEN);// 根据 OAuth2Authorization 删除相关令牌this.remove(authorization);}}

token示例:

在这里插入图片描述

4.6 构建请求令牌generatAuthenticationToken(resouceOwnerBaseAuthentication, clientPrincipal, registeredClient, authorizedScopes, usernamePasswordAuthentication);

/*** 生成新的令牌* @param resouceOwnerBaseAuthentication* @param clientPrincipal* @param registeredClient* @param authorizedScopes* @param usernamePasswordAuthentication* @return OAuth2AccessTokenAuthenticationToken
*/
@NotNull
private OAuth2AccessTokenAuthenticationToken generatAuthenticationToken(T resouceOwnerBaseAuthentication,OAuth2ClientAuthenticationToken clientPrincipal, RegisteredClient registeredClient,Set<String> authorizedScopes, Authentication usernamePasswordAuthentication) {// @formatter:offDefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder().registeredClient(registeredClient).principal(usernamePasswordAuthentication).authorizationServerContext(AuthorizationServerContextHolder.getContext()).authorizedScopes(authorizedScopes).authorizationGrantType(resouceOwnerBaseAuthentication.getAuthorizationGrantType()).authorizationGrant(resouceOwnerBaseAuthentication);// @formatter:onOAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient).principalName(usernamePasswordAuthentication.getName()).authorizationGrantType(resouceOwnerBaseAuthentication.getAuthorizationGrantType())// 0.4.0 新增的方法.authorizedScopes(authorizedScopes);// ----- Access token -----OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);if (generatedAccessToken == null) {OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,"The token generator failed to generate the access token.", ERROR_URI);throw new OAuth2AuthenticationException(error);}OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());if (generatedAccessToken instanceof ClaimAccessor) {authorizationBuilder.id(accessToken.getTokenValue()).token(accessToken,(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME,((ClaimAccessor) generatedAccessToken).getClaims()))// 0.4.0 新增的方法.authorizedScopes(authorizedScopes).attribute(Principal.class.getName(), usernamePasswordAuthentication);}else {authorizationBuilder.id(accessToken.getTokenValue()).accessToken(accessToken);}// ----- Refresh token -----OAuth2RefreshToken refreshToken = null;if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&// Do not issue refresh token to public client!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {if (this.refreshTokenGenerator != null) {Instant issuedAt = Instant.now();Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getRefreshTokenTimeToLive());refreshToken = new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt);}else {tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,"The token generator failed to generate the refresh token.", ERROR_URI);throw new OAuth2AuthenticationException(error);}refreshToken = (OAuth2RefreshToken) generatedRefreshToken;}authorizationBuilder.refreshToken(refreshToken);}OAuth2Authorization authorization = authorizationBuilder.build();//存储令牌(即令牌持久化)  详情见4.6.1this.authorizationService.save(authorization);LOGGER.debug("returning OAuth2AccessTokenAuthenticationToken");return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken,Objects.requireNonNull(authorization.getAccessToken().getClaims()));
}

可以看到上面都是调用this.tokenGenerator.generate(tokenContext)进行token的生成的

其首先调用的是DelegatingOAuth2TokenGeneratorgenerate方法

this.tokenGenerators有两个值:

@Nullable
@Override
public OAuth2Token generate(OAuth2TokenContext context) {for (OAuth2TokenGenerator<OAuth2Token> tokenGenerator : this.tokenGenerators) {OAuth2Token token = tokenGenerator.generate(context);if (token != null) {return token;}}return null;
}

其实现类用的是pigx提供的CustomeOAuth2AccessTokenGenerator个性化token生成

@Nullable
@Override
public OAuth2AccessToken generate(OAuth2TokenContext context) {if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) || !OAuth2TokenFormat.REFERENCE.equals(context.getRegisteredClient().getTokenSettings().getAccessTokenFormat())) {return null;}String issuer = null;if (context.getAuthorizationServerContext() != null) {issuer = context.getAuthorizationServerContext().getIssuer();}RegisteredClient registeredClient = context.getRegisteredClient();Instant issuedAt = Instant.now();Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());// @formatter:offOAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();if (StringUtils.hasText(issuer)) {claimsBuilder.issuer(issuer);}claimsBuilder.subject(context.getPrincipal().getName()).audience(Collections.singletonList(registeredClient.getClientId())).issuedAt(issuedAt).expiresAt(expiresAt).notBefore(issuedAt).id(UUID.randomUUID().toString());if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());}// @formatter:onif (this.accessTokenCustomizer != null) {// @formatter:offOAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder).registeredClient(context.getRegisteredClient()).principal(context.getPrincipal()).authorizationServerContext(context.getAuthorizationServerContext()).authorizedScopes(context.getAuthorizedScopes()).tokenType(context.getTokenType()).authorizationGrantType(context.getAuthorizationGrantType());if (context.getAuthorization() != null) {accessTokenContextBuilder.authorization(context.getAuthorization());}if (context.getAuthorizationGrant() != null) {accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant());}// @formatter:onOAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build();this.accessTokenCustomizer.customize(accessTokenContext);}OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();//将 UUID.randomUUID().toString()作为token返回return new OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER, UUID.randomUUID().toString(),accessTokenClaimsSet.getIssuedAt(), accessTokenClaimsSet.getExpiresAt(), 			 context.getAuthorizedScopes(),accessTokenClaimsSet.getClaims());
}

CustomeOAuth2TokenCustomizer可以看到个性化的内容,即获取token后返回的内容配置:

@Override
public void customize(OAuth2TokenClaimsContext context) {OAuth2TokenClaimsSet.Builder claims = context.getClaims();claims.claim(SecurityConstants.DETAILS_LICENSE, SecurityConstants.PIGX_LICENSE);String clientId = context.getAuthorizationGrant().getName();claims.claim(SecurityConstants.CLIENT_ID, clientId);claims.claim(SecurityConstants.ACTIVE, Boolean.TRUE);// 客户端模式不返回具体用户信息if (SecurityConstants.CLIENT_CREDENTIALS.equals(context.getAuthorizationGrantType().getValue())) {return;}PigxUser pigxUser = (PigxUser) context.getPrincipal().getPrincipal();claims.claim(SecurityConstants.DETAILS_USER_ID, pigxUser.getId());claims.claim(SecurityConstants.DETAILS_USERNAME, pigxUser.getUsername());
}

4.6.1:存储令牌(即令牌持久化) this.authorizationService.save(authorization);

这里采用PigxRedisOAuth2AuthorizationService进行令牌持久化,Spring securty Oauth2自带的是内存和jdbc持久化

可以看一下存储的格式是这样的:扩展记录 access-token 、username 的关系 1::token::username::admin::xxx

@Override
public void save(OAuth2Authorization authorization) {Assert.notNull(authorization, "authorization cannot be null");if (isState(authorization)) {String token = authorization.getAttribute("state");redisTemplate.setValueSerializer(RedisSerializer.java());redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.STATE, token), authorization, TIMEOUT,TimeUnit.MINUTES);}if (isCode(authorization)) {OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization.getToken(OAuth2AuthorizationCode.class);OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();long between = ChronoUnit.MINUTES.between(authorizationCodeToken.getIssuedAt(),authorizationCodeToken.getExpiresAt());redisTemplate.setValueSerializer(RedisSerializer.java());redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()),authorization, between, TimeUnit.MINUTES);}if (isRefreshToken(authorization)) {OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();long between = ChronoUnit.SECONDS.between(refreshToken.getIssuedAt(), refreshToken.getExpiresAt());redisTemplate.setValueSerializer(RedisSerializer.java());redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()),authorization, between, TimeUnit.SECONDS);}if (isAccessToken(authorization)) {OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());redisTemplate.setValueSerializer(RedisSerializer.java());redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()),authorization, between, TimeUnit.SECONDS);// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxxString tokenUsername = String.format("%s::%s::%s::%s::%s", tenantKeyStrResolver.key(), AUTHORIZATION,SecurityConstants.DETAILS_USERNAME, authorization.getPrincipalName(), accessToken.getTokenValue());redisTemplate.opsForValue().set(tokenUsername, accessToken.getTokenValue(), between, TimeUnit.SECONDS);}
}
5、认证成功处理器

this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, accessTokenAuthentication);

@SneakyThrows
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) {// 写入登录成功的日志OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;Map<String, Object> map = accessTokenAuthentication.getAdditionalParameters();if (MapUtil.isNotEmpty(map)) {//记录登录成功事件 主要有1、日志输出 2、数据表存储,详情见5.1sendSuccessEventLog(request, accessTokenAuthentication, map);}// 清除账号历史锁定次数clearLoginFailureTimes(map);// 输出tokensendAccessTokenResponse(response, authentication);
}

5.1 记录登录成功事件

sendSuccessEventLog(request, accessTokenAuthentication, map);

private void sendSuccessEventLog(HttpServletRequest request,OAuth2AccessTokenAuthenticationToken accessTokenAuthentication, Map<String, Object> map) {// 发送异步日志事件SecurityContext context = SecurityContextHolder.createEmptyContext();context.setAuthentication(accessTokenAuthentication);SecurityContextHolder.setContext(context);SysLogDTO logVo = SysLogUtils.getSysLog();logVo.setTitle("登录成功");logVo.setLogType(LogTypeEnum.NORMAL.getType());String startTimeStr = request.getHeader(CommonConstants.REQUEST_START_TIME);if (StrUtil.isNotBlank(startTimeStr)) {Long startTime = Long.parseLong(startTimeStr);Long endTime = System.currentTimeMillis();logVo.setTime(endTime - startTime);}logVo.setServiceId(accessTokenAuthentication.getRegisteredClient().getClientId());logVo.setCreateBy(MapUtil.getStr(map, SecurityConstants.DETAILS_USERNAME));logVo.setTenantId(Long.parseLong(tenantKeyStrResolver.key()));publisher.publishEvent(new SysLogEvent(logVo));
}

异步监听处理事件,调用upms的远程接口,存储对应的登录信息到数据表中:

/** Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at**     http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package com.pig4cloud.pigx.common.log.event;import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.pig4cloud.pigx.admin.api.dto.SysLogDTO;
import com.pig4cloud.pigx.admin.api.feign.RemoteLogService;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.jackson.PigxJavaTimeModule;
import com.pig4cloud.pigx.common.log.config.PigxLogProperties;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;import java.util.Objects;/*** @author lengleng 异步监听日志事件*/
@Slf4j
@RequiredArgsConstructor
public class SysLogListener implements InitializingBean {// new 一个 避免日志脱敏策略影响全局ObjectMapperprivate final static ObjectMapper objectMapper = new ObjectMapper();private final RemoteLogService remoteLogService;private final PigxLogProperties logProperties;@SneakyThrows@Async@Order@EventListener(SysLogEvent.class)public void saveSysLog(SysLogEvent event) {SysLogDTO source = (SysLogDTO) event.getSource();// json 格式刷参数放在异步中处理,提升性能if (Objects.nonNull(source.getBody()) && logProperties.isRequestEnabled()) {String params = objectMapper.writeValueAsString(source.getBody());source.setParams(StrUtil.subPre(params, logProperties.getMaxLength()));}source.setBody(null);remoteLogService.saveLog(source, SecurityConstants.FROM_IN);}@Overridepublic void afterPropertiesSet() {objectMapper.addMixIn(Object.class, PropertyFilterMixIn.class);String[] ignorableFieldNames = logProperties.getExcludeFields().toArray(new String[0]);FilterProvider filters = new SimpleFilterProvider().addFilter("filter properties by name",SimpleBeanPropertyFilter.serializeAllExcept(ignorableFieldNames));objectMapper.setFilterProvider(filters);objectMapper.registerModule(new PigxJavaTimeModule());}@JsonFilter("filter properties by name")class PropertyFilterMixIn {}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/282837.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PVE系列-防火墙的免费安静之旅IPfire

Ventoy一款引导盘可以引导各种启动盘安装盘的工具https://www.ventoy.net/cn/index.html 在它的兼容iso的列表 中发现了Ipfirehttps://wiki.ipfire.org/ &#xff0c;本来用着openwrt也挺好&#xff0c;忍不住的虚拟机尝了尝鲜&#xff0c;发现的功能有2&#xff0c; 安全吧&a…

Vue脚手架环境搭建

一、node安装 1、下载地址&#xff1a;Node.js 2、开始安装&#xff0c;下一步 3、 可以更改安装路径 4、 默认安装&#xff0c;继续下一步 5、不勾选&#xff0c;继续下一步 6、安装完成&#xff0c;点击finish 7、测试&#xff0c;打开CMD控制台&#xff0c;输入命令&#…

leetcode刷题日志-383赎金信

思路&#xff1a;分别用两个map记录ransomNote和magazine中的字符以及出现的次数。最后遍历记录ransomNote的map&#xff0c;如果ransomNote的map中出现的magazine的map中没有出现或者出现的次数小于ransomNote的map则返回false&#xff0c;否则返回true&#xff1b; class So…

代码随想录二刷 | 二叉树 | 从中序与后序遍历序列构造二叉树

代码随想录二刷 &#xff5c; 二叉树 &#xff5c; 从中序与后序遍历序列构造二叉树 题目描述解题思路代码实现 题目描述 106.从中序与后序遍历序列构造二叉树 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是…

【案例】注册表简介,新建一个右键菜单打开方式选项

这里写目录标题 来源注册表的介绍注册表编辑器VScode的打开方式菜单![image-20231217201730121](https://img-blog.csdnimg.cn/img_convert/56c02643df9e8ec3afb4f3ac5cc0cdd5.png)如何自定义一个右键菜单备份注册表新建一个菜单选项”右键用记事本打开“ DWORDQWORD可扩充字符…

【动态规划】06路径问题_不同路径II_C++(medium)

题目链接&#xff1a;leetcode不同路径II 目录 题目解析&#xff1a; 算法原理 1.状态表示 2.状态转移方程 3.初始化 4.填表顺序 5.返回值 编写代码 题目解析&#xff1a; 题目让我们求在考虑网格中有障碍物的情况下&#xff0c;从左上角到右下角将会有多少条不同的路径…

QT第一步

文章目录 软件下载软件安装QT的程序组新建项目 软件下载 qt下载网址&#xff1a;https://download.qt.io/archive/qt/   关于版本&#xff1a;     我选择的版本是5.14.2&#xff0c;这个版本是最后的二进制安装包的版本&#xff0c;在往后的版本就需要在线安装了。并且5…

python学习,1.变量和简单的数据类型

一、编写文章的目的 1.这是为了初学者而写的&#xff0c;学习python比较简单然后上手&#xff0c;也会过滤一些&#xff0c;如果没有提起到的&#xff0c;可以在学习的时候进行补充 2.相对来说&#xff0c;上手难度不会很难。 二、内容 1.让首字母大写&#xff1b;字母都大写…

【每日一题】2697. 字典序最小回文串-2023.12.13

题目&#xff1a; 2697. 字典序最小回文串 给你一个由 小写英文字母 组成的字符串 s &#xff0c;你可以对其执行一些操作。在一步操作中&#xff0c;你可以用其他小写英文字母 替换 s 中的一个字符。 请你执行 尽可能少的操作 &#xff0c;使 s 变成一个 回文串 。如果执行…

计算机中msvcr120.dll丢失怎样修复,这5个方法可以搞定

几乎在所有操作系统中&#xff0c;可分为两种库&#xff0c;一种是静态库&#xff08;.lib&#xff09;&#xff0c;另一种是动态库&#xff08;.dll&#xff09;。 为什么很多小伙伴在打开软件的时候会弹出“由于找不到XXX.dll文件&#xff0c;无法继续执行代码、、、、、、”…

Vue学习计划-Vue2--VueCLi(四)组件传值和自定义事件

1. 组件传值 组件化编码流程&#xff1a; 拆分静态组件&#xff1a;组件要按照功能点拆分&#xff0c;命名不要与html元素冲突实现动态组件&#xff1a;考虑好数据的存放位置&#xff0c;数据是一个组件在用&#xff0c;还是一些组件在用&#xff1a; 一个组件在用&#xff0c…

《点云处理》 提取点云内点和外点

前言 关于内点&#xff08;inliers&#xff09;和外点&#xff08;outliers&#xff09;在点云处理方向上是个非常常见的名词。有时候&#xff0c;内点也会被称之为有效点&#xff0c;而外点会被称之为无效点。所谓有效和无效都是相对而言的&#xff0c;无效不一定是真的没有意…