业务手动生成token(最简版)

扩展方式说明
本文介绍的是一种最简化的扩展方式——由应用自行签发 Token,不会触发 Spring Security 的标准认证流程。但所生成的 Token 仍能被 Spring Security 正确识别并用于后续的权限校验。

什么是 Token?

Token(令牌)就像一张“数字通行证”。当用户成功登录系统后,服务端会签发一个 Token,用户后续的请求只需携带这个 Token,即可证明身份并访问受保护的资源——就像用门禁卡进入办公楼一样。

通常,用户需提供用户名和密码,系统验证无误后才会颁发 Token。但在一些特定场景下,我们希望更灵活地生成 Token,无需重复输入密码或走完整登录流程。例如:

  • 注册即登录:用户完成注册后,系统可直接为其生成 Token,实现无缝登录体验;
  • 第三方集成登录:第三方回调当前在线用户的唯一凭据,pigx 验证后直接签发本平台的 Token,实现单点登录 pigx;
  • 特殊业务需求:在运维、测试或内部工具等场景中,可能需要绕过常规认证流程,直接下发 Token 以提升效率或满足安全策略。

通过灵活的 Token 发放机制,系统既能保障安全性,又能提升用户体验与集成能力。

环境说明

环境组件版本备注
PigX5.8
JDK17分支:jdk17

工作流程图解

下面这张图展示了我们自定义Token生成的完整流程:

sequenceDiagram
    participant Client as 客户端
    participant BizService as 业务服务
    participant Auth as pigx-auth

    Client->>BizService: 1: 业务请求
    BizService->>BizService: 2: 业务逻辑处理<br/>组装用户信息
    BizService->>Auth: 3: 拿用户信息生成token
    Auth->>Auth: 4: 用户信息转换为UserDetails<br/>根据clientId获取客户端配置<br/>组装Authentication, 生成token
    Auth-->>BizService: 5: 响应生成的token
    BizService-->>Client: 6: 返回token

流程说明:

  1. 业务系统发起Token生成请求(只需要用户名)
  2. 认证中心接收请求并生成Token
  3. 返回生成的Token给业务系统
  4. 业务系统可以使用这个Token访问其他服务

代码实现步骤

第一步:定义数据传输对象

首先,我们需要定义两个"信封",用来装载我们要传输的数据:

@Data
public class UserTokenDTO {

    // 请求信封:装载我们要发送的数据
    @Data
    public static class Request {
        private String username;  // 用户名(必填)
        private List<String> authorities = new ArrayList<>();  // 用户权限列表(可选)
    }

    // 响应信封:装载系统返回给我们的数据
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Response {
        private String accessToken;   // 访问令牌(主要的通行证)
        private String refreshToken;  // 刷新令牌(用来获取新的访问令牌)
    }
}

第二步:定义服务调用接口

这里定义了一个"电话号码",其他系统可以通过这个"号码"来请求生成Token:

@FeignClient(contextId = "remoteTokenService", value = ServiceNameConstants.AUTH_SERVICE)
public interface RemoteTokenService {
        @NoToken  // 这个注解表示调用这个接口不需要Token
        @PostMapping("/token/generate-token")  // 接口地址
        R<UserTokenDTO.Response> generateToken(@RequestBody UserTokenDTO.Request request);
}

第三步:实现Token生成逻辑

在认证中心添加实际的Token生成代码:

@Autowired
private OAuth2TokenGenerator<OAuth2AccessToken> tokenGenerator;

@Inner  // 这个注解表示只有内部系统可以调用
@SneakyThrows
@PostMapping("/generate-token")
public R<UserTokenDTO.Response> generateToken(@RequestBody UserTokenDTO.Request request) {
    
    // 第1步:创建一个"客户端配置",告诉系统这个Token的基本规则
    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))  // 刷新Token有效期:7天
                    .accessTokenFormat(OAuth2TokenFormat.REFERENCE)
                    .build())
            .build();

    // 第2步:构建用户信息对象
    // 这里我们根据传入的用户名创建一个完整的用户对象
    PigxUser pigUser = new PigxUser(
            110L,                    // 用户ID
            request.getUsername(),   // 用户名
            null,                    // 密码(这里不需要)
            "", "", "", "", "",      // 其他用户信息(暂时为空)
            1L,                      // 部门ID
            "",                      // 其他信息
            true, true,              // 账户状态
            UserTypeEnum.TOB.getStatus(), 
            true, false,
            // 将权限字符串转换为系统认识的权限对象
            request.getAuthorities().stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList())
    );

    // 第3步:创建认证对象
    Authentication usernamePasswordAuthentication = 
            new UsernamePasswordAuthenticationToken(pigUser, StrUtil.EMPTY);

    // 第4步:设置Token生成的上下文环境
    DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
            .registeredClient(registeredClient)
            .principal(usernamePasswordAuthentication)
            .authorizationServerContext(new AuthorizationServerContext() {
                @Override
                public String getIssuer() {
                    return "http://ai.com";  // Token发行者
                }

                @Override
                public AuthorizationServerSettings getAuthorizationServerSettings() {
                    return AuthorizationServerSettings.builder().build();
                }
            });

    // 第5步:开始构建授权信息
    OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization
            .withRegisteredClient(registeredClient)
            .principalName(usernamePasswordAuthentication.getName());

    // 第6步:生成访问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());

    // 第7步:保存访问Token信息
    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);
    }

    // 第8步:生成刷新Token(用来获取新的访问Token)
    OAuth2RefreshToken refreshToken;
    tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
    OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
    refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
    authorizationBuilder.refreshToken(refreshToken);

    // 第9步:保存完整的授权信息到数据库
    OAuth2Authorization authorization = authorizationBuilder
            .authorizationGrantType(AuthorizationGrantType.PASSWORD)
            .build();
    this.authorizationService.save(authorization);
    
    // 第10步:返回生成的Token
    return R.ok(new UserTokenDTO.Response(
            accessToken.getTokenValue(), 
            refreshToken.getTokenValue()));
}

第四步:业务代码中使用

现在其他业务代码就可以很简单地生成Token了:

private final RemoteTokenService remoteTokenService;

@Inner(value = false)
@RequestMapping("/test")
public R<UserTokenDTO.Response> demo() {
    // 创建请求对象
    UserTokenDTO.Request request = new UserTokenDTO.Request();
    request.setUsername("admin");  // 只需要提供用户名
    request.setAuthorities(List.of("ROLE_1""sys_user_add")); // 设置用户角色(ROLE_ID 固定写法)权限字符串
    
    // 调用Token生成服务
    return remoteTokenService.generateToken(request);
}
重要说明

setAuthorities 中配置的角色与权限必须与用户真实拥有的角色及其对应的权限字符串保持完全一致,且精确无误。任何不匹配或拼写偏差,都会导致基于 @HasPermission 的接口鉴权校验无法通过,从而引发访问失败。

使用说明:

  • username:要生成Token的用户名
  • authorities:用户拥有的权限列表,决定了用户能访问哪些功能

第五步:调整Token验证逻辑

最后,我们需要告诉系统如何识别我们自定义生成的Token:

// 在 PigxCustomOpaqueTokenIntrospector 中添加以下代码
if (SecurityConstants.FROM.equals(oldAuthorization.getRegisteredClientId())){
    // 如果是我们自定义生成的Token,直接返回用户信息
    return (PigxUser) ((UsernamePasswordAuthenticationToken) 
            Objects.requireNonNull(oldAuthorization)
                    .getAttributes()
                    .get(Principal.class.getName()))
                    .getPrincipal();
}
Token验证逻辑