From a098df18a3666e3a7a76a4fa481b353eab2d9d3e Mon Sep 17 00:00:00 2001 From: shimingxy Date: Tue, 27 Aug 2024 17:33:21 +0800 Subject: [PATCH] =?UTF-8?q?timebased=20otp=20=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{RQCodeUtils.java => QRCodeUtils.java} | 2 +- .../maxkey/entity/dto/TimeBasedDto.java | 5 + .../onetimepwd/impl/TimeBasedOtpAuthn.java | 9 +- .../src/app/entity/TimeBased.ts | 5 +- .../config/timebased/timebased.component.html | 87 ++++--------- .../config/timebased/timebased.component.ts | 38 ++++-- .../src/app/service/time-based.service.ts | 16 ++- .../maxkey-web-app/src/assets/i18n/en-US.json | 2 +- .../maxkey-web-app/src/assets/i18n/zh-CN.json | 2 +- .../maxkey-web-app/src/assets/i18n/zh-TW.json | 2 +- .../web/contorller/LoginEntryPoint.java | 4 +- .../contorller/OneTimePasswordController.java | 120 ++++++++++-------- 12 files changed, 151 insertions(+), 141 deletions(-) rename maxkey-common/src/main/java/org/dromara/maxkey/util/{RQCodeUtils.java => QRCodeUtils.java} (98%) create mode 100644 maxkey-core/src/main/java/org/dromara/maxkey/entity/dto/TimeBasedDto.java diff --git a/maxkey-common/src/main/java/org/dromara/maxkey/util/RQCodeUtils.java b/maxkey-common/src/main/java/org/dromara/maxkey/util/QRCodeUtils.java similarity index 98% rename from maxkey-common/src/main/java/org/dromara/maxkey/util/RQCodeUtils.java rename to maxkey-common/src/main/java/org/dromara/maxkey/util/QRCodeUtils.java index 9122399aa..cbae20f91 100644 --- a/maxkey-common/src/main/java/org/dromara/maxkey/util/RQCodeUtils.java +++ b/maxkey-common/src/main/java/org/dromara/maxkey/util/QRCodeUtils.java @@ -25,7 +25,7 @@ import com.google.zxing.BarcodeFormat; import com.google.zxing.MultiFormatWriter; import com.google.zxing.common.BitMatrix; -public class RQCodeUtils { +public class QRCodeUtils { public static void write2File(String path,String rqCodeText,String format,int width, int height ){ diff --git a/maxkey-core/src/main/java/org/dromara/maxkey/entity/dto/TimeBasedDto.java b/maxkey-core/src/main/java/org/dromara/maxkey/entity/dto/TimeBasedDto.java new file mode 100644 index 000000000..e639fd161 --- /dev/null +++ b/maxkey-core/src/main/java/org/dromara/maxkey/entity/dto/TimeBasedDto.java @@ -0,0 +1,5 @@ +package org.dromara.maxkey.entity.dto; + +public record TimeBasedDto(String displayName,String username,int digits,int period,String sharedSecret,String qrCode,String otpCode) { + +} diff --git a/maxkey-starter/maxkey-starter-otp/src/main/java/org/dromara/maxkey/password/onetimepwd/impl/TimeBasedOtpAuthn.java b/maxkey-starter/maxkey-starter-otp/src/main/java/org/dromara/maxkey/password/onetimepwd/impl/TimeBasedOtpAuthn.java index a5ad734c2..a3c3fad1d 100644 --- a/maxkey-starter/maxkey-starter-otp/src/main/java/org/dromara/maxkey/password/onetimepwd/impl/TimeBasedOtpAuthn.java +++ b/maxkey-starter/maxkey-starter-otp/src/main/java/org/dromara/maxkey/password/onetimepwd/impl/TimeBasedOtpAuthn.java @@ -51,10 +51,9 @@ public class TimeBasedOtpAuthn extends AbstractOtpAuthn { @Override public boolean validate(UserInfo userInfo, String token) { - _logger.debug("utcTime : " + dateFormat.format(new Date())); + _logger.debug("utcTime : {}" , dateFormat.format(new Date())); long currentTimeSeconds = System.currentTimeMillis() / 1000; - String sharedSecret = - PasswordReciprocal.getInstance().decoder(userInfo.getSharedSecret()); + String sharedSecret = PasswordReciprocal.getInstance().decoder(userInfo.getSharedSecret()); byte[] byteSharedSecret = Base32Utils.decode(sharedSecret); String hexSharedSecret = Hex.encodeHexString(byteSharedSecret); String timeBasedToken = ""; @@ -74,8 +73,8 @@ public class TimeBasedOtpAuthn extends AbstractOtpAuthn { Long.toHexString(currentTimeSeconds / interval).toUpperCase() + "", digits + ""); } - _logger.debug("token : " + token); - _logger.debug("timeBasedToken : " + timeBasedToken); + _logger.debug("token : {}" , token); + _logger.debug("timeBasedToken {}: " , timeBasedToken); if (token.equalsIgnoreCase(timeBasedToken)) { return true; } diff --git a/maxkey-web-frontend/maxkey-web-app/src/app/entity/TimeBased.ts b/maxkey-web-frontend/maxkey-web-app/src/app/entity/TimeBased.ts index 68948979b..082f631b5 100644 --- a/maxkey-web-frontend/maxkey-web-app/src/app/entity/TimeBased.ts +++ b/maxkey-web-frontend/maxkey-web-app/src/app/entity/TimeBased.ts @@ -24,7 +24,8 @@ export class TimeBased extends BaseEntity { digits!: String; period!: String; sharedSecret!: String; + formatSharedSecret!: String; hexSharedSecret!: String; - rqCode!: String; - otp!: string; + qrCode!: String; + otpCode!: string; } diff --git a/maxkey-web-frontend/maxkey-web-app/src/app/routes/config/timebased/timebased.component.html b/maxkey-web-frontend/maxkey-web-app/src/app/routes/config/timebased/timebased.component.html index 4e074edaa..7a9914a67 100644 --- a/maxkey-web-frontend/maxkey-web-app/src/app/routes/config/timebased/timebased.component.html +++ b/maxkey-web-frontend/maxkey-web-app/src/app/routes/config/timebased/timebased.component.html @@ -1,99 +1,68 @@ -
+
-
+
id - + - {{ 'mxk.timebased.displayName' | i18n }} + {{ 'mxk.timebased.displayName' | i18n + }} - + {{ 'mxk.timebased.username' | i18n }} - + {{ 'mxk.timebased.digits' | i18n }} - + {{ 'mxk.timebased.period' | i18n }} - + - {{ 'mxk.timebased.sharedSecret' | i18n }} + {{ 'mxk.timebased.sharedSecret' | i18n + }} - + - {{ 'mxk.timebased.hexSharedSecret' | i18n }} - - - - - - {{ 'mxk.timebased.one-timePassword' | i18n }} + {{ 'mxk.timebased.one-timePassword' | i18n + }} - + - - - - + + +
-
+ \ No newline at end of file diff --git a/maxkey-web-frontend/maxkey-web-app/src/app/routes/config/timebased/timebased.component.ts b/maxkey-web-frontend/maxkey-web-app/src/app/routes/config/timebased/timebased.component.ts index 00ac5b2b2..0d3ef618e 100644 --- a/maxkey-web-frontend/maxkey-web-app/src/app/routes/config/timebased/timebased.component.ts +++ b/maxkey-web-frontend/maxkey-web-app/src/app/routes/config/timebased/timebased.component.ts @@ -28,16 +28,16 @@ import { Console } from 'console'; @Component({ selector: 'app-timebased', templateUrl: './timebased.component.html', - styleUrls: ['./timebased.component.less'], + styleUrls: ['./timebased.component.less'] }) export class TimebasedComponent implements OnInit { form: { submitting: boolean; model: TimeBased; } = { - submitting: false, - model: new TimeBased() - }; + submitting: false, + model: new TimeBased() + }; isDisabled = true; @@ -48,7 +48,7 @@ export class TimebasedComponent implements OnInit { private timeBasedService: TimeBasedService, private msg: NzMessageService, private cdr: ChangeDetectorRef - ) {} + ) { } ngOnInit(): void { /*this.form = this.fb.group({ @@ -62,7 +62,7 @@ export class TimebasedComponent implements OnInit { public: [1, [Validators.min(1), Validators.max(3)]], publicUsers: [null, []] });*/ - this.timeBasedService.get('').subscribe(res => { + this.timeBasedService.view('').subscribe(res => { this.form.model.init(res.data); this.formatSecret(); this.cdr.detectChanges(); @@ -70,17 +70,31 @@ export class TimebasedComponent implements OnInit { } formatSecret(): void { - this.form.model.sharedSecret = concatArrayString(splitString(this.form.model.sharedSecret, 4), ' '); - this.form.model.hexSharedSecret = concatArrayString(splitString(this.form.model.hexSharedSecret, 4), ' '); + this.form.model.formatSharedSecret = concatArrayString(splitString(this.form.model.sharedSecret, 4), ' '); + //this.form.model.hexSharedSecret = concatArrayString(splitString(this.form.model.hexSharedSecret, 4), ' '); + } + + generate(): void { + this.form.submitting = true; + this.form.model.trans(); + this.timeBasedService.generate('').subscribe(res => { + if (res.code == 0) { + this.form.model.init(res.data); + this.formatSecret(); + //this.msg.success(`提交成功`); + } else { + //this.msg.success(`提交失败`); + } + this.form.submitting = false; + this.cdr.detectChanges(); + }); } onSubmit(): void { this.form.submitting = true; - this.form.model.trans(); + //this.form.model.trans(); this.timeBasedService.update(this.form.model).subscribe(res => { if (res.code == 0) { - this.form.model.init(res.data); - this.formatSecret(); this.msg.success(`提交成功`); } else { this.msg.success(`提交失败`); @@ -98,7 +112,7 @@ export class TimebasedComponent implements OnInit { } else { this.msg.error('验证失败'); } - }) + }); // this.timeBasedService.verify(otp) } diff --git a/maxkey-web-frontend/maxkey-web-app/src/app/service/time-based.service.ts b/maxkey-web-frontend/maxkey-web-app/src/app/service/time-based.service.ts index 732abf310..29d429ec8 100644 --- a/maxkey-web-frontend/maxkey-web-app/src/app/service/time-based.service.ts +++ b/maxkey-web-frontend/maxkey-web-app/src/app/service/time-based.service.ts @@ -27,18 +27,22 @@ import { BaseService } from './base.service'; }) export class TimeBasedService extends BaseService { constructor(private _httpClient: HttpClient) { - super(_httpClient, '/config'); + super(_httpClient, '/config/timebased'); } - override get(id: String): Observable> { - return this.http.get>(`${this.server.urls.base}/timebased?generate=NO`); + view(id: String): Observable> { + return this.http.get>(`${this.server.urls.base}/view`); + } + + generate(id: String): Observable> { + return this.http.get>(`${this.server.urls.base}/generate`); } override update(body: any): Observable> { - return this.http.get>(`${this.server.urls.base}/timebased?generate=YES`); + return this.http.put>(`${this.server.urls.base}/update`, body); } - verify(otp: string): Observable>{ - return this.http.get>(`${this.server.urls.base}/verify?otp=` + otp); + verify(otp: string): Observable> { + return this.http.get>(`${this.server.urls.base}/verify?otpCode=${otp}`); } } diff --git a/maxkey-web-frontend/maxkey-web-app/src/assets/i18n/en-US.json b/maxkey-web-frontend/maxkey-web-app/src/assets/i18n/en-US.json index fa45c9a20..a3f3fb101 100644 --- a/maxkey-web-frontend/maxkey-web-app/src/assets/i18n/en-US.json +++ b/maxkey-web-frontend/maxkey-web-app/src/assets/i18n/en-US.json @@ -541,7 +541,7 @@ "username": "username", "digits": "digits", "period": "period", - "sharedSecret": "sharedSecret(BASE32)", + "sharedSecret": "sharedSecret", "hexSharedSecret": "sharedSecret( HEX )", "rqCode": "RQCode", "one-timePassword": "One-Time Password" diff --git a/maxkey-web-frontend/maxkey-web-app/src/assets/i18n/zh-CN.json b/maxkey-web-frontend/maxkey-web-app/src/assets/i18n/zh-CN.json index 95aeebdfd..47eb6cf8c 100644 --- a/maxkey-web-frontend/maxkey-web-app/src/assets/i18n/zh-CN.json +++ b/maxkey-web-frontend/maxkey-web-app/src/assets/i18n/zh-CN.json @@ -527,7 +527,7 @@ "username": "账号", "digits": "长度", "period": "周期", - "sharedSecret": "共享密钥(BASE32)", + "sharedSecret": "共享密钥", "hexSharedSecret": "共享密钥( HEX )", "rqCode": "二维码", "one-timePassword": "一次性密码" diff --git a/maxkey-web-frontend/maxkey-web-app/src/assets/i18n/zh-TW.json b/maxkey-web-frontend/maxkey-web-app/src/assets/i18n/zh-TW.json index 3c8cc559c..92e3c0ed6 100644 --- a/maxkey-web-frontend/maxkey-web-app/src/assets/i18n/zh-TW.json +++ b/maxkey-web-frontend/maxkey-web-app/src/assets/i18n/zh-TW.json @@ -527,7 +527,7 @@ "username": "賬號", "digits": "長度", "period": "週期", - "sharedSecret": "共享密鑰(BASE32)", + "sharedSecret": "共享密鑰", "hexSharedSecret": "共享密鑰( HEX )", "rqCode": "二維碼", "one-timePassword": "一次性密碼" diff --git a/maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/LoginEntryPoint.java b/maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/LoginEntryPoint.java index 7ee381cca..db66ca8e5 100644 --- a/maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/LoginEntryPoint.java +++ b/maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/LoginEntryPoint.java @@ -48,7 +48,7 @@ import org.dromara.maxkey.password.sms.SmsOtpAuthnService; import org.dromara.maxkey.authn.provider.scancode.ScanCodeService; import org.dromara.maxkey.persistence.service.SocialsAssociatesService; import org.dromara.maxkey.persistence.service.UserInfoService; -import org.dromara.maxkey.util.RQCodeUtils; +import org.dromara.maxkey.util.QRCodeUtils; import org.dromara.maxkey.web.WebConstants; import org.dromara.maxkey.web.WebContext; import org.slf4j.Logger; @@ -286,7 +286,7 @@ public class LoginEntryPoint { String ticket = scanCodeService.createTicket(); log.debug("ticket: {}",ticket); String encodeTicket = PasswordReciprocal.getInstance().encode(ticket); - BufferedImage bufferedImage = RQCodeUtils.write2BufferedImage(encodeTicket, "gif", 300, 300); + BufferedImage bufferedImage = QRCodeUtils.write2BufferedImage(encodeTicket, "gif", 300, 300); String rqCode = Base64Utils.encodeImage(bufferedImage); HashMap codeMap = new HashMap<>(); codeMap.put("rqCode", rqCode); diff --git a/maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/OneTimePasswordController.java b/maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/OneTimePasswordController.java index 5c43b4b6e..1c1c35ae5 100644 --- a/maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/OneTimePasswordController.java +++ b/maxkey-webs/maxkey-web-maxkey/src/main/java/org/dromara/maxkey/web/contorller/OneTimePasswordController.java @@ -18,29 +18,29 @@ package org.dromara.maxkey.web.contorller; import java.awt.image.BufferedImage; -import java.util.HashMap; -import org.apache.commons.codec.binary.Hex; import org.apache.commons.lang3.StringUtils; import org.dromara.maxkey.authn.annotation.CurrentUser; import org.dromara.maxkey.crypto.Base32Utils; import org.dromara.maxkey.crypto.Base64Utils; import org.dromara.maxkey.crypto.password.PasswordReciprocal; import org.dromara.maxkey.entity.Message; +import org.dromara.maxkey.entity.dto.TimeBasedDto; import org.dromara.maxkey.entity.idm.UserInfo; import org.dromara.maxkey.password.onetimepwd.algorithm.OtpKeyUriFormat; import org.dromara.maxkey.password.onetimepwd.algorithm.OtpSecret; import org.dromara.maxkey.password.onetimepwd.impl.TimeBasedOtpAuthn; import org.dromara.maxkey.persistence.service.UserInfoService; -import org.dromara.maxkey.util.RQCodeUtils; +import org.dromara.maxkey.util.QRCodeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; /** @@ -48,8 +48,8 @@ import org.springframework.web.bind.annotation.ResponseBody; * @author Crystal.Sea * */ -@Controller -@RequestMapping(value = { "/config" }) +@RestController +@RequestMapping(value = { "/config/timebased" }) public class OneTimePasswordController { static final Logger logger = LoggerFactory.getLogger(OneTimePasswordController.class); @@ -62,55 +62,73 @@ public class OneTimePasswordController { @Autowired TimeBasedOtpAuthn timeBasedOtpAuthn; - @RequestMapping(value = {"/timebased"}) - @ResponseBody - public Message timebased( - @RequestParam(name="generate") String generate, - @CurrentUser UserInfo currentUser) { - HashMaptimebased =new HashMap<>(); - - generate(generate,currentUser); - - String sharedSecret = - PasswordReciprocal.getInstance().decoder(currentUser.getSharedSecret()); - - otpKeyUriFormat.setSecret(sharedSecret); - String otpauth = otpKeyUriFormat.format(currentUser.getUsername()); - byte[] byteSharedSecret = Base32Utils.decode(sharedSecret); - String hexSharedSecret = Hex.encodeHexString(byteSharedSecret); - BufferedImage bufferedImage = RQCodeUtils.write2BufferedImage(otpauth, "gif", 300, 300); - String rqCode = Base64Utils.encodeImage(bufferedImage); - - timebased.put("displayName", currentUser.getDisplayName()); - timebased.put("username", currentUser.getUsername()); - timebased.put("digits", otpKeyUriFormat.getDigits()); - timebased.put("period", otpKeyUriFormat.getPeriod()); - timebased.put("sharedSecret", sharedSecret); - timebased.put("hexSharedSecret", hexSharedSecret); - timebased.put("rqCode", rqCode); - return new Message>(timebased); + @GetMapping(value = {"/view"}) + public Message view(@CurrentUser UserInfo currentUser) { + UserInfo user = userInfoService.get(currentUser.getId()); + String sharedSecret = ""; + String qrCode = ""; + if(StringUtils.isNotBlank(user.getSharedSecret())) { + sharedSecret = PasswordReciprocal.getInstance().decoder(user.getSharedSecret()); + qrCode = genQRCode(sharedSecret,currentUser.getUsername()); + } + return new Message<>( + new TimeBasedDto( + user.getDisplayName(), + user.getUsername(), + otpKeyUriFormat.getDigits(), + otpKeyUriFormat.getPeriod(), + sharedSecret, + qrCode, + "" + )); } - - public void generate(String generate,@CurrentUser UserInfo currentUser) { - if((StringUtils.isNotBlank(generate) - && generate.equalsIgnoreCase("YES")) - ||StringUtils.isBlank(currentUser.getSharedSecret())) { - - byte[] byteSharedSecret = OtpSecret.generate(otpKeyUriFormat.getCrypto()); - String sharedSecret = Base32Utils.encode(byteSharedSecret); - sharedSecret = PasswordReciprocal.getInstance().encode(sharedSecret); - currentUser.setSharedSecret(sharedSecret); - userInfoService.updateSharedSecret(currentUser); - + + @GetMapping(value = {"/generate"}) + public Message generate(@CurrentUser UserInfo currentUser) { + //generate + byte[] byteSharedSecret = OtpSecret.generate(otpKeyUriFormat.getCrypto()); + String sharedSecret = Base32Utils.encode(byteSharedSecret); + String qrCode = genQRCode(sharedSecret,currentUser.getUsername()); + return new Message<>( + new TimeBasedDto( + currentUser.getDisplayName(), + currentUser.getUsername(), + otpKeyUriFormat.getDigits(), + otpKeyUriFormat.getPeriod(), + sharedSecret, + qrCode, + "" + )); + } + + @PutMapping(value = {"/update"}) + public Message update(@RequestBody TimeBasedDto timeBasedDto , @CurrentUser UserInfo currentUser) { + // 从当前用户信息中获取共享密钥 + UserInfo user = new UserInfo(); + user.setId(currentUser.getId()); + user.setSharedSecret(PasswordReciprocal.getInstance().encode(timeBasedDto.sharedSecret())); + // 计算当前时间对应的动态密码 + if (StringUtils.isNotBlank(timeBasedDto.otpCode()) && timeBasedOtpAuthn.validate(user, timeBasedDto.otpCode())) { + userInfoService.updateSharedSecret(user); + return new Message<>(Message.SUCCESS); + } else { + return new Message<>(Message.FAIL); } } - @RequestMapping("/verify") - public Message verify(@RequestParam("otp") String otp, @CurrentUser UserInfo currentUser) { + public String genQRCode(String sharedSecret,String username) { + otpKeyUriFormat.setSecret(sharedSecret); + String otpauth = otpKeyUriFormat.format(username); + BufferedImage bufferedImage = QRCodeUtils.write2BufferedImage(otpauth, "gif", 300, 300); + return Base64Utils.encodeImage(bufferedImage); + } + + @GetMapping("/verify") + public Message verify(@RequestParam("otpCode") String otpCode, @CurrentUser UserInfo currentUser) { // 从当前用户信息中获取共享密钥 - String sharedSecret = PasswordReciprocal.getInstance().decoder(currentUser.getSharedSecret()); + UserInfo user = userInfoService.get(currentUser.getId()); // 计算当前时间对应的动态密码 - boolean validate = timeBasedOtpAuthn.validate(currentUser, otp); + boolean validate = timeBasedOtpAuthn.validate(user, otpCode); if (validate) { return new Message<>(0,"One-Time Password verification succeeded"); } else {