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.mzph.cn/news/229627.shtml

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

相关文章

论文阅读:Learning sRGB-to-Raw-RGB De-rendering with Content-Aware Metadata

论文阅读&#xff1a;Learning sRGB-to-Raw-RGB De-rendering with Content-Aware Metadata Abstract 大多数的 Camera ISP 会将 RAW 图经过一系列的处理&#xff0c;变成 sRGB 图像&#xff0c;ISP 的处理中很多模块是非线性的操作&#xff0c;这些操作会破坏环境光照的线性…

【LLM】Prompt Engineering

Prompt Engineering CoTCoT - SCToTGoT CoT: Chain-of-Thought 通过这样链式的思考&#xff0c;Model输出的结果会更准确 CoT-SC: Self-Consistency Improves Chain of Thought Reasoning in Language Models 往往&#xff0c;我们会使用Greedy decode这样的策略&#xff0c…

arcgis更改服务注册数据库账号及密码

最近服务器数据库密码换了&#xff0c;gis服务也得换下数据库连接密码。传统官方的更改方式&#xff08;上传连接配置文件&#xff09;&#xff1a; ArcGIS Server数据库注册篇(I) — 更新数据库密码_arcgis server sde换密码-CSDN博客 方式太麻烦了&#xff0c;需要安装ArcG…

springboot221酒店管理系统

springboot221酒店管理系统 源码获取&#xff1a; https://docs.qq.com/doc/DUXdsVlhIdVlsemdX

通话状态监听-Android13

通话状态监听-Android13 1、Android Telephony 模块结构2、监听和广播获取通话状态2.1 注册2.2 通话状态通知2.3 通话状态 3、通知状态流程* 关键日志 frameworks/base/core/java/android/telephony/PhoneStateListener.java 1、Android Telephony 模块结构 Android Telephony…

数据结构-猴子吃桃问题

一、需求分析 有一群猴子摘了一堆桃子&#xff0c;他们每天都吃当前桃子的一半且再多吃一个&#xff0c;到了第10天就只余下一个桃子。用多种方法实现求出原来这群猴子共摘了多少个桃子。要求&#xff1a; 1)采用数组数据结构实现上述求解&#xff1b; 2)采用链数据结构实现上述…

TrustZone之其他设备及可信基础系统架构

一、其他设备 最后,我们将查看系统中的其他设备,如下图所示: 我们的示例TrustZone启用的系统包括一些尚未涵盖的设备,但我们需要这些设备来构建一个实际的系统。 • 一次性可编程存储器(OTP)或保险丝 这些是一旦写入就无法更改的存储器。与每个芯片上都包含相同…

数据仓库与数据挖掘小结

更加详细的只找得到pdf版本 填空10分 判断并改错10分 计算8分 综合20分 客观题 填空10分 判断并改错10分--错的要改 mooc中的--尤其考试题 名词解释12分 4个&#xff0c;每个3分 经常碰到的专业术语 简答题40分 5个&#xff0c;每道8分 综合 画roc曲线 …

02.Git常用基本操作

一、基本配置 &#xff08;1&#xff09;打开Git Bash &#xff08;2&#xff09;配置姓名和邮箱 git config --global user.name "Your Name" git config --global user.email "Your email" 因为Git是分布式版本控制工具&#xff0c;所以每个用户都需要…

数据安全传输基础设施平台(一)

1引言 1.1项目简介 数据安全传输基础设置平台项目&#xff08;简称&#xff0c;数据传输平台&#xff09;&#xff0c;是一款基础设施类项目&#xff0c;为集团、企业信息系统的提供统一、标准的信息安全服务&#xff1b;解决企业和企业之间&#xff0c;集团内部信息数据的传…

gitee提交代码步骤介绍(含git环境搭建)

1、gitee官网地址 https://gitee.com; 2、Windows中安装git环境 参考博客&#xff1a;《Windows中安装Git软件和TortoiseGit软件》&#xff1b; 3、设置用户名和密码 这里的用户名和密码就是登录gitee网站的用户名和密码如果设置错误&#xff0c;可以在Windows系统的“凭据管理…

【深度学习目标检测】九、基于yolov5的安全帽识别(python,目标检测)

YOLOv5是目标检测领域一种非常优秀的模型&#xff0c;其具有以下几个优势&#xff1a; 1. 高精度&#xff1a;YOLOv5相比于其前身YOLOv4&#xff0c;在目标检测精度上有了显著的提升。YOLOv5使用了一系列的改进&#xff0c;如更深的网络结构、更多的特征层和更高分辨率的输入图…

IDEA2023 + spring cloud 工程热部署设置方法

基于spring cloud 工程进行热部署 &#xff0c;实现每次修改工程源文件&#xff0c;后台自动启动&#xff0c;方便开发测试工作。具体分为5步骤即可&#xff1a; 1、修改工程的pom文件&#xff0c;增加adding devtools 工具包。 <dependency> <groupId>org.s…

js基础入门

先来一点js基础&#xff0c;其实js大部分的时候都在处理对象或者数组。 对象四个基本操作&#xff1a;增删改查 掌握元素的增删改查&#xff0c;了解如何拷贝&#xff0c;深拷贝和浅拷贝的区别。详情见代码 <script>//创建对象一共有三种赋值声明的语法let obj{} //赋值…

Vue 项目关于在生产环境下调试

前言 开发项目时&#xff0c;在本地调试没问题&#xff0c;但是部署到生产会遇到一些很奇怪的问题&#xff0c;本地又没法调&#xff0c;就需要在生产环境/域名下进行调试。 在这里介绍一个插件Vue force dev ,浏览器扩展里下载 即便是设置了Vue.config.devtoolsfalse 只要安…

认知能力测验,③如何破解语言常识类测试题?

作为认知能力测评中的一个环节&#xff0c;语言常识类&#xff0c;是大概率的出现&#xff0c;不同的用人单位可能略有不同&#xff0c;语言是一切的基础&#xff0c;而常识则意味着我们的知识面的宽度。 语言常识类的测试&#xff0c;如果要说技巧&#xff1f;难说....更多的…

maui sqlite开发一个商城加购物车的演示(3)

购物车界面及代码 <?xml version"1.0" encoding"utf-8" ?> <ContentPage xmlns"http://schemas.microsoft.com/dotnet/2021/maui"xmlns:x"http://schemas.microsoft.com/winfx/2009/xaml"xmlns:syncfusion"clr-namesp…

ArcMap自定义脚本工具箱迁移至ArcGIS pro

本文记录了将ArcMap10.7创建的自定义脚本工具箱&#xff08;.tbx&#xff09;迁移至ArcGIS pro的过程 ArcGIS Pro使用的是python版本与ArcMap不同&#xff0c;前者为python3&#xff0c;后者为python2。由于python3 和 python2 的部分语法不兼容&#xff0c;以及一些地理处理工…

87 GB 模型种子,GPT-4 缩小版,超越ChatGPT3.5,多平台在线体验

瞬间爆火的Mixtral 8x7B 大家好&#xff0c;我是老章 最近风头最盛的大模型当属Mistral AI 发布的Mixtral 8x7B了&#xff0c;火爆程度压过Google的Gemini。 缘起是MistralAI二话不说&#xff0c;直接在其推特账号上甩出了一个87GB的种子 随后Mixtral公布了模型的一些细节&am…