keycloak实现手机验证码登录(兼容账号密码登录)

文章介绍了如何在Keycloak中实现手机验证码登录功能,包括添加自定义认证SPI(如SmsAuthenticatorFactory和SmsAuthenticator),配置依赖,以及自定义登录流程。作者详细展示了如何实现自定义认证工厂和登录页面的交互过程。
摘要由CSDN通过智能技术生成

keycloak实现手机验证码登录(兼容账号密码登录)

keycloak自身只能通过账号密码登录,为了实现手机验证码登录,需要实现自定义的认证SPI+自定义登录页。废话不多说,直接上方法

自定义认证SPI

1.添加依赖

<dependency>
	<groupId>org.keycloak</groupId>
	<artifactId>keycloak-server-spi</artifactId>
	<version>19.0.2</version>
</dependency>
<dependency>
	<groupId>org.keycloak</groupId>
	<artifactId>keycloak-server-spi-private</artifactId>
	<version>19.0.2</version>
</dependency>
<dependency>
	<groupId>org.keycloak</groupId>
	<artifactId>keycloak-services</artifactId>
	<version>19.0.2</version>
</dependency>

2.实现自定义认证SPI

需要实现两个类,可以用一个新的java项目来实现
1)实现AuthenticatorFactory

@AutoService(AuthenticatorFactory.class)
public class SmsAuthenticatorFactory implements AuthenticatorFactory {
	// 这个属性用于注册SPI用,需要定义好
	public static final String PROVIDER_ID = "sms-authenticator";

	private static final SmsAuthenticator SINGLETON = new SmsAuthenticator();

	@Override
	public String getId() {
		return PROVIDER_ID;
	}

	@Override
	public String getDisplayType() {
		return "SMS Authentication";
	}

	@Override
	public String getHelpText() {
		return "Validates an OTP sent via SMS to the users mobile phone.";
	}

	@Override
	public String getReferenceCategory() {
		return "otp";
	}

	@Override
	public boolean isConfigurable() {
		return true;
	}

	@Override
	public boolean isUserSetupAllowed() {
		return false;
	}

	@Override
	public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
		return REQUIREMENT_CHOICES;
	}

	@Override
	public List<ProviderConfigProperty> getConfigProperties() {
		// 可以在这里自定义认证属性,在这里添加了测试用的一些按钮
		return List.of(
				new ProviderConfigProperty(SmsConstants.CODE_LOGIN_ENABLED, "是否启用手机验证码登录", "是否启用手机验证码登录,不启用将以用户名密码登录", ProviderConfigProperty.BOOLEAN_TYPE, true),
				new ProviderConfigProperty(SmsConstants.SIMULATION_MODE, "测试模式", "在测试模式中,不会发送手机验证码,验证码将以日志输出方式显示", ProviderConfigProperty.BOOLEAN_TYPE, false),
				new ProviderConfigProperty(SmsConstants.OMNIPOTENT_CODE_ENABLED, "是否启用万能验证码", "是否启用万能验证码,生产上不可开启!!", ProviderConfigProperty.BOOLEAN_TYPE, false),
				new ProviderConfigProperty(SmsConstants.OMNIPOTENT_CODE, "万能验证码", "需要启用万能验证码后才生效!", ProviderConfigProperty.STRING_TYPE, "887154")
		);
	}

	@Override
	public Authenticator create(KeycloakSession session) {
		return SINGLETON;
	}

	@Override
	public void init(Config.Scope config) {
	}

	@Override
	public void postInit(KeycloakSessionFactory factory) {
	}

	@Override
	public void close() {
	}

}

2)因为需要用到前端表达,所以需要继承AbstractUsernameFormAuthenticator并且实现Authenticator。

public class SmsAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {

	private static final String MOBILE_NUMBER_FIELD = "mobile_number";

	@Override
	public void authenticate(AuthenticationFlowContext context) {
		MultivaluedMap<String, String> formData = new MultivaluedMapImpl<>();
		String loginHint = context.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM);
		String rememberMeUsername = AuthenticationManager.getRememberMeUsername(context.getRealm(), context.getHttpRequest().getHttpHeaders());

		if (context.getUser() != null) {
			LoginFormsProvider form = context.form();
			form.setAttribute(LoginFormsProvider.USERNAME_HIDDEN, true);
			form.setAttribute(LoginFormsProvider.REGISTRATION_DISABLED, true);
			context.getAuthenticationSession().setAuthNote(USER_SET_BEFORE_USERNAME_PASSWORD_AUTH, "true");
		} else {
			context.getAuthenticationSession().removeAuthNote(USER_SET_BEFORE_USERNAME_PASSWORD_AUTH);
			if (loginHint != null || rememberMeUsername != null) {
				if (loginHint != null) {
					formData.add(AuthenticationManager.FORM_USERNAME, loginHint);
				} else {
					formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername);
					formData.add("rememberMe", "on");
				}
			}
		}
		Response challengeResponse = challenge(context, formData);
		context.challenge(challengeResponse);
	}

	@Override
	public void action(AuthenticationFlowContext context) {
		AuthenticatorConfigModel config = context.getAuthenticatorConfig();
		Boolean codeEnabled = BooleanUtil.toBoolean(config.getConfig().getOrDefault(SmsConstants.CODE_LOGIN_ENABLED, "true"));
		Boolean omnipotentCodeEnabled = BooleanUtil.toBoolean(config.getConfig().getOrDefault(SmsConstants.OMNIPOTENT_CODE_ENABLED, "false"));
		String omnipotentCode = config.getConfig().get(SmsConstants.OMNIPOTENT_CODE);
		MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
		RealmModel realmModel = context.getRealm();
		if (formData.containsKey("cancel")) {
			context.cancelLogin();
			return;
		}
		if(formData.containsKey("grant_type") && formData.get("grant_type").get(0).equalsIgnoreCase("code") && BooleanUtil.isTrue(codeEnabled)) {
			// 手机验证码登录
			if(!validatePhoneForm(context, formData, realmModel,omnipotentCodeEnabled,omnipotentCode)){
				return;
			}
		} else if (!validateForm(context, formData)) {
			// 账号密码登录
			return;
		}
		context.success();
	}

	@Override
	public boolean requiresUser() {
		return false;
	}

	@Override
	public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
		return user.getFirstAttribute(MOBILE_NUMBER_FIELD) != null;
	}

	@Override
	public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
		// this will only work if you have the required action from here configured:
		// https://github.com/dasniko/keycloak-extensions-demo/tree/main/requiredaction
		user.addRequiredAction("mobile-number-ra");
	}

	@Override
	public void close() {
	}

	protected Response challenge(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
		LoginFormsProvider forms = context.form();

		if (formData.size() > 0) forms.setFormData(formData);

		return forms.createLoginUsernamePassword();
	}

	protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
		return validateUserAndPassword(context, formData);
	}

	protected boolean validatePhoneForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData, RealmModel realmModel,Boolean omnipotentCodeEnabled,String omnipotentCode) {
		// 通过手机号获取用户名;
		return true;
	}

	protected Response handleError(AuthenticationFlowContext context, String error, String fieId){
		Response challengeResponse = this.challenge(context, error, fieId);
		return challengeResponse;
	}

}

附上一些用到的常量

public class SmsConstants {
	public String SIMULATION_MODE = "simulation";

	public String CODE_LOGIN_ENABLED = "code_login_enabled";

	public String OMNIPOTENT_CODE = "omnipotent_code";

	public String OMNIPOTENT_CODE_ENABLED = "omnipotent_code_enabled";
}

3)实现完这两个类之后,打包成jar

4)因为我用的是官网下载的容器版keycloak,所以需要把这个jar放进去
用了docker-compose部署的示例:
![在这里插入图片描述](https://img-blog.csdnimg.cn/3091b48b333448a1906d29a4e25f15e6.png

将jar放入providers目录下后挂入容器中
然后进入容器中执行以下命令

bin/kc.sh build --spi-hostname-provider=sms-authenticator

验证是否成功:进入master管理页面,搜索一下是否存在sms-authenticator
在这里插入图片描述
在这里插入图片描述

如果存在,那已经成功了一半。

3.自定义认证流程

1)在认证页面新建流程
在这里插入图片描述
2)按以下流程图添加
在这里插入图片描述
3)开启测试用的验证码
在这里插入图片描述
4)启用流程
在这里插入图片描述
在这里插入图片描述
到此,已经完成了自定义认证SPI,并自定义认证流程!

自定义登录主题

要实现手机验证码登录,必须自定义登录主题
大概思路是:保留原有的登录表单,新增一个手机验证码的表单,通过按钮进行切换。使用原本的认证接口,用grant_typel来区分是账号密码登录还是手机号码登录。
大家有兴趣可以看下官方文档keycloak主题

demo演示

在这里插入图片描述
在这里插入图片描述

  • 13
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Keycloak是一个开源的身份和访问管理解决方案,它为应用程序和服务提供了安全的用户身份验证和授权功能。它支持多种身份提供者和协议,包括微信登录。 要使用Keycloak实现小程序的微信登录,首先需要在Keycloak中配置微信作为一个身份提供者。这需要获取微信开放平台的App ID和App Secret,将它们添加到Keycloak的身份提供者配置中。 一旦配置完成,开发人员可以在小程序中实现微信登录功能。首先,用户需要点击登录按钮来触发微信登录请求。在后台,开发人员需要将登录请求发送到Keycloak服务器,并将微信的App ID和App Secret作为参数一起发送。 Keycloak服务器会验证这些参数,并将微信的授权登录页面返回给小程序。用户将在微信授权登录页面上进行身份验证,并授权小程序访问其个人信息。 验证成功后,Keycloak服务器将通知小程序,并返回一个授权码。小程序将授权码发送回Keycloak服务器,以获取访问令牌和刷新令牌。 一旦小程序获得访问令牌和刷新令牌,它可以将其存储在本地,并将其用于后续的API调用和身份验证。这样,用户可以使用微信账号登录小程序,并访问其个人信息和其他受保护的资源。 总而言之,使用Keycloak实现小程序的微信登录需要在Keycloak中配置微信作为一个身份提供者,并在小程序中实现Keycloak的交互,以获取访问令牌和刷新令牌。这样,用户可以使用微信账号登录小程序,并进行身份验证和访问受保护的资源。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值