文章目录
- 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);
//从请求头中获取租户拼接区分租户的key
String 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
对象中的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
方法,对客户的信息进行一个查询以及注册
根据客户端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);
}
//组装登录认证对象:详情见3
Authentication authorizationGrantAuthentication = this.authenticationConverter.convert(request);
//登录认证对象为null 抛出异常
if (authorizationGrantAuthentication == null) {
throwError(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, OAuth2ParameterNames.GRANT_TYPE);
}
//登录认证对象是AbstractAuthenticationToken的实例
//将其转换为 AbstractAuthenticationToken 类型,并设置其详细信息
/*
remoteAddress
sessionId等
*/
if (authorizationGrantAuthentication instanceof AbstractAuthenticationToken) {
((AbstractAuthenticationToken) authorizationGrantAuthentication)
.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
//认证管理器进行认证,详情见4
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication);
//认证成功处理,详情见5
this.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.1
if (!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.2
checkParams(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.3
return 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
@Override
public 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
,即调用OAuth2ResourceOwnerBaseAuthenticationProvider
的authenticate
方法,方法内容如下所示:
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//获取登录认证对象信息
//对于密码登录这里是OAuth2ResourceOwnerPasswordAuthenticationToken
T resouceOwnerBaseAuthentication = (T) authentication;
//获取经过身份验证的客户端信息clientPrincipal,详情见4.1
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(
resouceOwnerBaseAuthentication);
//从客户端登录认证对象信息中获取注册的客户端信息
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
//检查注册的客户端信息,这里主要对其授权类型进行一个判断,详情见4.2
checkClient(registeredClient);
//处理登录认证对象信息中的授权范围,存储到authorizedScopes中
Set<String> authorizedScopes;
// Default to configured scopes
if (!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 security
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = buildToken(reqParameters);
//打印获得的UsernamePasswordAuthenticationToken
LOGGER.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);
// 没有设置并发控制走原有逻辑生成 || 设置同时在线为 true
if (Objects.isNull(onlineQuantity) || BooleanUtil.toBooleanObject((String) onlineQuantity)) {
//构建请求令牌、刷新令牌 详情见4.6
return generatAuthenticationToken(resouceOwnerBaseAuthentication, clientPrincipal, registeredClient,
authorizedScopes, usernamePasswordAuthentication);
}
// 不允许同时在线,删除原有username 关联的所有token
PigxRedisOAuth2AuthorizationService redisOAuth2AuthorizationService = (PigxRedisOAuth2AuthorizationService) this.authorizationService;
//详情见4.5
redisOAuth2AuthorizationService.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类型
//如果是将其值赋值给clientPrincipal
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
}
//如果clientPrincipal有值并且已经认证过,那么就返回clientPrincipal
if (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(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);
当前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.1
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
//详情见4.4.2
this.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);
//获取客户端id
String 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的值赋值给finalClientId
String finalClientId = clientId;
//获取到支持当前授权类型grantType的PigxUserDetailsService
//如果有多个就取order最大的PigxUserDetailsService
Optional<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.1
UserDetails 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.1
Cache 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.2
cache.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)加入到dbAuthsSet
Arrays.stream(info.getRoles()).forEach(roleId -> dbAuthsSet.add(SecurityConstants.ROLE + roleId));
// 获取资源(权限)加入到dbAuthsSet
dbAuthsSet.addAll(Arrays.asList(info.getPermissions()));
}
//调用AuthorityUtils的createAuthorityList方法
//将dbAuthsSet中的信息存入到authorities中,类型是SimpleGrantedAuthority
Collection<? 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"));
}
//获取当前密码给局部变量presentedPassword
String presentedPassword = authentication.getCredentials().toString();
//调用spring security进行账号密码匹配,详细信息见4.4.2.1
if (!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-token
String authenticationName = authentication.getName();
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
String 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 查询存储的 OAuth2Authorization
OAuth2Authorization 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:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthentication)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(authorizedScopes)
.authorizationGrantType(resouceOwnerBaseAuthentication.getAuthorizationGrantType())
.authorizationGrant(resouceOwnerBaseAuthentication);
// @formatter:on
OAuth2Authorization.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.1
this.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 (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:off
OAuth2TokenClaimsSet.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:on
if (this.accessTokenCustomizer != null) {
// @formatter:off
OAuth2TokenClaimsContext.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:on
OAuth2TokenClaimsContext 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::xxx
String 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.1
sendSuccessEventLog(request, accessTokenAuthentication, map);
}
// 清除账号历史锁定次数
clearLoginFailureTimes(map);
// 输出token
sendAccessTokenResponse(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 一个 避免日志脱敏策略影响全局ObjectMapper
private 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);
}
@Override
public 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 {
}
}