HZERO 根据用户账号获取 Token 方案

问题背景

客户需求:在 HZERO 后端根据用户账号获取对应的 Token。客户原思路是”根据账号查出密码 → 解密 → 调用登录接口获取 Token”。

一、为什么”查密码→解密”方案不可行

HZERO 用户密码使用 BCrypt 单向哈希算法存储,数据库中保存的是哈希值,不可逆、不可解密。因此”从数据库查出密码 → 解密 → 调用登录接口”这条路在技术上走不通。

二、根据是否有用户明文密码,分两种方案

方案一:有用户明文密码 — 标准 OAuth2 password 模式

如果调用方持有用户的明文密码(例如用户自己输入),HZERO 原生支持标准 OAuth2 Password Grant,直接调用即可获取 Token,无需二次开发:

请求:

POST /oauth/oauth/token
Authorization: Basic <Base64(client_id:client_secret)>
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=用户账号&password=明文密码

返回示例:

{
  "access_token": "***",
  "token_type": "bearer",
  "refresh_token": "***",
  "expires_in": 32399999,
  "scope": "default"
}

注意事项:

  • 端点路径是 /oauth/oauth/token(两层 oauth 路径,不是 /oauth/token
  • client_idclient_secret 使用项目实际配置的客户端凭据
  • 密码传输明文,建议接口走 HTTPS
  • Token 有效期可通过 OAuth 服务端配置调整

方案二:无用户明文密码 — 自定义登录接口(二开)

如果调用方没有用户密码(例如服务间调用、管理端代操作等场景),需要在 hzero-oauth 服务中进行二次开发,实现自定义认证流程。

HZERO 已提供了标准扩展组件 LoginTokenService,核心开发步骤如下:

1. 自定义认证对象 AuthenticationToken

继承 AbstractAuthenticationToken,封装认证信息(如用户账号 + 加签字符串):

public class CustomAuthenticationToken extends AbstractAuthenticationToken {

    private final Serializable principal;  // 用户账号
    private final String sign;             // 加签字符串

    // 未认证构造方法
    public CustomAuthenticationToken(Serializable principal, String sign) {
        super(null);
        this.principal = principal;
        this.sign = sign;
        setAuthenticated(false);
    }

    // 已认证构造方法
    public CustomAuthenticationToken(Serializable principal,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.sign = null;
        super.setAuthenticated(true);
    }

    @Override
    public Serializable getCredentials() { return null; }

    @Override
    public Serializable getPrincipal() { return this.principal; }

    public String getSign() { return this.sign; }
}

2. 自定义认证器 AuthenticationProvider

实现 AuthenticationProvider 接口,在 authenticate() 方法中:

  • 校验加签字符串(自定义安全逻辑)
  • 通过 UserDetailsService 加载用户
  • 返回已认证的 Authentication
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) {
        CustomAuthenticationToken token = (CustomAuthenticationToken) authentication;
        String username = (String) token.getPrincipal();
        String sign = token.getSign();

        // 1. 校验加签字符串(自定义安全逻辑)
        // TODO: 实现你的签名校验逻辑,例如 HMAC/RSA 验签

        // 2. 加载用户
        UserDetails user = userDetailsService.loadUserByUsername(username);

        // 3. 返回已认证的 Authentication
        return new CustomAuthenticationToken(username, user.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return CustomAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

3. 自定义 LoginTokenService

继承 LoginTokenService,在 attemptAuthentication() 中从请求提取参数:

public class CustomLoginTokenService extends LoginTokenService {

    public CustomLoginTokenService(TokenGranter tokenGranter,
            ClientDetailsService clientDetailsService,
            OAuth2RequestFactory oAuth2RequestFactory,
            CustomAuthenticationProvider authenticationProvider) {
        super(tokenGranter, clientDetailsService, oAuth2RequestFactory, authenticationProvider);
    }

    @Override
    protected Authentication attemptAuthentication(HttpServletRequest request) {
        String username = request.getParameter("username");
        String sign = request.getParameter("sign");
        return new CustomAuthenticationToken(username, sign);
    }
}

4. 配置类

@Configuration
@AutoConfigureAfter(AuthorizationServerEndpointsConfiguration.class)
public class TokenConfiguration {

    private final AuthorizationServerEndpointsConfigurer endpoints = new AuthorizationServerEndpointsConfigurer();

    @Autowired
    private List<AuthorizationServerConfigurer> configurers = Collections.emptyList();

    @PostConstruct
    public void init() {
        for (AuthorizationServerConfigurer configurer : configurers) {
            try {
                configurer.configure(endpoints);
            } catch (Exception e) {
                throw new IllegalStateException("Cannot configure endpoints", e);
            }
        }
    }

    @Bean
    public CustomLoginTokenService customLoginTokenService(
            CustomAuthenticationProvider customAuthenticationProvider) {
        return new CustomLoginTokenService(
            endpoints.getTokenGranter(),
            endpoints.getClientDetailsService(),
            endpoints.getOAuth2RequestFactory(),
            customAuthenticationProvider
        );
    }
}

5. 登录接口

@RestController
public class CustomLoginController {

    @Autowired
    private CustomLoginTokenService customLoginTokenService;

    @PostMapping("/token/custom")
    public ResponseEntity<AuthenticationResult> loginToken(HttpServletRequest request) {
        AuthenticationResult result = customLoginTokenService.loginForToken(request);
        return Results.success(result);
    }
}

调用方式:

POST /oauth/token/custom
Content-Type: application/x-www-form-urlencoded

username=用户账号&sign=加签字符串&client_id=xxx&client_secret=***

三、详细文档参考

开放平台文档《定制登录获取令牌》:

https://open.hand-china.com/document-center/doc/product/10002/11014

文档中包含完整的类图说明和短信验证码登录的代码示例,可直接参考。另外文档目录中还有《非标单点登录示例》也可作为补充参考。

作者: Jack.shang

jack.shang 程序员->项目经理->技术总监->项目总监->部门总监->事业部总经理->子公司总经理->集团产品运营支持