shimingxy 8 месяцев назад
Родитель
Сommit
a098df18a3

+ 1 - 1
maxkey-common/src/main/java/org/dromara/maxkey/util/RQCodeUtils.java → 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 ){

+ 5 - 0
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) {
+
+}

+ 4 - 5
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;
         }

+ 3 - 2
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;
 }

+ 28 - 59
maxkey-web-frontend/maxkey-web-app/src/app/routes/config/timebased/timebased.component.html

@@ -1,99 +1,68 @@
 <nz-card>
-  <form nz-form [formGroup]="formGroup" (ngSubmit)="onSubmit()" se-container="1">
+  <form nz-form [formGroup]="formGroup" se-container="1">
     <div nz-row style="width: 100%">
-      <div nz-col nzFlex="2" style="text-align: center"><img src="{{ form.model.rqCode }}" /></div>
+      <div nz-col nzFlex="2" style="text-align: center"><img src="{{ form.model.qrCode }}" /></div>
       <div nz-col nzFlex="3">
         <nz-form-item style="display: none">
           <nz-form-label [nzMd]="6" nzFor="id">id</nz-form-label>
           <nz-form-control [nzMd]="18" nzErrorTip="The input is not valid id!">
-            <input [(ngModel)]="form.model.id" [ngModelOptions]="{ standalone: true }" nz-input name="id" id="id" value="id" />
+            <input [(ngModel)]="form.model.id" [ngModelOptions]="{ standalone: true }" nz-input name="id" id="id"
+              value="id" />
           </nz-form-control>
         </nz-form-item>
         <nz-form-item>
-          <nz-form-label [nzSm]="6" [nzXs]="24" nzFor="displayName">{{ 'mxk.timebased.displayName' | i18n }}</nz-form-label>
+          <nz-form-label [nzSm]="6" [nzXs]="24" nzFor="displayName">{{ 'mxk.timebased.displayName' | i18n
+            }}</nz-form-label>
           <nz-form-control [nzSm]="14" [nzXs]="36" [nzXl]="48" nzErrorTip="The input is not displayName!">
-            <input
-              nz-input
-              [disabled]="isDisabled"
-              [(ngModel)]="form.model.displayName"
-              [ngModelOptions]="{ standalone: true }"
-              name="displayName"
-              id="displayName"
-              value="0"
-            />
+            <input nz-input [disabled]="isDisabled" [(ngModel)]="form.model.displayName"
+              [ngModelOptions]="{ standalone: true }" name="displayName" id="displayName" value="0" />
           </nz-form-control>
         </nz-form-item>
         <nz-form-item>
           <nz-form-label [nzSm]="6" [nzXs]="24" nzFor="username">{{ 'mxk.timebased.username' | i18n }}</nz-form-label>
           <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid username!">
-            <input
-              nz-input
-              [disabled]="isDisabled"
-              [(ngModel)]="form.model.username"
-              [ngModelOptions]="{ standalone: true }"
-              name="username"
-              id="username"
-            />
+            <input nz-input [disabled]="isDisabled" [(ngModel)]="form.model.username"
+              [ngModelOptions]="{ standalone: true }" name="username" id="username" />
           </nz-form-control>
         </nz-form-item>
         <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 [disabled]="isDisabled" [(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 [disabled]="isDisabled" [(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>
-          <nz-form-label [nzSm]="6" [nzXs]="24" nzFor="sharedSecret">{{ 'mxk.timebased.sharedSecret' | i18n }}</nz-form-label>
+          <nz-form-label [nzSm]="6" [nzXs]="24" nzFor="sharedSecret">{{ 'mxk.timebased.sharedSecret' | i18n
+            }}</nz-form-label>
           <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid sharedSecret!">
-            <input
-              nz-input
-              [disabled]="isDisabled"
-              [(ngModel)]="form.model.sharedSecret"
-              [ngModelOptions]="{ standalone: true }"
-              name="sharedSecret"
-              id="sharedSecret"
-            />
+            <input nz-input [disabled]="isDisabled" [(ngModel)]="form.model.formatSharedSecret"
+              [ngModelOptions]="{ standalone: true }" name="formatSharedSecret" id="formatSharedSecret" />
           </nz-form-control>
         </nz-form-item>
         <nz-form-item>
-          <nz-form-label [nzSm]="6" [nzXs]="24" nzFor="hexSharedSecret">{{ 'mxk.timebased.hexSharedSecret' | i18n }}</nz-form-label>
-          <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid hexSharedSecret!">
-            <input
-              nz-input
-              [disabled]="isDisabled"
-              [(ngModel)]="form.model.hexSharedSecret"
-              [ngModelOptions]="{ standalone: true }"
-              name="hexSharedSecret"
-              id="hexSharedSecret"
-            />
-          </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-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"
-            />
+            <input nz-input [(ngModel)]="form.model.otpCode" [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]="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>
+          <button nz-button nzType="primary" (click)="generate()">{{ 'mxk.text.generate' | i18n }}</button>
+          <button nz-button nzType="primary" (click)="onSubmit()" [nzLoading]="form.submitting">{{ 'mxk.text.save' |
+            i18n }}</button>
+          <button nz-button nzType="primary" (click)="verify($event, form.model.otpCode)">{{ 'mxk.text.verify' | i18n
+            }}</button>
         </nz-form-item>
       </div>
     </div>
   </form>
-</nz-card>
+</nz-card>

+ 25 - 11
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), ' ');
   }
 
-  onSubmit(): void {
+  generate(): void {
     this.form.submitting = true;
     this.form.model.trans();
-    this.timeBasedService.update(this.form.model).subscribe(res => {
+    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.timeBasedService.update(this.form.model).subscribe(res => {
+      if (res.code == 0) {
         this.msg.success(`提交成功`);
       } else {
         this.msg.success(`提交失败`);
@@ -98,7 +112,7 @@ export class TimebasedComponent implements OnInit {
       } else {
         this.msg.error('验证失败');
       }
-    })
+    });
     // this.timeBasedService.verify(otp)
   }
 

+ 10 - 6
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<TimeBased> {
   constructor(private _httpClient: HttpClient) {
-    super(_httpClient, '/config');
+    super(_httpClient, '/config/timebased');
   }
 
-  override get(id: String): Observable<Message<TimeBased>> {
-    return this.http.get<Message<TimeBased>>(`${this.server.urls.base}/timebased?generate=NO`);
+  view(id: String): Observable<Message<TimeBased>> {
+    return this.http.get<Message<TimeBased>>(`${this.server.urls.base}/view`);
+  }
+
+  generate(id: String): Observable<Message<TimeBased>> {
+    return this.http.get<Message<TimeBased>>(`${this.server.urls.base}/generate`);
   }
 
   override update(body: any): Observable<Message<TimeBased>> {
-    return this.http.get<Message<TimeBased>>(`${this.server.urls.base}/timebased?generate=YES`);
+    return this.http.put<Message<TimeBased>>(`${this.server.urls.base}/update`, body);
   }
 
-  verify(otp: string): Observable<Message<TimeBased>>{
-    return this.http.get<Message<TimeBased>>(`${this.server.urls.base}/verify?otp=` + otp);
+  verify(otp: string): Observable<Message<TimeBased>> {
+    return this.http.get<Message<TimeBased>>(`${this.server.urls.base}/verify?otpCode=${otp}`);
   }
 }

+ 1 - 1
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"

+ 1 - 1
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": "一次性密码"

+ 1 - 1
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": "一次性密碼"

+ 2 - 2
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<String,String> codeMap = new HashMap<>();
 		 codeMap.put("rqCode", rqCode);

+ 69 - 51
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) {
-        HashMap<String,Object >timebased =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<HashMap<String,Object >>(timebased);
+    @GetMapping(value = {"/view"})
+    public Message<TimeBasedDto> 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<TimeBasedDto> 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<String> 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<String> 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 {