两因素身份验证增强您的Spring Security

本文介绍如何通过Nexmo在基于Spring Boot和Spring Security的Web应用中添加两因素身份验证。当用户登录时,除了用户名和密码外,还需要通过手机接收并输入验证码,以增强安全性。文章涵盖了创建新角色、处理验证信息、设置Nexmo客户端以及整合到Spring Security的过程。
摘要由CSDN通过智能技术生成

通过要求用户提供第二种身份验证,双重身份验证为您的Web应用程序增加了一层额外的安全保护。 常见的第二个因素包括:

  • 验证码生物识别电子邮件或短信代码

让我们探讨如何利用Nexmo向现有的Web应用程序中添加双重身份验证。

Before you begin

为了跟随本教程,您将需要以下内容:

Get the Code

克隆入门科。

git clone https://github.com/cr0wst/demo-twofactor.git -b getting-started
cd demo-twofactor

Let’s See What We’re Working With

The example application is built using Spring Boot. If you have Gradle installed on your system, you should be able to execute the bootRun task to start the application.

如果没有,则无后顾之忧; 仓库包含一个Gradle包装器,仍然可以让您执行任务。

./gradlew bootRun

这将下载所有依赖项,编译应用程序,然后启动嵌入式服务器。

Once the server has been started, you should be able to navigate to http://localhost:8080 to see the sample application.

共三页:

  • The home page - accessible by everybody.
  • The login page - allows users to enter a username and password (default is demo/demo).
  • The secret page - accessible only by users with the Role.USERS role.

Adding Two-Factor Authentication

用户登录时,我们唯一的接受标准是他们提供了用户名和密码。 如果此信息被盗怎么办? 我们可以使用的什么东西实际上位于用户附近?

我保证您和我们的用户中有将近90%会触手可及。 一部手机。

运作方式如下:

  1. 用户将照常登录到您的应用程序。他们将被提示输入四位数的验证码。同时,四位数的验证码将发送到他们帐户中的电话号码。 如果他们的帐户上没有电话号码,我们将允许他们绕过两因素身份验证。他们输入的代码将被检查以确保与我们发送给他们的代码相同。

We are going to utilize the Nexmo Verify API to generate the code and to check and see if the code they entered is valid.

Creating a New Role

第一步将是创建一个新角色。 该角色将用于使经过身份验证的用户处于炼狱状态,直到我们验证了他们的身份。

添加PRE_VERIFICATION_USER角色角色枚举。

// src/main/net/smcrow/demo/twofactor/user/Role.java
public enum Role implements GrantedAuthority {
    USER, PRE_VERIFICATION_USER;
    // ...
}

为了将其用作默认角色,我们需要更新getAuthorities()的方法标准用户详细信息类。

// src/main/net/smcrow/demo/twofactor/user/StandardUserDetails.java
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    Set<GrantedAuthority> authorities = new HashSet<>();
    authorities.add(Role.PRE_VERIFICATION_USER);
    return authorities;
}

Handling Verification Information

Nexmo将为我们提供要求编号确认用户提供的代码时将使用该代码。 我们可以通过多种方式存储此信息。 在本教程中,我们将其持久化到数据库中。

Storing Verification Information

首先,建立一个验证中的课程校验包。

// src/main/net/smcrow/demo/twofactor/verify/Verification.java
@Entity
public class Verification {
    @Id
    @Column(unique = true, nullable = false)
    private String phone;

    @Column(nullable = false)
    private String requestId;

    @Column(nullable = false)
    private Date expirationDate;

    @PersistenceConstructor
    public Verification() {
        // Empty constructor for JPA
    }

    public Verification(String phone, String requestId, Date expirationDate) {
        this.phone = phone;
        this.requestId = requestId;
        this.expirationDate = expirationDate;
    }

    // ... Getters and Setters
}

注意,我们还存储了截止日期。 默认情况下,Verify API请求仅有效五分钟。 在以下情况之一时,它们将从表中删除:

  • 用户已成功验证其身份。它们已过期。

我们将利用Spring Scheduler定期清理它们。

Working with the Verification Information

创建验证库界面校验包。

// src/mainnet/smcrow/demo/twofactor/verify/VerificationRepository.java
@Repository
public interface VerificationRepository extends JpaRepository<Verification, String> {
    Optional<Verification> findByPhone(String phone);

    void deleteByExpirationDateBefore(Date date);
}

Deleting Expired Requests

在里面两个因素包,创建以下配置类。

// src/main/net/smcrow/demo/twofactor/ScheduleConfiguration.java
@Configuration
@EnableScheduling
public class ScheduleConfiguration {
    @Autowired
    private VerificationRepository verificationRepository;

    @Scheduled(fixedDelay = 1000)
    @Transactional
    public void purgeExpiredVerifications() {
        verificationRepository.deleteByExpirationDateBefore(new Date());
    }
}

这将设置一个计划的命令,使其每秒钟执行一次,以查询是否已过期验证实体并删除它们。

Setting Up the Nexmo Client

We will be using the nexmo-java client for interacting with Nexmo.

Declare the Dependency

首先,在build.gradle文件。

dependencies {
    // .. other dependencies
    compile('com.nexmo:client:3.3.0')
}

Provide Information

现在,在application.properties文件。

# Add your nexmo credentials
nexmo.api.key=your-api-key
nexmo.api.secret=your-api-secret

Define the Beans

接下来,我们将定义NexmoClient和验证客户作为豆。 这将允许Spring将它们作为依赖项注入到我们的NexmoVerificationService。

将以下定义添加到双重因素应用类。

// src/main/net/smcrow/demo/twofactor/TwofactorApplication.java
@Bean
public NexmoClient nexmoClient(Environment environment) {
    AuthMethod auth = new TokenAuthMethod(
            environment.getProperty("nexmo.api.key"),
            environment.getProperty("nexmo.api.secret")
    );
    return new NexmoClient(auth);
}

@Bean
public VerifyClient nexmoVerifyClient(NexmoClient nexmoClient) {
    return nexmoClient.getVerifyClient();
}

Create the NexmoVerificationService

我们将创建一个服务,该服务将允许我们指示客户提出请求。

添加NexmoVerificationService到校验包。

// src/main/net/smcrow/demo/twofactor/verify/NexmoVerificationService.java
@Service
public class NexmoVerificationService {
    private static final String APPLICATION_BRAND = "2FA Demo";
    private static final int EXPIRATION_INTERVALS = Calendar.MINUTE;
    private static final int EXPIRATION_INCREMENT = 5;
    @Autowired
    private VerificationRepository verificationRepository;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private VerifyClient verifyClient;

    public Verification requestVerification(String phone) throws VerificationRequestFailedException {
        Optional<Verification> matches = verificationRepository.findByPhone(phone);
        if (matches.isPresent()) {
            return matches.get();
        }

        return generateAndSaveNewVerification(phone);
    }

    public boolean verify(String phone, String code) throws VerificationRequestFailedException {
        try {
            Verification verification = retrieveVerification(phone);
            if (verifyClient.check(verification.getRequestId(), code).getStatus() == 0) {
                verificationRepository.delete(phone);
                return true;
            }

            return false;
        } catch (VerificationNotFoundException e) {
            requestVerification(phone);
            return false;
        } catch (IOException | NexmoClientException e) {
            throw new VerificationRequestFailedException(e);
        }
    }

    private Verification retrieveVerification(String phone) throws VerificationNotFoundException {
        Optional<Verification> matches = verificationRepository.findByPhone(phone);
        if (matches.isPresent()) {
            return matches.get();
        }

        throw new VerificationNotFoundException();
    }

    private Verification generateAndSaveNewVerification(String phone) throws VerificationRequestFailedException {
        try {
            VerifyResult result = verifyClient.verify(phone, APPLICATION_BRAND);
            if (StringUtils.isBlank(result.getErrorText())) {
                String requestId = result.getRequestId();
                Calendar now = Calendar.getInstance();
                now.add(EXPIRATION_INTERVALS, EXPIRATION_INCREMENT);

                Verification verification = new Verification(phone, requestId, now.getTime());
                return verificationRepository.save(verification);
            }
        } catch (IOException | NexmoClientException e) {
            throw new VerificationRequestFailedException(e);
        }

        throw new VerificationRequestFailedException();
    }
}

此类中有两种主要方法:

  • requestVerficiation用来请求验证。校验 which is used to 校验 the provided code provided by the user.
The requestVerification Method

该方法首先检查看看我们是否已经有针对该用户电话号码的待定验证请求。 如果用户尝试再次登录到应用程序,这可以使我们向用户提供相同的请求ID。

如果没有任何先前的验证,则需要一个新的验证码并将其保存到数据库中。 如果由于某种原因,我们无法为他们分配新的代码,验证RequestFailedException被抛出。

将此例外添加到校验包。

// src/main/net/smcrow/demo/twofactor/verify/VerificationRequestFailedException.java
public class VerificationRequestFailedException extends Throwable {
    public VerificationRequestFailedException() {
        this("Failed to verify request.");
    }

    public VerificationRequestFailedException(String message) {
        super(message);
    }

    public VerificationRequestFailedException(Throwable cause) {
        super(cause);
    }
}
The verify Method

的校验方法将请求ID和代码发送到Nexmo进行验证。 如果验证成功,Nexmo将返回零状态。 成功验证后,验证实体已从数据库中删除,并且真正返回。

如果我们找不到验证实体,也许它已过期,我们请求一个新的实体并返回false。 如果有任何问题需要验证,我们会抛出一个验证RequestFailedException。

的检索验证方法会抛出一个验证NotFoundException如果验证找不到。

将此例外添加到校验包。

// src/main/net/smcrow/demo/twofactor/verify/VerificationNotFoundException.java

public class VerificationNotFoundException extends Throwable {
    public VerificationNotFoundException() {
        this("Failed to find verification.");
    }

    public VerificationNotFoundException(String message) {
        super(message);
    }
}

Using the NexmoVerificationService

我们将使用该服务发送代码和验证代码。 验证成功后,便会发送代码。

Triggering the Request for Verification

让我们实施一个自定义AuthenticationSuccessHandler用户成功通过身份验证后将被调用。

添加TwoFactorAuthenticationSuccessHandler到校验包。

// src/main/net/smcrow/demo/twofactor/verify/TwoFactorAuthenticationSuccessHandler.java
@Component
public class TwoFactorAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private static final String VERIFICATION_URL = "/verify";
    private static final String INDEX_URL = "/";

    @Autowired
    private NexmoVerificationService verificationService;

    @Autowired
    private UserRepository userRepository;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        String phone = ((StandardUserDetails) authentication.getPrincipal()).getUser().getPhone();
        if (phone == null || !requestAndRegisterVerification(phone)) {
            bypassVerification(request, response, authentication);
            return;
        }

        new DefaultRedirectStrategy().sendRedirect(request, response, VERIFICATION_URL);
    }

    private boolean requestAndRegisterVerification(String phone) {
        try {
            return verificationService.requestVerification(phone) != null;
        } catch (VerificationRequestFailedException e) {
            return false;
        }
    }

    private void bypassVerification(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        verificationService.updateAuthentication(authentication);
        new DefaultRedirectStrategy().sendRedirect(request, response, INDEX_URL);
    }
}

用户成功通过身份验证后,我们将检查他们是否具有电话号码。

如果他们有电话号码,则将密码发送到他们的设备。 如果他们没有电话号码,或者我们无法发送代码,则允许他们绕过验证。

的绕过验证方法依赖于updateAuthentication的方法NexmoVerificationService。

将此添加到NexmoVerificationService:

// src/main/net/smcrow/demo/twofactor/verify/NexmoVerificationService.java
public void updateAuthentication(Authentication authentication) {
    Role role = retrieveRoleFromDatabase(authentication.getName());
    List<GrantedAuthority> authorities = new ArrayList<>();
    authorities.add(role);

    Authentication newAuthentication = new UsernamePasswordAuthenticationToken(
            authentication.getPrincipal(),
            authentication.getCredentials(),
            authorities
    );

    SecurityContextHolder.getContext().setAuthentication(newAuthentication);
}

private Role retrieveRoleFromDatabase(String username) {
    Optional<User> match = userRepository.findByUsername(username);
    if (match.isPresent()) {
        return match.get().getRole();
    }

    throw new UsernameNotFoundException("Username not found.");
}

此方法用于分配角色在数据库中为当前用户定义并删除PRE_VERIFICATION_USER 角色.

Prompting The User for a Code

Once the user has been sent a code, they are forwarded to the verification page. Let’s work on creating that page next.

创建一个新的HTML文件,名为verify.html在里面资源/模板目录。

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="default">
<head>
    <meta charset="UTF-8" />
    <title>Two Factor Authorization Demo</title>
</head>
<body>
<div layout:fragment="content" class="container">
    <div class="col-lg-12 alert alert-danger text-center" th:if="${param.error}">There was an error with your login.</div>
    <div class="col-lg-4 offset-lg-4 text-left">
        <form th:action="@{/verify}" method="post">
            <h1>Verify</h1>
            <p>A text message has been sent to your mobile device. Please enter the code below:</p>
            <div class="form-group">
                <label for="code">Verification Code</label>
                <input type="text" class="form-control" id="code" name="code" placeholder="4-Digit Code" />
            </div>
            <button type="submit" class="btn btn-primary">Verify</button>
        </form>
    </div>
</div>
</body>
</html>

我们还需要一个控制器来将页面提供给用户。 创建验证控制器在里面校验包。

// src/main/net/smcrow/demo/twofactor/verify/VerificationController.java
@Controller
public class VerificationController {
    @Autowired
    private NexmoVerificationService verificationService;

    @PreAuthorize("hasRole('PRE_VERIFICATION_USER')")
    @GetMapping("/verify")
    public String index() {
        return "verify";
    }

    @PreAuthorize("hasRole('PRE_VERIFICATION_USER')")
    @PostMapping("/verify")
    public String verify(@RequestParam("code") String code, Authentication authentication) {
        User user = ((StandardUserDetails) authentication.getPrincipal()).getUser();
        try {
            if (verificationService.verify(user.getPhone(), code)) {
                verificationService.updateAuthentication(authentication);
                return "redirect:/";
            }

            return "redirect:verify?error";
        } catch (VerificationRequestFailedException e) {
            // Having issues generating keys let them through.
            verificationService.updateAuthentication(authentication);
            return "redirect:/";
        }
    }
}

该控制器通过指数方法,并通过来处理表单提交校验方法。

只有拥有以下内容的用户才能访问此页面:PRE_VERIFICATION_USER角色。 成功验证后,updateAuthentication方法再次被用来将其保留为原来的角色。

Finishing Up the Verification Chain

最后一步是更新AppSecurityConfiguration使用我们的TwoFactorAuthenticationSuccessHandler。

修改AppSecurityConfiguration连接我们的处理程序并通过successHandler方法。

// src/main/net/smcrow/demo/twofactor/AppSecurityConfiguration.java
@Autowired
private TwoFactorAuthenticationSuccessHandler twoFactorAuthenticationSuccessHandler;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    // Webjar resources
    httpSecurity.authorizeRequests().antMatchers("/webjars/**").permitAll()
    .and().formLogin().loginPage("/login").permitAll()
            .successHandler(twoFactorAuthenticationSuccessHandler)
    .and().logout().permitAll();
}

Try it Out!

您需要将您的电话号码添加到data.sql文件。

We aren’t going to be doing any validation on the phone number, and it needs to be in E.164 format.

INSERT INTO user (username, password, role, phone) VALUES
    ('phone', 'phone', 'USER', 15555555555);

您现在应该启动并运行了。 启动应用程序,然后尝试登录。假设您的API密钥,API机密和种子电话号码正确; 您应该会收到一条包含四位数代码的短信。

What Did We Do?

我们做了一个很多东西的。

简而言之,我们实施了两因素身份验证以更好地保护我们的应用程序。 我们这样做是:

  • 创建一个自定义AuthenticationSuccessHandler在向他们提供代码后将用户转发到验证页面。使用nexmo-java库,将其包装在NexmoVerificationService,以向我们的用户发送验证码。利用Spring Scheduler删除过期的验证码。建立一个页面供用户输入他们的验证码。

Check out the final code from this tutorial on GitHub.

Looking Ahead

有两种方法可以实现两因素身份验证。 如果您对示例代码中使用的任何框架和技术感到好奇,请参考以下清单:

Don’t forget that you can be a Nexmo contributor to the nexmo-java client.

from: https://dev.to//cr0wst/beefing-up-your-spring-security-with-two-factor-authentication-4m5p

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值