📄 自定义用户信息生成token处理
内部资料,请扫码登录
pigcloud
特别说明
感谢群友 人生只当静默#202303071712 大佬 提供的整合文档
本文是针对 Pigx 商业版 4.6.0 版本实现,其他版本能否适用请自行参考
# 业务背景
- Pigx 一直使用 OAuth2 认证,最初使用的是 spring-security-oauth2。在 2020 年,Spring 宣布停止维护 spring-security-oauth2,并研发了 spring-security-oauth2-authorization-server,可能用于替换原有的认证模式。
- 针对不同业务场景,可能需要按业务逻辑生成 token,例如:业务用户的登录和注册、手机号码短信验证修改密码并登录等。这些场景需要先经过业务服务处理完成才能生成 token。尽管使用现有的 Pigx 认证模式可以实现这些场景的调整,但存在一定的复杂性。因此,我们考虑在业务服务处理过程中拿用户信息去认证服务生成 token。
# 自定生成 Token 流程图
# 代码实现
# ① auth 服务增加生成 token 请求
pigx-auth 中找到 com.pig4cloud.pigx.auth.endpoint.PigTokenEndpoint 类
// 注入生成token service
private final TokenApplicationService tokenApplicationService;
@PostMapping("/xxx/xxx")
public R<TokenInfoDTO> generateToken(@RequestBody AppUserInfoDTO userInfo) {
return tokenApplicationService.generateToken(userInfo);
}
# ② 定义请求实体
请求用户信息、token 信息、响应用户信息
@Data
@Schema(description = "app 用户 DTO")
public class AppUserInfoDTO {
@Schema(description = "clientId", required = true)
private String clientId;
@Schema(description = "userId", required = true)
private Long userId;
@Schema(description = "用户名", required = true)
private String username;
@Schema(description = "密码", required = true)
private String password;
@Schema(description = "手机号")
private String phone;
@Schema(description = "头像")
private String avatar;
@Schema(description = "拓展字段:昵称")
private String nickname;
@Schema(description = "拓展字段:姓名")
private String name;
@Schema(description = "拓展字段:邮箱")
private String email;
@Schema(description = "锁定标记")
private String lockFlag;
@Schema(description = "微信小程序登录openId")
private String wxOpenid;
}
@Data
@Schema(description = "token信息")
public class TokenInfoDTO {
@Schema(description = "访问token")
private String access_token;
@Schema(description = "刷新token")
private String refresh_token;
@Schema(description = "token有效时间(秒)")
private Long expires_in;
@Schema(description = "用户信息")
private TokenUserDTO user_info;
}
@Data
@Schema(description = "用户VO")
public class TokenUserDTO {
@Schema(description = "用户id")
private Long userId;
@Schema(description = "用户名")
private String username;
}
# ③ 定义 TokenApplicationService
public interface TokenApplicationService {
R<TokenInfoDTO> generateToken(AppUserInfoDTO userInfo);
}
# ④ 实现 TokenApplicationServiceImpl
@Slf4j
@Service
@RequiredArgsConstructor
public class TokenApplicationServiceImpl implements TokenApplicationService {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
@Deprecated
private Supplier<String> refreshTokenGenerator;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
// 注册的client的repo
private final RegisteredClientRepository registeredClientRepository;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
@Override
public R<TokenInfoDTO> generateToken(AppUserInfoDTO userInfo) {
UserDetails userDetails = this.convertUserDetailsAppUser(userInfo);
OAuth2AccessTokenAuthenticationToken oAuth2AccessToken = this.buildAccessTokenAuthentication(userInfo.getClientId(), userDetails);
TokenInfoDTO tokenInfo = this.convertAccessToken(oAuth2AccessToken);
return R.ok(tokenInfo);
}
private OAuth2AccessTokenAuthenticationToken buildAccessTokenAuthentication(String clientId, UserDetails userDetails) {
// 查询客户端信息
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
if (Objects.isNull(registeredClient)) {
log.error("RegisteredClient is null, clientId:{}", clientId);
return null;
}
String clientSecret = registeredClient.getClientSecret();
// 构造OAuth2ClientAuthenticationToken
OAuth2ClientAuthenticationToken clientAuthentication = new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, clientSecret, Collections.emptyMap());
// 填充客户端信息
OAuth2ClientAuthenticationToken oAuth2ClientAuthenticationToken = new OAuth2ClientAuthenticationToken(registeredClient,
clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
// 授权服务范围
Set<String> authorizedScopes = new LinkedHashSet<>();
authorizedScopes.addAll(registeredClient.getScopes());
// 构造授权对象
OAuth2ResourceOwnerBaseAuthenticationToken resourceOwnerBaseAuthentication = new OAuth2ResourceOwnerSmsAuthenticationToken(new AuthorizationGrantType(SecurityConstants.APP),
oAuth2ClientAuthenticationToken, authorizedScopes, Maps.newHashMap());
// 重新构造OAuth2ClientAuthenticationToken
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(resourceOwnerBaseAuthentication);
// 构造用户密码认证对象
Authentication usernamePasswordAuthentication = new UsernamePasswordAuthenticationToken(userDetails, null
, this.authoritiesMapper.mapAuthorities(userDetails.getAuthorities()));
Object onlineQuantity = registeredClient.getClientSettings().getSettings().get(CommonConstants.ONLINE_QUANTITY);
// 没有设置并发控制走原有逻辑生成 || 设置同时在线为 true
if (Objects.isNull(onlineQuantity) || BooleanUtil.toBooleanObject((String) onlineQuantity)) {
return generatAuthenticationToken(resourceOwnerBaseAuthentication, clientPrincipal, registeredClient,
authorizedScopes, usernamePasswordAuthentication);
}
// 不允许同时在线,删除原有username 关联的所有token
PigRedisOAuth2AuthorizationService redisOAuth2AuthorizationService = (PigRedisOAuth2AuthorizationService) this.authorizationService;
redisOAuth2AuthorizationService.removeByUsername(usernamePasswordAuthentication);
return generatAuthenticationToken(resourceOwnerBaseAuthentication, clientPrincipal, registeredClient,
authorizedScopes, usernamePasswordAuthentication);
}
private OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
OAuth2ClientAuthenticationToken clientPrincipal = null;
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
}
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
return clientPrincipal;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
/**
* 生成新的令牌
* @param resouceOwnerBaseAuthentication
* @param clientPrincipal
* @param registeredClient
* @param authorizedScopes
* @param usernamePasswordAuthentication
* @return OAuth2AccessTokenAuthenticationToken
*/
@NotNull
private OAuth2AccessTokenAuthenticationToken generatAuthenticationToken(OAuth2ResourceOwnerBaseAuthenticationToken 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(AuthorizationGrantType.PASSWORD)
.authorizationGrant(resouceOwnerBaseAuthentication);
// @formatter:on
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(usernamePasswordAuthentication.getName())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
// 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();
this.authorizationService.save(authorization);
log.debug("returning OAuth2AccessTokenAuthenticationToken");
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken,
Objects.requireNonNull(authorization.getAccessToken().getClaims()));
}
private TokenInfoDTO convertAccessToken(Authentication authentication) {
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
TokenInfoDTO tokenInfo = new TokenInfoDTO();
tokenInfo.setAccess_token(accessToken.getTokenValue());
tokenInfo.setRefresh_token(refreshToken.getTokenValue());
if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
tokenInfo.setExpires_in(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
}
if (CollUtil.isNotEmpty(additionalParameters)) {
TokenUserDTO tokenUserDTO = new TokenUserDTO();
tokenUserDTO.setUserId(Convert.toLong(additionalParameters.getOrDefault(SecurityConstants.DETAILS_USER_ID,null)));
tokenUserDTO.setUsername(Convert.toStr(additionalParameters.getOrDefault(SecurityConstants.DETAILS_USERNAME,null)));
tokenInfo.setUser_info(tokenUserDTO);
}
// 无状态 注意删除 context 上下文的信息
SecurityContextHolder.clearContext();
return tokenInfo;
}
/**
* 构建userdetails
*
* @param user 用户信息
* @return UserDetails
*/
private UserDetails convertUserDetailsAppUser(AppUserInfoDTO user) {
Set<String> dbAuthsSet = new HashSet<>();
Collection<? extends GrantedAuthority> authorities = AuthorityUtils
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
// 构造security用户
return new PigxUser(user.getUserId(), user.getUsername(), null, user.getPhone(), user.getAvatar(),
user.getNickname(), user.getName(), user.getEmail(), CommonConstants.TENANT_ID_1,
SecurityConstants.BCRYPT + user.getPassword(), true, true, UserTypeEnum.TOC.getStatus(), true,
!CommonConstants.STATUS_LOCK.equals(user.getLockFlag()), authorities);
}
}
# ⑤ 微服务中定义获取 token feign 接口
com.pig4cloud.pigx.admin.api.feign.RemoteTokenService
@PostMapping("/xxx/xxx")
R<TokenInfoDTO> generateToken(@RequestBody AppUserInfoDTO userInfo, @RequestHeader(SecurityConstants.FROM) String from);
# ⑥ 接入使用
// 注入feign接口
private final RemoteTokenService remoteTokenService;
private R<TokenInfoDTO> generateToken() {
AppUserInfoDTO userInfo = new AppUserInfoDTO();
// clientId为后台终端配置
userInfo.setClientId("xxx");
userInfo.setUserId(1L);
userInfo.setUsername("admin");
userInfo.setPassword("$2a$10$cE02oZ1N4mkdA6JHJUP7/uAJ3TQdVgO3kLRvLo");
userInfo.setPhone("18200000001");
userInfo.setAvatar("");
userInfo.setNickname("管理员");
userInfo.setName("管理员");
userInfo.setEmail("admin@mail.com");
userInfo.setLockFlag("0");
userInfo.setWxOpenid("0");
R<TokenInfoDTO> tokenRes = remoteTokenService.generateToken(userInfo, SecurityConstants.FROM_IN);
return tokenRes;
}
# ⑦ 生成 token 内容
{
"code": 0,
"msg": null,
"data": {
"access_token": "fdc2a8c6-b4c8-48f9-a498-071a0142c335",
"refresh_token": "WxAnoVhu5912cLk11b7rQ0kOklOdzapn9GsEqG5Q80dMxwa5PlK4CNYRPxoWhJG31gYpUWoIbdMzz2frJtmxPATQdgAr84dR5WM_ULQ_BZB2YCa7RS-HPffSnhsbBHCZ",
"expires_in": "43200",
"user_info": {
"userId": "1",
"username": "admin"
}
}
}