Kaynağa Gözat

增加一次性动态口令验证功能

zen 2 yıl önce
ebeveyn
işleme
af25f72a3c

+ 1 - 0
maxkey-web-frontend/maxkey-web-app/src/app/entity/TimeBased.ts

@@ -26,4 +26,5 @@ export class TimeBased extends BaseEntity {
   sharedSecret!: String;
   hexSharedSecret!: String;
   rqCode!: String;
+  otp!: string;
 }

+ 21 - 7
maxkey-web-frontend/maxkey-web-app/src/app/routes/config/timebased/timebased.component.html

@@ -14,7 +14,7 @@
           <nz-form-control [nzSm]="14" [nzXs]="36" [nzXl]="48" nzErrorTip="The input is not displayName!">
             <input
               nz-input
-              readonly
+              [disabled]="isDisabled"
               [(ngModel)]="form.model.displayName"
               [ngModelOptions]="{ standalone: true }"
               name="displayName"
@@ -28,7 +28,7 @@
           <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid username!">
             <input
               nz-input
-              readonly
+              [disabled]="isDisabled"
               [(ngModel)]="form.model.username"
               [ngModelOptions]="{ standalone: true }"
               name="username"
@@ -39,13 +39,13 @@
         <nz-form-item>
           <nz-form-label [nzSm]="6" [nzXs]="24" nzFor="digits">{{ 'mxk.timebased.digits' | i18n }}</nz-form-label>
           <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid digits!">
-            <input nz-input readonly [(ngModel)]="form.model.digits" [ngModelOptions]="{ standalone: true }" name="digits" id="digits" />
+            <input nz-input [disabled]="isDisabled" [(ngModel)]="form.model.digits" [ngModelOptions]="{ standalone: true }" name="digits" id="digits" />
           </nz-form-control>
         </nz-form-item>
         <nz-form-item>
           <nz-form-label [nzSm]="6" [nzXs]="24" nzFor="period">{{ 'mxk.timebased.period' | i18n }}</nz-form-label>
           <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid period!">
-            <input nz-input readonly [(ngModel)]="form.model.period" [ngModelOptions]="{ standalone: true }" name="period" id="period" />
+            <input nz-input [disabled]="isDisabled" [(ngModel)]="form.model.period" [ngModelOptions]="{ standalone: true }" name="period" id="period" />
           </nz-form-control>
         </nz-form-item>
         <nz-form-item>
@@ -53,7 +53,7 @@
           <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid sharedSecret!">
             <input
               nz-input
-              readonly
+              [disabled]="isDisabled"
               [(ngModel)]="form.model.sharedSecret"
               [ngModelOptions]="{ standalone: true }"
               name="sharedSecret"
@@ -66,7 +66,7 @@
           <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid hexSharedSecret!">
             <input
               nz-input
-              readonly
+              [disabled]="isDisabled"
               [(ngModel)]="form.model.hexSharedSecret"
               [ngModelOptions]="{ standalone: true }"
               name="hexSharedSecret"
@@ -74,10 +74,24 @@
             />
           </nz-form-control>
         </nz-form-item>
+        <nz-form-item>
+          <nz-form-label [nzSm]="6" [nzXs]="24" nzFor="one-timePassword">{{ 'mxk.timebased.one-timePassword' | i18n }}</nz-form-label>
+          <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid One-Time Password!">
+            <input
+              nz-input
+              [(ngModel)]="form.model.otp"
+              [ngModelOptions]="{ standalone: true }"
+              placeholder="请在生成后输入一次性密码用于验证"
+              name="oneTimePassword"
+              id="oneTimePassword"
+            />
+          </nz-form-control>
+        </nz-form-item>
         <nz-form-item style="width: 100%">
-          <nz-form-control [nzOffset]="7" [nzSpan]="12">
+          <nz-form-control [nzOffset]="7" [nzSpan]="3">
             <button nz-button nzType="primary" type="submit" [nzLoading]="form.submitting">{{ 'mxk.text.generate' | i18n }}</button>
           </nz-form-control>
+            <button nz-button nzType="primary" (click)="verify($event,form.model.otp)">{{ 'mxk.text.verify' | i18n }}</button>
         </nz-form-item>
       </div>
     </div>

+ 17 - 1
maxkey-web-frontend/maxkey-web-app/src/app/routes/config/timebased/timebased.component.ts

@@ -28,7 +28,7 @@ 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: {
@@ -39,6 +39,8 @@ export class TimebasedComponent implements OnInit {
     model: new TimeBased()
   };
 
+  isDisabled = true;
+
   formGroup: FormGroup = new FormGroup({});
 
   constructor(
@@ -87,4 +89,18 @@ export class TimebasedComponent implements OnInit {
       this.cdr.detectChanges();
     });
   }
+
+  verify(e: MouseEvent, otp: string): void {
+    e.preventDefault();
+    this.timeBasedService.verify(otp).subscribe(res => {
+      if (res.code == 0) {
+        this.msg.success(`验证成功`);
+      } else {
+        this.msg.error('验证失败');
+      }
+    })
+    // this.timeBasedService.verify(otp)
+  }
+
+  protected readonly String = String;
 }

+ 4 - 0
maxkey-web-frontend/maxkey-web-app/src/app/service/time-based.service.ts

@@ -37,4 +37,8 @@ export class TimeBasedService extends BaseService<TimeBased> {
   override update(body: any): Observable<Message<TimeBased>> {
     return this.http.get<Message<TimeBased>>(`${this.server.urls.base}/timebased?generate=YES`);
   }
+
+  verify(otp: string): Observable<Message<TimeBased>>{
+    return this.http.get<Message<TimeBased>>(`${this.server.urls.base}/verify?otp=` + otp);
+  }
 }

+ 3 - 1
maxkey-web-frontend/maxkey-web-app/src/assets/i18n/en-US.json

@@ -543,7 +543,8 @@
 			"period": "period",
 			"sharedSecret": "sharedSecret(BASE32)",
 			"hexSharedSecret": "sharedSecret( HEX  )",
-			"rqCode": "RQCode"
+			"rqCode": "RQCode",
+      "one-timePassword": "One-Time Password"
 		},
 		"socialsproviders": {
 			"icon": "Icon",
@@ -683,6 +684,7 @@
 			"close": "Close",
 			"submit": "Submit",
 			"generate": "Generate",
+      "verify": "Verify",
 			"upload": "Upload",
 			"save": "Save",
 			"year": "Year",

+ 3 - 1
maxkey-web-frontend/maxkey-web-app/src/assets/i18n/zh-CN.json

@@ -529,7 +529,8 @@
 			"period": "周期",
 			"sharedSecret": "共享密钥(BASE32)",
 			"hexSharedSecret": "共享密钥( HEX  )",
-			"rqCode": "二维码"
+			"rqCode": "二维码",
+      "one-timePassword": "一次性密码"
 		},
 		"socialsassociate": {
 			"icon": "图标",
@@ -686,6 +687,7 @@
 			"close": "关闭",
 			"submit": "提交",
 			"generate": "生成",
+      "verify": "验证",
 			"upload": "上传",
 			"save": "保存",
 			"year": "年",

+ 3 - 1
maxkey-web-frontend/maxkey-web-app/src/assets/i18n/zh-TW.json

@@ -529,7 +529,8 @@
 			"period": "週期",
 			"sharedSecret": "共享密鑰(BASE32)",
 			"hexSharedSecret": "共享密鑰( HEX  )",
-			"rqCode": "二維碼"
+			"rqCode": "二維碼",
+      "one-timePassword": "一次性密碼"
 		},
 		"socialsassociate": {
 			"icon": "圖標",
@@ -686,6 +687,7 @@
 			"close": "關閉",
 			"submit": "提交",
 			"generate": "生成",
+      "verify": "驗證",
 			"upload": "上傳",
 			"save": "保存",
 			"year": "年",

+ 17 - 0
maxkey-webs/maxkey-web-maxkey/src/main/java/org/maxkey/web/contorller/OneTimePasswordController.java

@@ -30,6 +30,7 @@ import org.maxkey.entity.Message;
 import org.maxkey.entity.UserInfo;
 import org.maxkey.password.onetimepwd.algorithm.OtpKeyUriFormat;
 import org.maxkey.password.onetimepwd.algorithm.OtpSecret;
+import org.maxkey.password.onetimepwd.impl.TimeBasedOtpAuthn;
 import org.maxkey.persistence.service.UserInfoService;
 import org.maxkey.util.RQCodeUtils;
 import org.slf4j.Logger;
@@ -58,6 +59,9 @@ public class OneTimePasswordController {
     @Autowired
     OtpKeyUriFormat otpKeyUriFormat;
 
+    @Autowired
+    private TimeBasedOtpAuthn timeBasedOtpAuthn;
+
     @RequestMapping(value = {"/timebased"})
     @ResponseBody
     public ResponseEntity<?> timebased(
@@ -99,5 +103,18 @@ public class OneTimePasswordController {
             
         }
     }
+
+    @RequestMapping("/verify")
+    public ResponseEntity<?> verify(@RequestParam("otp") String otp, @CurrentUser UserInfo currentUser) {
+        // 从当前用户信息中获取共享密钥
+        String sharedSecret = PasswordReciprocal.getInstance().decoder(currentUser.getSharedSecret());
+        // 计算当前时间对应的动态密码
+        boolean validate = timeBasedOtpAuthn.validate(currentUser, otp);
+        if (validate) {
+            return new Message<>(0,"One-Time Password verification succeeded").buildResponse();
+        } else {
+            return new Message<>(2,"One-Time Password verification failed").buildResponse();
+        }
+    }
     
 }