背景说明

使用Keycloak作为账号体系的项目中,经常会被问到Keycloak如何实现手机验证码登录,Keycloak有没有内置的基于短信的登录实现SMS-based two-/multi-factor-authentication (2FA/MFA) ;

Keycloak目前只内置一种基于 Google Authenticator 的 2FA 选项。

这篇主要讨论实现上述的需求的几种方案,以及相应的优缺点:

  • 定制 Authentication SPI,实现Keycloak统一的浏览器手机验证码登录;

  • 定制 Authentication SPI,实现基于 Resource Owner Password Credentials (Direct Access Grants)的手机验证码登录;

  • 使用 token-exchange

  • 使用 Identity Providers 实现手机验证码登录

定制 Authentication SPI,实现Keycloak统一的浏览器手机验证码登录

原理及实现

这种方式主要的工作原理是:

  1. 定制Keycloak登录主题,点击发送验证码有Keycloak服务进行验证码的发送,缓存和验证;

  2. 新增一个 keycloak-2fa-sms-authenticator 验证器,进行手机号和验证码的校验

具体实现代码可以参考:

对应的github地址

优缺点

优点是:

- 基于Keycloak标准扩展实现,安全风险可控- 基于浏览器登录,各业务统一的登录逻辑

缺点是:

- 浏览器登录在某些端,比如APP,并不适合- 短信发送集成到Keycloak,各个业务无法再支持自定义模板及信息定义;

基于 Resource Owner Password Credentials (Direct Access Grants)的手机验证码登录

Direct Access Grants概念

Resource Owner Password Credentials Grant (Direct Access Grants)

This is referred to in the Admin Console as Direct Access Grants. This is used by REST clients that want to obtain a token on behalf of a user. It is one HTTP POST request that contains the credentials of the user as well as the id of the client and the client’s secret (if it is a confidential client). The user’s credentials are sent within form parameters. The HTTP response contains identity, access, and refresh tokens.

相关的 RESTApi请求

curl --location --request POST 'http://localhost:8080/auth/realms/austintest/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=server-admin' \
--data-urlencode 'client_secret=ee0c1f08-775d-4195-b5ca-19eb9b923822' \
--data-urlencode 'username=admin1' \
--data-urlencode 'password=123456' \
--data-urlencode 'grant_type=password'

在后台的 Keycloak 的Flow中:

去掉Password 的Requirement 为 ALTERNATIVE即可

 curl --location --request POST 'http://localhost:8018/auth/realms/austin-local/protocol/openid-connect/token' --header 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'client_id=austin-app1' --data-urlencode 'client_secret=d622f435-4aad-48b7-bf18-e3cba0e4d76a' --data-urlencode 'username=admin1' --data-urlencode 'grant_type=password'

可以在client中单独设置Flow

优缺点

优点:

  • 验证码的发送和校验完全由业务方控制

  • 业务方可以很方便的进行拓展,不管是基于手机号验证码,还是邮箱验证码;

缺点:

  • 由于去掉了用户的密码校验,所以client获取用户令牌的安全级别下降,需要很小心的控制 client 是否开启 Direct Access Grants,以及client对应的scope;

  • 手机号对应的用户名,需要业务方自行保存,对应于手机号保存在业务方数据库的实现是方便,但是如果把手机号放在Keycloak的User的attribute中则还需要额外的定制修改。

实现手机号验证登录

定制ValidateUserAttributeAuthenticator

该校验器主要实现 Direct Access Grant 校验用户 的几种方式

  • 指定用户名校验用户

  • 指定用户邮箱校验用户

  • 根据全局设定的 属性名校验用户

  • 根据请求参数指定的属性名校验用户

  • 根据默认的属性名 phone 校验用户

可能出现的报错:

  • 未指定用户名,用户邮箱,以及属性值,会提示"Missing parameter: username"
String attributeValue = retrieveAttributeValue(context, attributeName);
if (username == null && attributeValue == null) {context.getEvent().error(Errors.USER_NOT_FOUND);Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Missing parameter: username");context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);return;
}

  • 指定的用户名或者邮箱,找到多个用户,提示: “Invalid user credentials”
UserModel user = null;
if (username != null) {try {user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);} catch (ModelDuplicateException mde) {logger.error(mde);Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Invalid user credentials");context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);return;}
}
  • 提供的属性名对应的属性值找到多个用户,提示: “Invalid user credentials is not unique”
// find User By attribute
if (user == null) {List<UserModel> users = context.getSession().users().searchForUserByUserAttributeStream(context.getRealm(), attributeName, attributeValue).collect(Collectors.toList());if (users.size() > 1) {logger.error(new ModelDuplicateException("User with " + attributeName + "=" + attributeValue + " is not unique!"));Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Invalid user credentials is not unique");context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);return;}if (users.size() == 1) {user = users.get(0);}
}
  • 提供的信息,找不到对应的用户,提示: “Invalid user credentials”
if (user == null) {context.getEvent().error(Errors.USER_NOT_FOUND);Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);return;
}

完整的authenticate 函数如下

@Override
public void authenticate(AuthenticationFlowContext context) {String username = retrieveUsername(context);AuthenticatorConfigModel config = context.getAuthenticatorConfig();String attributeName = null;if (config.getConfig() != null) {attributeName = config.getConfig().get("attributeName");} else {attributeName = retrieveAttributeValue(context, "attributeName");}if (attributeName == null) {attributeName = "phone";}String attributeValue = retrieveAttributeValue(context, attributeName);if (username == null && attributeValue == null) {context.getEvent().error(Errors.USER_NOT_FOUND);Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Missing parameter: username");context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);return;}UserModel user = null;if (username != null) {try {user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);} catch (ModelDuplicateException mde) {logger.error(mde);Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Invalid user credentials");context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);return;}}// find User By attributeif (user == null) {List<UserModel> users = context.getSession().users().searchForUserByUserAttributeStream(context.getRealm(), attributeName, attributeValue).collect(Collectors.toList());if (users.size() > 1) {logger.error(new ModelDuplicateException("User with " + attributeName + "=" + attributeValue + " is not unique!"));Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Invalid user credentials is not unique");context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);return;}if (users.size() == 1) {user = users.get(0);}}if (user == null) {context.getEvent().error(Errors.USER_NOT_FOUND);Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);return;}context.getEvent().detail(Details.USERNAME, user.getUsername());context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, user.getUsername());context.setUser(user);context.success();
enticator.ATTEMPTED_USERNAME, user.getUsername());context.setUser(user);context.success();
}

指定属性值名称的逻辑

  1. 配置中的属性名称优先级最高,考虑的原则是管理员设置的安全性最高;

  2. 请求参数中的 “attributeName” 的值;

  3. 默认的 “phone”;

打包部署Provider

  • META-INF.services 添加提供器信息

  • maven 编译
mvn clean install
  • 部署Jar包

把生成target下的jar包,拷贝到 $KC_HOME/standalone/deployments 目录下

keycloak启动时会自动解析加载该提供器

  • 确认安装成功

登录Keycloak 管理控制台,在右上角下拉菜单中,选择 Server Info

查看Providers, 搜索我们的提供器ID,查看是否存在

定制校验流程并绑定

  • 在Authentication的Flows中选择Direct Grant,并进行拷贝,命名为Direct Grant User Attribute

  • 删除原有的Username Validation

  • 添加执行器,选择我们定制的

添加后点击左侧箭头,移动到最顶部

  • 配置属性名

  • 全局绑定

  • 客户端作用域绑定

测试验证

创建好两个测试用户,对应的属性值如下

  • admin1

其中 两个 mobile的属性值一样;

phone 正确

phone 找不到

修改配置,指定属性名为openid

删除配置,就可以通过 attributeName 指定业务自己的属性名

小结

到这里基本上就实现了手机验证码登录的需求,这里的发送验证码,校验验证由各个可信业务方进行处理。

我们根据客户端的是否开启direct Grant,客户端scope,以及客户端绑定校验流程,严格控制相关权限的发放。

Keycloak实现手机验证码登录相关推荐

  1. 阿里云短信平台实现手机验证码登录

    阿里云短信平台实现手机验证码登录 首先创建一个工具类 工具类AliyunMessageUtil代码如下所示: public class AliyunMessageUtil {private stati ...

  2. 验证码登录开发----手机验证码登录

    手机验证码登录 需求分析 为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能 手机验证码登录的优点: 方便快捷.无需注册,直接登录 使用短信验证码作为登录凭证,无需记忆密码 安全 登录流程: ...

  3. Flutter学习第十五天:2021年最新版超详细Flutter实现Mob+SMSSDK手机验证码登录实现,Android和Flutter混合开发?

    Flutter实现手机验证码登录 第一步:在mob平台配置SMSSDK环境 第二步:建立flutter项目和android的library文件 第三步:在Android的library文件中部署mob ...

  4. 技术人员需要了解的手机验证码登录风险

    手机验证码登录是一种常见的应用登录方式,简单方便,不用记忆密码,市面上能见到的APP基本都支持这种登录方式,很多应用还把登录和注册集成到了一起,注册+登录一气呵成,给用户省去了很多麻烦,颇有一机在手. ...

  5. 【瑞吉外卖】day08:短信发送、手机验证码登录

    目录 4. 短信发送​编辑 4.1 短信服务介绍 4.2 阿里云短信服务介绍 4.3 阿里云短信服务准备 4.4 代码开发 5. 手机验证码登录 5.1 需求分析 5.2 数据模型 5.3 前端页面分 ...

  6. Java实现手机验证码登录和SpringSecurity权限控制

    手机验证码登录和SpringSecurity权限控制 手机快速登录功能,就是通过短信验证码的方式进行登录.这种方式相对于用户名密码登录方式,用户不需要记忆自己的密码,只需要通过输入手机号并获取验证码就 ...

  7. 瑞吉外卖(6)—手机验证码登录

    目录 一.手机验证码登录 1.1 短信发送 1.2 短信验证码登陆 1.2.1 需求分析 1.2.2 数据模型 1.2.3 代码开发 发送验证码(给的资料有点残缺,这里修改了) 使用验证码登陆(使用m ...

  8. 【瑞吉外卖项目】DAY5——第六章 手机验证码登录

    本章内容介绍手机验证码登录 点击获取验证码 收到短信,并输入验证码 点击登录,登录成功 短信发送_短信服务介绍和阿里云短信服务介绍 短信服务介绍 目前市面上有很多第三方提供的短信服务,这些第三方短信服 ...

  9. SpringSecurityOAuth2(7) 账号密码登录、手机验证码登录

    GitHub地址 码云地址 SpringSecurity 调用流程: 首先会进入UsernamePasswordAuthenticationFilter并且设置权限为null和是否授权为false,然 ...

最新文章

  1. linux 中samba账号登录密码,ubuntu下的Samba配置:使每个用户可以用自己的用户名和密码登录自己的home目录...
  2. 三星s8和android auto,手机资讯导报:穿上马甲也认得三星GalaxyS8与LGG6再曝光
  3. Scala学习(二)--- 控制结构和函数
  4. Linux下 安装Redis并配置服务
  5. 交换机跟计算机系统有关系,网速跟交换机有关系吗
  6. 用mycat做读写分离:基于 MySQL主从复制
  7. pgslq表的字段类型_Python 爬取微信公众号文章和评论 (基于 Fiddler 抓包分析)
  8. GaussDB(openGauss)宣布开源,性能超越 MySQL 与 PostgreSQL
  9. OSChina 周五乱弹 ——变态要从娃娃抓起
  10. 兄弟9055cdn硒鼓清零_dcp—9020cdn硒鼓怎么清零
  11. 创业公司如何切入巨头垄断的芯片市场?
  12. 爬取豆瓣评论连接mysql_Scrapy爬取豆瓣图书数据并写入MySQL
  13. mysql 几个超时参数(timeout)解释
  14. python 求解二次规划(quadprog)
  15. 电阻分压可以当作电源供电吗
  16. ExtJS中的renderTo何applyTo的差别
  17. Android 杂记 - 存货盘点用的客户端
  18. phpmywind 子菜单调用
  19. SSM疫情防控志愿者管理系统 志愿者服务信息系统 大学志愿者管理系统Java
  20. 关于VS编译跨端工程出现error C2059的一个解决方案

热门文章

  1. offer--刷题之路(持续更新)
  2. php网站开发教程下载_《PHP网站开发实例教程》源代码 全面的PHP案例源代码 - 下载 - 搜珍网...
  3. 使用Bcrypt进行密码加密
  4. jsp执行原理(详解)
  5. 微信小程序实现导航功能
  6. 那么问题来了? int(a/b) 和 a//b 的区别在哪里呢? 例1:
  7. 让群众少跑腿数据多跑路,华为云Stack助力上海政务跑出“极速”
  8. Retrofit2网络请求的path部分的“/”斜杠乱码为“百分号2F”,请求结果为400的请求无效
  9. 航空航天空气动力学高性能计算解决方案
  10. 游戏中子弹的回收重用