spring boot mfa totp 登录代码验证不起作用

S H*_*S H 0 java authentication spring-boot totp

我正在尝试使用 totp 在我的应用程序中实现 MFA 身份验证。波纹管是我使用的库。注册用户一切顺利,我收到了二维码,扫描它并每 30 秒在谷歌身份验证器中获取一次代码。当我尝试登录以验证代码时,代码验证不起作用(在身份验证服务中,方法验证)。我花了几个小时但无法弄清楚,尝试了不同的用户、日志但没有成功。

<dependency>
            <groupId>dev.samstevens.totp</groupId>
            <artifactId>totp</artifactId>
            <version>1.7.1</version>
        </dependency>
Run Code Online (Sandbox Code Playgroud)

这是我的代码

AuthContoller.java


import com.example.jsonfaker.model.dto.LoginRequest;
import com.example.jsonfaker.model.dto.SignupRequest;
import com.example.jsonfaker.model.dto.VerifyRequest;
import com.example.jsonfaker.service.Exporter;
import com.example.jsonfaker.service.UserAuthService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequestMapping("/auth")
@CrossOrigin
public class AuthController {
    private final Exporter exporter;

    private final UserAuthService userAuthService;

    public AuthController(Exporter exporter, UserAuthService userAuthService) {
        this.exporter = exporter;
        this.userAuthService = userAuthService;
    }

    @PostMapping("/login")
    public ResponseEntity<String> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
        String response = userAuthService.login(loginRequest);
        return ResponseEntity
                .ok()
                .body(response);
    }

    @PostMapping("/register2FA")
    public ResponseEntity<byte[]> registerUser2FA(@Valid @RequestBody SignupRequest signupRequest) throws Exception {

        userAuthService.register2FA(signupRequest);
        byte[] qrCodeBytes = userAuthService.mfaAccountSetup(signupRequest.getUsername());

        return ResponseEntity
                .ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .header(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=\""+exporter.exportFileNameQR() + ".png\"")
                .body(qrCodeBytes);
    }

    @PostMapping("/register")
    public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signupRequest) throws Exception {
        userAuthService.simpleRegister(signupRequest);
        return ResponseEntity.ok(HttpStatus.CREATED);
    }

    @PostMapping("/verify")
    public ResponseEntity<String> authenticateUser2FA(@Valid @RequestBody VerifyRequest verifyRequest) throws Exception {
        String response = userAuthService.verify(verifyRequest.getUsername(), verifyRequest.getCode());
        return ResponseEntity
                .ok()
                .body(response);
    }


}

Run Code Online (Sandbox Code Playgroud)

这是我的代币管理器

import dev.samstevens.totp.code.*;
import dev.samstevens.totp.exceptions.QrGenerationException;
import dev.samstevens.totp.qr.QrData;
import dev.samstevens.totp.qr.QrGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import dev.samstevens.totp.time.SystemTimeProvider;
import dev.samstevens.totp.time.TimeProvider;
import dev.samstevens.totp.util.Utils;
import org.springframework.stereotype.Service;

@Service("mfaTokenManager")
public class DefaultMFATokenManager implements MFATokenManager {


    private final SecretGenerator secretGenerator;


    private final QrGenerator qrGenerator;


    private final CodeVerifier codeVerifier;

    public DefaultMFATokenManager(SecretGenerator secretGenerator, QrGenerator qrGenerator, CodeVerifier codeVerifier) {
        this.secretGenerator = secretGenerator;
        this.qrGenerator = qrGenerator;
        this.codeVerifier = codeVerifier;
    }

    @Override
    public String generateSecretKey() {
        return secretGenerator.generate();
    }

    @Override
    public String getQRCode(String secret) throws QrGenerationException {
        QrData data = new QrData.Builder().label("MFA")
                .secret(secret)
                .issuer("Daniel token")
                .algorithm(HashingAlgorithm.SHA1)
                .digits(6)
                .period(30)
                .build();
        return Utils.getDataUriForImage(
                qrGenerator.generate(data),
                qrGenerator.getImageMimeType()
        );
    }

    @Override
    public boolean verifyTotp(String code, String secret) {
        TimeProvider timeProvider = new SystemTimeProvider();
        CodeGenerator codeGenerator = new DefaultCodeGenerator();
        CodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
        System.out.println(timeProvider.getTime());
        System.out.println(codeGenerator);

        return verifier.isValidCode(secret, code);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我的身份验证服务


import com.example.jsonfaker.model.Roles;
import com.example.jsonfaker.model.SystemUser;
import com.example.jsonfaker.model.dto.LoginRequest;
import com.example.jsonfaker.model.dto.SignupRequest;
import com.example.jsonfaker.model.dto.TokenResponse;
import com.example.jsonfaker.repository.RolesRepository;
import com.example.jsonfaker.repository.SystemUserRepository;
import com.example.jsonfaker.security.AuthoritiesConstants;
import com.example.jsonfaker.security.jwt.JwtUtils;
import com.example.jsonfaker.twoFA.MFATokenManager;
import com.example.jsonfaker.twoFA.MfaTokenData;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.stream.Collectors;

import static java.util.Objects.nonNull;

@Service
public class UserAuthService {
    private final SystemUserRepository systemUserRepository;
    private final RolesRepository rolesRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final MFATokenManager mfaTokenManager;
    private final AuthenticationManager authenticationManager;
    private final LoginUserService loginUserService;
    private final JwtUtils jwtUtils;

    public UserAuthService(SystemUserRepository systemUserRepository, RolesRepository rolesRepository, BCryptPasswordEncoder bCryptPasswordEncoder, MFATokenManager mfaTokenManager, AuthenticationManager authenticationManager, LoginUserService loginUserService, JwtUtils jwtUtils) {
        this.systemUserRepository = systemUserRepository;
        this.rolesRepository = rolesRepository;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
        this.mfaTokenManager = mfaTokenManager;
        this.authenticationManager = authenticationManager;
        this.loginUserService = loginUserService;
        this.jwtUtils = jwtUtils;
    }

    public void simpleRegister(SignupRequest signupRequest) throws Exception {
        if(systemUserRepository.findByUsername(signupRequest.getUsername()).isPresent()){
            throw new Exception("User with this username exists");
        }

        Roles simpleUserRole = new Roles();
        simpleUserRole.setName(AuthoritiesConstants.USER);

        SystemUser user = new SystemUser();
        user.setPassword(bCryptPasswordEncoder.encode(signupRequest.getPassword()));
        user.setUsername(signupRequest.getUsername());
        user.setAuthorities(rolesRepository.findAllByName("ROLE_USER").stream().collect(Collectors.toSet()));
        user.setSecret(mfaTokenManager.generateSecretKey());
        systemUserRepository.save(user);

    }

    public void register2FA(SignupRequest signupRequest) throws Exception {
        if(systemUserRepository.findByUsername(signupRequest.getUsername()).isPresent()){
            throw new Exception("User with this username exists");
        }
        Roles simpleUserRole = new Roles();
        simpleUserRole.setName(AuthoritiesConstants.USER);

        SystemUser user = new SystemUser();
        user.setPassword(bCryptPasswordEncoder.encode(signupRequest.getPassword()));
        user.setUsername(signupRequest.getUsername());
        user.setAuthorities(rolesRepository.findAllByName("ROLE_USER").stream().collect(Collectors.toSet()));
        user.setTwoFAisEnabled(Boolean.TRUE);
        user.setSecret(mfaTokenManager.generateSecretKey());
        systemUserRepository.save(user);
    }

    public byte[] mfaAccountSetup(String username) throws Exception {
        SystemUser user = systemUserRepository.findByUsername(username).get();
        if (!nonNull(user)){
            throw new Exception("Unable to find user with this username");
        }
        if(!user.isTwoFAisEnabled()){
            throw new Exception("2FA is not enabled for this account");
        }
        MfaTokenData token =  new MfaTokenData(mfaTokenManager.getQRCode(user.getSecret()), user.getSecret());
        System.out.println("Mfa code :" +token.getMfaCode());

        String base64Image = token.getQrCode().split(",")[1];
        byte[] imageBytes = javax.xml.bind.DatatypeConverter.parseBase64Binary(base64Image);
        return imageBytes;
    }

    public String login(LoginRequest loginRequest){
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(),
                        loginRequest.getPassword()
                )
        );
        SecurityContextHolder.getContext().setAuthentication(authentication);

        if(systemUserRepository.findByUsername(loginRequest.getUsername()).get().isTwoFAisEnabled()){
            return "verify code now";
        }

        SystemUser userDetails = (SystemUser) authentication.getPrincipal();

        String jwt = jwtUtils.generateJwtToken(userDetails);

        return new TokenResponse(jwt).toString();

    }

    public String verify(String username, String code) throws Exception {

        SystemUser user = systemUserRepository.findByUsername(username).get();
        if (!nonNull(user)){
            throw new Exception("Unable to find user with this username");
        }

        if (!mfaTokenManager.verifyTotp(code, user.getSecret())){
            return "unable to auth";
        }
        return "token here";

    }
}

Run Code Online (Sandbox Code Playgroud)

S H*_*S H 5

终于找到问题了,手机上时间延迟了2分钟,我把它设置成和电脑上一样就可以了。问题是,在验证令牌时,应用程序会为每次令牌生成使用 30 秒的间隔,并且如果手机或其他设备上的延迟在未来或过去大于 30 秒,则时间戳与用于验证的时间戳不匹配。

这是我使用的库的文档,使用前请务必仔细阅读。 https://github.com/samdjstevens/java-totp

这是我在项目中遵循的文章: https ://www.javadevjournal.com/spring-security/two-factor-authentication-with-spring-security/

使用 TOTP 开始项目之前的有用阅读: https://www.freecodecamp.org/news/how-time-based-one-time-passwords-work-and-why-you-should-use-them-in-your -app-fdd2b9ed43c3/

关于 2FA 的 YouTube 视频: https://www.youtube.com/watch?v=ZXFYT- BG2So