河北廊坊建设银行网站,活动页面图片,网站seo方案模板,婚庆公司广告语文章目录 O、前言一、总流程概括#xff1a;二、具体流程分析PIG提供的具体流程图#xff1a;鉴权请求报文示例0、网关前置处理1、客户端认证处理2、正式接受登录请求3、组装认证对象4、认证管理器进行认证#xff08;授权认证调用#xff09;5、认证成功处理器 O、前言
对… 文章目录 O、前言一、总流程概括二、具体流程分析PIG提供的具体流程图鉴权请求报文示例0、网关前置处理1、客户端认证处理2、正式接受登录请求3、组装认证对象4、认证管理器进行认证授权认证调用5、认证成功处理器 O、前言
对pig框架获取Token流程中的主要部分进行分析和整理,方便日后的学习、复习。
一、总流程概括
说明 对pig框架通过用户名密码的形式生成token认证的总流程进行分析。
1、通过浏览器或者PostMan等发送请求报文
请求直接访问网关通过网关去进行其他微服务项目的访问
报文示例解析
//请求方式 请求路径请求参数 HTTP 1.1 协议
POST /auth/oauth2/token?grant_typepasswordscopeserver 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//用户名称和密码
usernameadminpasswordYehdBPev2、在网关中对验证码进行校验、对前端通过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_typepasswordscopeserver HTTP/1.1
Host: pig-gateway:9999
Authorization: Basic dGVzdDp0ZXN0
Content-Type: application/x-www-form-urlencoded
Content-Length: 32
usernameadminpasswordYehdBPev0、网关前置处理
对于获取token的请求经过网关会进行两个前置处理分别是验证码校验和前端已加密的密码的解密。
①验证码校验待学习
涉及到的类ValidateCodeGatewayFilter
②前端已加密的用户密码进行解密
涉及到的类PasswordDecoderFilter
说明
在前端登录的请求报文中前端会通过AES对称加密算法对登录的密码进行加密传输具体过程不展开如上鉴权请求报文中的password示例
usernameadminpasswordYehdBPev后端对该密码进行解密的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_typepasswordscopeserver HTTP/1.1
Authorization: Basic dGVzdDp0ZXN0Basic Auth: test/test
usernamebananapasswordBi6KFBD0明文123456/**
* 根据请求的clientId 查询客户端配置是否是加密传输
* param request 请求上下文
* return true 加密传输 、 false 原文传输
*/
private boolean isEncClient(ServerHttpRequest request) {//获得请求头Basic加密的内容即客户端的信息username/passwordString 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_FLAG0关闭加密 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对象中的authenticated为false表示还未进行认证
Authentication authenticationRequest this.authenticationConverter.convert(request);2、OAuth2ClientAuthenticationFilter类
这里调用的是2.1中ProviderManager的authenticate方法对客户端进行认证
Authentication authenticationResult this.authenticationManager.authenticate(authenticationRequest);2.1、ProviderManager类
这里会通过迭代器遍历provider找到适合的、对应的provider进行处理
最终这里的provider实现类是2.2ClientSecretAuthenticationProvider,调用2.2ClientSecretAuthenticationProvider的authenticate方法
result provider.authenticate(authentication);2.2、ClientSecretAuthenticationProvider类
在ClientSecretAuthenticationProvider的authenticate方法中调用registeredClientRepository的实现类2.3PigxRemoteRegisteredClientRepository的findByClientId方法
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);返回客户端认证的令牌对象信息OAuth2ClientAuthenticationToken给authenticationResult
此时OAuth2ClientAuthenticationToken对象中的authenticated为true表示已经进行认证
之后调用OAuth2ClientAuthenticationFilter类doFilterInternal方法中的如下方法调用认证成功的处理器
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);此时的this.authenticationSuccessHandler就是OAuth2ClientAuthenticationFilter即调用OAuth2ClientAuthenticationFilter的onAuthenticationSuccess方法
下面就是将客户端授权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方法对客户的信息进行一个查询以及注册
根据客户端idClientId先调用远程接口获取客户端的信息
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_typepasswordscopeserver HTTP/1.1的grant_typepassword*/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授权范围*/MultiValueMapString, 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集合中SetString 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中MapString, 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(ListAuthenticationConverter converters)用于添加认证转换器保存到当前类中的converters成员变量中
public final class DelegatingAuthenticationConverter implements AuthenticationConverter {private final ListAuthenticationConverter converters;/*** Constructs a {code DelegatingAuthenticationConverter} using the provided parameters.** param converters a {code List} of {link AuthenticationConverter}(s)*/public DelegatingAuthenticationConverter(ListAuthenticationConverter converters) {Assert.notEmpty(converters, converters cannot be empty);this.converters Collections.unmodifiableList(new LinkedList(converters));}NullableOverridepublic 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授权范围*/MultiValueMapString, 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, SetString scopes, MapString, Object additionalParameters) {super(authorizationGrantType, clientPrincipal, scopes, additionalParameters);}
}OAuth2ResourceOwnerBaseAuthenticationToken构造器
public OAuth2ResourceOwnerBaseAuthenticationToken(AuthorizationGrantType authorizationGrantType,Authentication clientPrincipal, Nullable SetString scopes,Nullable MapString, 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 即调用OAuth2ResourceOwnerBaseAuthenticationProvider的authenticate方法方法内容如下所示
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中SetString 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中MapString, 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 authenticatedtrue 表示认证通过*/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);在这里调用的buildToken的OAuth2ResourceOwnerBaseAuthenticationProvider处理自定义授权抽象类其中的UsernamePasswordAuthenticationToken是一个抽象方法
public abstract UsernamePasswordAuthenticationToken buildToken(MapString, Object reqParameters);由于当前是用户密码授权因此其具体处理用户名密码授权的实现类是OAuth2ResourceOwnerPasswordAuthenticationProvider其中实现的方法如下
从登录认证对象信息中获取的其他入参信息中获取Username和Password信息分别赋值给局部变量username和password
Override
public UsernamePasswordAuthenticationToken buildToken(MapString, 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);当前authenticationManager是ProviderManager调用其中的方法
其中provider是AbstractUserDetailsAuthenticationProvider的具体实现类PigxDaoAuthenticationProvider result provider.authenticate(authentication);首先是调用AbstractUserDetailsAuthenticationProvider的authenticate方法
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);其调用实现类PigxDaoAuthenticationProvider的retrieveUser方法来获取用户信息支持多用户体系
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中MapString, PigxUserDetailsService userDetailsServiceMap SpringUtil.getBeansOfType(PigxUserDetailsService.class);//将clientId的值赋值给finalClientIdString finalClientId clientId;//获取到支持当前授权类型grantType的PigxUserDetailsService//如果有多个就取order最大的PigxUserDetailsServiceOptionalPigxUserDetailsService 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 PigxDefaultUserDetailsServiceImpl的public 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去获取用户的名称RUserInfo 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(RUserInfo result) {// formatter:off//通过RetOps进行远程调用的判空处理return RetOps.of(result).getData()//调用convertUserDetails方法对远程调用获取的UserInfo进行处理.map(this::convertUserDetails).orElseThrow(() - new UsernameNotFoundException(用户不存在));// formatter:on
}default UserDetails convertUserDetails(UserInfo info) {SetString 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;MapString, 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);SetString keys redisTemplate.keys(tokenUsernameKey);if (CollUtil.isEmpty(keys)) {return;}ListObject 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,SetString 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的生成的
其首先调用的是DelegatingOAuth2TokenGenerator的generate方法
this.tokenGenerators有两个值
Nullable
Override
public OAuth2Token generate(OAuth2TokenContext context) {for (OAuth2TokenGeneratorOAuth2Token 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.TokenOAuth2AuthorizationCode 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;MapString, 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, MapString, 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;SneakyThrowsAsyncOrderEventListener(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 {}}