内容目录
一、authenticator解决了什么问题二、authenticator的原理三、springboot集成authenticator四、做成可复用starter五、参考
谷歌Authenticator是谷歌推出的一种双因素身份验证应用程序。它是一种为用户提供额外层次的账户保护的安全工具。传统的认证方式通常只依赖于用户名和密码,而双因素身份验证则需要用户提供两个不同类型的验证信息,以增加账户的安全性。
谷歌Authenticator通过生成动态的一次性密码来实现双因素身份验证。当你登录一个已启用谷歌Authenticator的系统或服务时时,需要输入用户名和密码,然后打开 Authenticator 应用来获取当前的一次性密码。这个密码每30秒钟更新一次,只在短暂的时间段内有效。
这种一次性密码是通过基于时间戳的算法计算得出的,同时还需要通过与账户绑定的密钥进行验证。由于每个密码只在极短的时间内有效,并且密码是动态变化的,即使有人获得了您的用户名和密码,他们也无法成功登录您的账户,因为他们没有有效的一次性密码。
谷歌Authenticator还可以与多个账户关联,这意味着您可以在一个应用中管理多个账户的一次性密码。它对于保护您的各种在线账户(如电子邮件、社交媒体、金融服务)非常有用。
简而言之,谷歌Authenticator是一种提供额外层次安全保护的双因素身份验证应用程序。它通过生成动态的一次性密码来增加账户的安全性,并在登录过程中要求用户提供额外的验证信息。
谷歌Authenticator本质上解决了以下问题:
总之,谷歌Authenticator增加了双因素身份验证的安全性,提供了一种简便而有效的方式来保护您的帐户免受未经授权访问和针对性攻击的威胁。
谷歌Authenticator是基于TOTP算法实现的验证方式,TOTP(Time-Based One-Time Password是谷歌Authenticator中使用的一种身份验证方法。它基于时间的动态密码算法,用于生成一次性密码(One-Time Passwords)。
当启用谷歌Authenticator并为特定帐户配置时,它会与该帐户关联一个密钥。该密钥私密地存储在您的设备上。每30秒钟,该密钥都会与当前时间戳进行计算,并生成一个新的一次性密码。
当您需要进行身份验证时,您可以打开谷歌Authenticator应用程序,输入相关帐户的用户名,然后应用程序会基于与服务器同步的时间戳生成相应的一次性密码。您将此密码输入到身份验证页面或应用程序中,以确认您是合法用户。
TOTP 提供了一种额外的安全层次,因为即使有人获得了您的用户名和密码,仍然需要一个有效的一次性密码才能访问您的帐户。这增加了保护您的帐户免受未经授权访问的可能性。
TOTP是HOTP的一个变种,将HOTP中的计数器C替换为依托时间的参数T,T是由当前时间(CurrentUnixTime、初始时间(T0)、步长(X)决定的。即:
$$ T = (Current Unix time - T0) / X $$
TOTP 是谷歌 Authenticator 中使用的基于时间的动态密码算法,提供了一种增强的身份验证机制,以确保只有授权用户能够访问其帐户。
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
</dependency>
googleauth 是一个开源的 Java 库,用于在 Java 应用程序中实现谷歌 Authenticator 功能。它提供了一组类和方法,让开发人员能够轻松地集成谷歌 Authenticator 的功能到他们的 Java 应用程序中。googleauth 库使用谷歌 Authenticator 的算法来生成一次性密码,并提供了验证这些密码的功能。
编写生成秘钥、二维码以及code验证工具类:
@Slf4j
@Service
public class GoogleAuthenticatorService {
private static final GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator();
private static final String KEY_FORMAT = "otpauth://totp/%s?secret=%s";
private static final String IMAGE_EXT = "png";
private static final int WIDTH = 300;
private static final int HEIGHT = 300;
@Autowired
private UserDao userDao;
@PostConstruct
public void init() {
googleAuthenticator.setCredentialRepository(new ICredentialRepository() {
@Override
public String getSecretKey(String userName) {
return userDao.getSecretKey(userName);
}
@Override
public void saveUserCredentials(String userName, String secretKey, int validationCode, List<Integer> scratchCodes) {
userDao.saveUserCredentials(userName, secretKey);
}
});
log.info("GoogleAuthenticator初始化成功...");
}
/**
* 生成二维码链接
*/
private String getQrUrl(String username) {
//调用createCredentials都会生成新的secretKey
GoogleAuthenticatorKey key = googleAuthenticator.createCredentials(username);
log.info("username={},secretKey={}", username, key.getKey());
return String.format(KEY_FORMAT, username, key.getKey());
}
/**
* 验证code
*/
public boolean validCode(String username, int code) {
return googleAuthenticator.authorizeUser(username, code);
}
/**
* 生成二维码
*/
public void genQRImage(String username, ServletOutputStream stream) {
try {
String content = getQrUrl(username);
BitMatrix bm = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT);
MatrixToImageWriter.writeToStream(bm, IMAGE_EXT, stream);
} catch (WriterException | IOException e) {
log.error("occur error",e);
}
}
}
编写逻辑入口:
@Slf4j
@Controller
public class IndexController {
@Autowired
private GoogleAuthenticatorService googleAuthenticatorService;
/**
* 二次验证,生成二维码
*/
@RequestMapping("/qrcode")
public void qrcode(String username, HttpServletResponse response) {
try (ServletOutputStream stream = response.getOutputStream()) {
googleAuthenticatorService.genQRImage(username, stream);
} catch (IOException e) {
log.error("occur error", e);
}
}
/**
* 二次验证,输入google authenticator上的6位数字,成功跳转到首页
*/
@RequestMapping("/verify")
public String verify(String username, int code) {
boolean validCode = googleAuthenticatorService.validCode(username, code);
if (validCode){
return "index";
}
return "error";
}
}
用户登录成功后,需要验证authenticator验证码才能跳转到引导页,也就说明一次性验证码验证态与登录态是强绑定的。
拦截登录接口,把登录路径做成配置化,由starter读取,登录完成后跳转到绑定authenticator或者输入验证码。并且对于其他路径也要同时校验登录态和TOTP验证态。
通过配置控制开启全局authenticator验证能力,包含绑定和code验证。
在用户管理维度,做成可视化能力,可在用户粒度控制是否启用authenticator二次认证
前边我们把接入authenticator二次验证直接写入到了springboot项目中,那么如果有其他项目要接入,还是要从头到尾写一遍,所以我们可以写成springboot-starter的方式,做成可复用的能力。
属性配置:
@ConfigurationProperties(prefix = "application.url")
@Data
public class GoogleAuthenticatorProperties {
//是否启用
private boolean enabled;
//注册路径
private String register;
//登录路径
private String login;
//验证code路径
private String verify;
}
设置是否启用Authenticator以及登录注册路径。
拦截器:
@Slf4j
public class GoogleAuthenticatorInterceptor implements HandlerInterceptor {
@Autowired
GoogleAuthenticatorProperties googleAuthenticatorProperties;
@Autowired
private GoogleAuthenticatorService googleAuthenticatorService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getRequestURI();
if(Objects.equals(url,googleAuthenticatorProperties.getRegister())
|| Objects.equals(url,googleAuthenticatorProperties.getLogin())
|| Objects.equals(url,googleAuthenticatorProperties.getVerify())) {
return true;
}
String token = request.getHeader("token");
//todo 检查token有效性和合法性,以及从服务端能否拿到TOTP验证态
if(!(this.checkTokenValid(token) && this.checkTOTPValid(token))) {
return false;
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
String url = request.getRequestURI();
String username = request.getParameter("username");
//如果是登录,登录成功后跳转到绑定Authenticator秘钥或者输入code
if(Objects.equals(url,googleAuthenticatorProperties.getLogin())) {
try (ServletOutputStream stream = response.getOutputStream()) {
googleAuthenticatorService.genQRImage(username, stream);
return;
} catch (IOException e) {
log.error("发生错误", e);
}
}
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
}
如果是登录、注册或者验证code路径,preHandle跳过校验,否则检查用户登录态和code验证态。对于postHandle处理完请求返回数据视图之前,如果检查是登录请求,那么返回绑定TOTP验证二维码或者输入code表单。
自动注入配置类:
@Configuration
@EnableConfigurationProperties(GoogleAuthenticatorProperties.class)
@ConditionalOnProperty(prefix = "application.url", name = "enabled", havingValue = "true")
public class GoogleAuthAutoConfiguration {
@Bean
public GoogleAuthenticatorService googleAuthenticatorService() {
return new GoogleAuthenticatorService();
}
@Bean
public GoogleAuthenticatorInterceptor googleAuthenticatorInterceptor() {
return new GoogleAuthenticatorInterceptor();
}
@Configuration
public class CustomInterceptorConfig implements WebMvcConfigurer {
private GoogleAuthenticatorInterceptor googleAuthenticatorInterceptor;
public CustomInterceptorConfig() {
googleAuthenticatorInterceptor = googleAuthenticatorInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(googleAuthenticatorInterceptor)
.addPathPatterns("/**") // 可以根据需要指定拦截的路径
.order(Ordered.LOWEST_PRECEDENCE); // 设置执行顺序为最低优先级
}
}
}
主要生命相关依赖的bean以及拦截器,拦截器优先级会放到比较低的位置,从而不影响springboot项目本身的拦截器执行顺序。
入口配置:
在starter模块创建META-INF/spring.factories配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
authenticator.GoogleAuthAutoConfiguration
在依赖该starter的springboot项目启动时会自动加载并解析实例化配置类。
上述步骤是主要的步骤和实现,在具体编码实现时还需要调整和打磨细节问题,完成上述步骤后把starter打成jar,在springboot应用引入并做好相关配置就能使用authenticator能力了。
https://github.com/wstrange/GoogleAuth
https://rstyro.github.io/blog/2019/04/29/SpringBoot-Google%E4%BA%8C%E6%AD%A5%E9%AA%8C%E8%AF%81/
https://www.cnblogs.com/50614090/p/5848409.html
https://juejin.cn/post/6898891886677721102
本文分享自 PersistentCoder 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!