Google Authenticator接入教程
1.引入依赖
<!--google authentication-->
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.1</version>
</dependency>
2.实现ICredentialRepository接口
import com.geotmt.saas.usercenter.server.service.UserGoogleAuthService;
import com.warrenstrange.googleauth.ICredentialRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class GoogleAuthCredentialRepository implements ICredentialRepository {
@Autowired
private UserGoogleAuthService googleAuthService;
@Override
public String getSecretKey(String userName) {
return googleAuthService.getSecretKey(userName);
}
@Override
public void saveUserCredentials(String userName, String secretKey, int validationCode, List<Integer> scratchCodes) {
googleAuthService.saveUserCredentials(userName, secretKey);
}
}
3.账户生成密钥(见createCredentials方法)
import com.geotmt.saas.usercenter.server.dao.DictMapper;
import com.geotmt.saas.usercenter.server.domain.Dict;
import com.geotmt.saas.usercenter.server.domain.DictExample;
import com.geotmt.saas.usercenter.server.util.Constans;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorConfig;
import com.warrenstrange.googleauth.ICredentialRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.frameworkset.util.ReflectionUtils;
import org.springframework.stereotype.Service;
import javax.servlet.ServletOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@Slf4j
@Service
public class GoogleAuthenticatorService {
private static final String KEY_FORMAT = "otpauth://totp/%s?secret=%s&issuer=%s";
private static final String IMAGE_EXT = "png";
private static final int WIDTH = 300;
private static final int HEIGHT = 300;
private static final int DEFAULT_WINDOW_SIZE = 3;
private static final String GOOGLE_AUTH_DIC_CODE = "google_authenticator_offset";
private final GoogleAuthenticator GOOGLE_AUTHENTICATOR;
private final GoogleAuthenticatorConfig authenticatorConfig;
private final DictMapper dictMapper;
public GoogleAuthenticatorService(ICredentialRepository credentialRepository, DictMapper dictMapper) {
authenticatorConfig = new GoogleAuthenticatorConfig.GoogleAuthenticatorConfigBuilder().build();
GOOGLE_AUTHENTICATOR = new GoogleAuthenticator(authenticatorConfig);
GOOGLE_AUTHENTICATOR.setCredentialRepository(credentialRepository);
log.info("GoogleAuthenticator初始化成功...");
this.dictMapper = dictMapper;
}
/**
* 生成二维码
*/
public String genQRImage(String username, String secretKey, ServletOutputStream stream) {
String content = null;
try {
content = getQrUrl(username, secretKey);
BitMatrix bm = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT);
MatrixToImageWriter.writeToStream(bm, IMAGE_EXT, stream);
} catch (WriterException | IOException e) {
log.error("genQRImage occur error", e);
}
return content;
}
/**
* 重置、新增、开启
*
* @param account
*/
public void createCredentials(String account) {
// 调用createCredentials都会生成新的secretKey
GOOGLE_AUTHENTICATOR.createCredentials(account);
}
/**
* 生成二维码链接
*/
private String getQrUrl(String username, String secretKey) throws UnsupportedEncodingException {
log.info("username={}, secretKey={}", username, secretKey);
return String.format(KEY_FORMAT, username, secretKey, URLEncoder.encode(Constans.SYS_NAME, "UTF-8").replace("+", "%20"));
}
}
4.生成绑定密钥和账户的二维码和校验
@Autowired
private GoogleAuthenticatorService googleAuthenticatorService;
@Autowired
private UserGoogleAuthService userGoogleAuthService;
@Autowired
private ApplicationContext applicationContext;
/**
* 生成二维码
*/
@GetMapping("/auth/qrcode")
@LogMethodAccess(title = "用户中心-获取OTP绑定二维码", businessType = BusinessType.OTP_CODE)
public GeoResponse<String> qrcode(String username, HttpServletResponse response) {
try (ServletOutputStream stream = response.getOutputStream()) {
Optional<UserGoogleAuth> authOptional = userGoogleAuthService.findByUserAccount(SecureUtils.decrypt(Constans.AES_SECRET,username));
if (!authOptional.isPresent()) {
return GeoResponse.wrap("not found");
}
UserGoogleAuth googleAuth = authOptional.get();
if((new Date().getTime() - googleAuth.getUpdateTime().getTime()) > 86400 * 1000){
return GeoResponse.wrap("请求二维码已过期!");
}
String content = googleAuthenticatorService.genQRImage(googleAuth.getUserAccount(), googleAuth.getSecretKey(), stream);
if (StringUtils.isEmpty(content)) {
return GeoResponse.wrap("not found");
}
return GeoResponse.wrap("success");
} catch (IOException e) {
log.error("generate qrcode occur error", e);
return new GeoResponse(e.getLocalizedMessage(), StatusCode.CODE_INTERNAL_ERROR);
}
}
/**
* 输入google authenticator上的6位数字
*/
@GetMapping("/auth/verify")
public GeoResponse<Boolean> verify(String username, int code) {
boolean validCode = googleAuthenticatorService.validCode(SecureUtils.decrypt(Constans.AES_SECRET,username), code);
return GeoResponse.wrap(validCode);
}
5.设置偏移
/**
* 验证code
*/
public boolean validCode(String username, int code) {
ICredentialRepository repository = GOOGLE_AUTHENTICATOR.getCredentialRepository();
if(StringUtils.isEmpty(username)){
log.warn("username 解密后为空");
return false;
}
String secretKey = repository.getSecretKey(username);
if (StringUtils.isEmpty(secretKey)) {
log.error("not found secretKey username: {}", username);
return false;
}
// 使用反射调用checkCode方法
Method method = ReflectionUtils.findMethod(GOOGLE_AUTHENTICATOR.getClass(),
"checkCode", String.class, long.class, long.class, int.class);
method.setAccessible(true);
try {
// calculate the num of time window
int offset = this.getGoogleAuthenticatorOffset();
int widowNum = offset == 0 ? DEFAULT_WINDOW_SIZE : (offset * 4 + 1);
return (Boolean) method.invoke(GOOGLE_AUTHENTICATOR, secretKey, code, System.currentTimeMillis(), widowNum);
} catch (Exception e) {
log.error("validCode ex: ", e);
return false;
}
}
参考:
https://cloud.tencent.com/developer/article/2310461