业务背景

  • 本文主要介绍如何自定义 Token 生成逻辑,
    不依赖于 OAuth2 协议的严格参数要求
    。比如只提供用户名,即可生成指定用户的 Token,使业务流程更加灵活、简便。
  • 在实际业务中,通常在用户注册完成后需要直接生成 Token,方便用户在注册后即刻登录。此时可以通过定制化的 Token 生成逻辑,根据业务规则灵活地生成 Token,以满足业务流程需求。
  • 当然这种方式过于暴力,绕过了 Spring Security 认证服务器的各种校验,所以在使用时需要谨慎

手动生成 Token 流程图

定义服务调用 Feign 和实体

在 upms-api 模块新增如下实体和 feign client

@Data
public class UserTokenDTO {
    /**
     * 用户名
     */
    private String username;
    
    /**
     * 权限列表
     */
    private List<String> authorities = new ArrayList<>();
}
@FeignClient(contextId = "remoteTokenService", value = ServiceNameConstants.AUTH_SERVICE)
public interface RemoteTokenService {

    @NoToken
    @PostMapping("/token/generate-token")
    R generateToken(@RequestBody UserTokenDTO userTokenDTO);
}

认证中心提供生成令牌端点

在 PigTokenEndpoint 体面添加如下接口代码,方便 feign 调用

private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;

@Inner
@SneakyThrows
@PostMapping("/generate-token")
public R generateToken(@RequestBody UserTokenDTO userTokenDTO) {
    // 构建授权信息
    RegisteredClient registeredClient = RegisteredClient.withId(SecurityConstants.FROM).clientId(SecurityConstants.FROM)
            .authorizationGrantType(AuthorizationGrantType.PASSWORD)
            .tokenSettings(TokenSettings.builder()
                    .accessTokenTimeToLive(Duration.ofHours(24)) // token 有效期 24 小时
                    .refreshTokenTimeToLive(Duration.ofDays(7))  // refresh token 有效期 7 天
                    .accessTokenFormat(OAuth2TokenFormat.REFERENCE)
                    .build())
            .build();

    // 构建用户信息 , 这里只拼接了  UserTokenDTO 中的 username 和 authorities,其他字段写死,可以自行场地;
    PigUser pigUser = new PigUser(1L, 1L, userTokenDTO.getUsername(), StrUtil.EMPTY, StrUtil.EMPTY
            , true, true, true, true, userTokenDTO.getAuthorities()
            .stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));

    Authentication usernamePasswordAuthentication = new UsernamePasswordAuthenticationToken(pigUser, StrUtil.EMPTY);

    DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
            .registeredClient(registeredClient)
            .principal(usernamePasswordAuthentication)
            .authorizationServerContext(AuthorizationServerContextHolder.getContext());

    OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization
            .withRegisteredClient(registeredClient)
            .principalName(usernamePasswordAuthentication.getName());

    // ----- Access token -----
    OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN)
            .authorizationGrantType(AuthorizationGrantType.PASSWORD)
            .authorizationGrant(new OAuth2ClientAuthenticationToken(registeredClient, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null))
            .build();
    OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);

    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()))
                .attribute(Principal.class.getName(), usernamePasswordAuthentication);
    } else {
        authorizationBuilder.id(accessToken.getTokenValue()).accessToken(accessToken);
    }

    // ----- Refresh token -----
    OAuth2RefreshToken refreshToken;
    tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
    OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
    refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
    authorizationBuilder.refreshToken(refreshToken);

    OAuth2Authorization authorization = authorizationBuilder
            .authorizationGrantType(AuthorizationGrantType.PASSWORD)
            .build();

    this.authorizationService.save(authorization);

    return R.ok(new OAuth2AccessTokenAuthenticationToken(registeredClient, usernamePasswordAuthentication, accessToken,
            refreshToken, authorization.getAccessToken().getClaims()));
}

业务代码调用签发 token

private final RemoteTokenService remoteTokenService;

// 拼接请求参数
UserTokenDTO userTokenDTO = new UserTokenDTO();
userTokenDTO.setUsername("admin");  // 只需要用户名,就可以生成 token
userTokenDTO.setAuthorities(List.of("sys_user_add")); // 权限列表的字符串,能调用的接口范围

// feign 调用 auth 模块生成 token
R<OAuth2AccessTokenAuthenticationToken> authenticationTokenR = remoteTokenService.generateToken(userTokenDTO);
// token 信息
OAuth2AccessToken accessToken = authenticationTokenR.getData().getAccessToken();
// 刷新 token 信息
OAuth2RefreshToken refreshToken = authenticationTokenR.getData().getRefreshToken();
// 客户端信息
RegisteredClient registeredClient = authenticationTokenR.getData().getRegisteredClient();

校验令牌微调

通过如上代码,我们已经实现了 token 的方法;接下来我们

需要对校验令牌的逻辑进行微调,以便资源服务器能够识别咱这个暴力 token。

PigCustomOpaqueTokenIntrospector 调整,如果是自定义签发的 token,直接返回用户信息

if (SecurityConstants.FROM.equals(oldAuthorization.getRegisteredClientId())){
    return (PigUser) ((UsernamePasswordAuthenticationToken) .requireNonNull(oldAuthorization).getAttributes().get(Principal.class.getName())).getPrincipal();
}

♥️ 获取支持

遇到问题?

如果您在使用过程中遇到任何问题、有功能建议或需求,请点击此卡片前往 Gitee 仓库提交 Issue。