·前言·
Java Spring是目前企业开发中使用较多的一种java开发框架,因此,基于该框架的安全内容尤为重要。Secure Code Warrior编码实验室的Java Spring涉及到的安全问题有9个,分别为缺少功能级别的访问控制、不恰当的身份验证、记录和监控不足、SQL注入、明文存储密码、路径遍历、服务器请求伪造、XML外部实体(XXE)、任意文件上传。
因涉及内容较多,完整内容将会在本公众号拆分为多篇内容分别发出。本文为该系列的第二篇——安全问题二:不恰当的身份验证。
往期内容请查看Java Spring编码安全系列。
安全问题二:不恰当的身份验证
题目
> Implement multi-factor authentication in Java Spring
在 Java Spring 中实现多重身份验证
> Implement MFA as an added layer of security to the login functionality.
将 MFA(多重身份验证) 实施为登录功能的附加安全层
1、介绍
Like many applications, VikingBank uses a login functionality to authenticate users. Currently the application logs in users based on their username and password. Ideally, there should be two layers of security. These layers are:
- Something you know: this is what only a user can know, for example, a password, a pin code, etc.
- Something you have: this is what a user has in their possession, for example, an authenticator app or a hardware token.
This lab is about implementing the second layer in order to have multi-factor authentication (MFA). This layer grants the user extra protection: even if a malicious actor obtains a password, it's much harder to also gain access to the victim's authenticator.
与许多应用程序一样,VikingBank 使用登录功能来验证用户身份。目前,该应用程序根据用户名和密码登录用户。理想情况下,应该有两层安全性。这些层是:
- 你知道的东西:只有用户才能知道的东西,例如密码、PIN 码等。
- 你拥有的东西:这是用户拥有的东西,例如,身份验证器应用程序或硬件令牌。
本实验是关于实施第二层以进行多因素身份验证 (MFA)。这一层为用户提供了额外的保护:即使恶意行为者获得了密码,也很难获得对受害者身份验证器的访问权限。
2、源码及框架
MfaConfig.java
package vikingbank.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
public class MfaConfig {
public SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/bankaccounts").authenticated()
.requestMatchers("/auth/setup").authenticated()
.anyRequest().permitAll())
.formLogin((form) -> form
.successHandler((request, response, authentication) -> {
//implement successHandler
}));
return http.build();
}
}
*左右滑动查看更多
3、步骤一实现 AbstractAuthenticationToken
在 Spring 框架中,当用户通过用户名和密码成功验证时,AuthenticationProvider 将返回一个 Authentication 对象,然后它将此 Authentication 设置在 SecurityContext 中,以确定谁被验证的详细信息。
该框架还提供了 AbstractAuthenticationToken,可用于创建自定义身份验证流程。第一步将创建一个扩展 AbstractAuthenticationToken 的类以捕获 AuthenticationProvider 的身份验证。它将充当临时身份验证,直到用户输入正确的验证码(本实验稍后介绍)。
3.1 task1
打开分配文件夹中的 security/MultiFactorAuthentication.java。
-
使用AbstractAuthenticationToken 类扩展此类。
-
创建一个构造函数,并在其主体中以Collections.emptyList() 作为参数调用super 方法。
-
用户名和密码验证保存在验证对象中。将其注入构造函数中。
Task Solution
private final Authentication authentication;
public MultiFactorAuthentication(Authentication authentication) {
super(Collections.emptyList());
this.authentication = authentication;
}
*左右滑动查看更多
打开分配文件夹中的 security/MultiFactorAuthentication.java
MultiFactorAuthentication.java.
package vikingbank.web.security;
public class MultiFactorAuthentication {
}
*左右滑动查看更多
改完如下:
MultiFactorAuthentication.java
package vikingbank.web.security;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import java.util.Collections;
public class MultiFactorAuthentication extends AbstractAuthenticationToken {
private final Authentication authentication;
public MultiFactorAuthentication(Authentication authentication) {
// 调用父类构造函数,并传入一个空的权限列表
super(Collections.emptyList());
this.authentication = authentication;
}
public Object getPrincipal() {
// 获取认证对象的主体信息
return authentication.getPrincipal();
}
public Object getCredentials() {
// 获取认证对象的凭证信息
return authentication.getCredentials();
}
// 可以添加其他自定义方法或重写父类的方法
}
*左右滑动查看更多
3.2 task2
- 创建一个访问 Authentication 字段的新公共方法。这个方法稍后会派上用场。
step Solution:
python
step Solution
public Authentication getAuthentication() {
return this.authentication;
}
*左右滑动查看更多
直接将Solution加到代码中去:
MultiFactorAuthentication.java
package vikingbank.web.security;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import java.util.Collections;
public class MultiFactorAuthentication extends AbstractAuthenticationToken {
private final Authentication authentication;
public MultiFactorAuthentication(Authentication authentication) {
// 调用父类构造函数,并传入一个空的权限列表
super(Collections.emptyList());
this.authentication = authentication;
}
public Authentication getAuthentication() {
// 获取内部的Authentication对象
return this.authentication;
}
public Object getPrincipal() {
// 获取认证对象的主体信息
return authentication.getPrincipal();
}
public Object getCredentials() {
// 获取认证对象的凭证信息
return authentication.getCredentials();
}
// 可以添加其他自定义方法或重写父类的方法
}
*左右滑动查看更多
3.3 task3
- 重写getPrincipal 方法,返回认证字段的principal。
- 重写getCredentials 方法,返回认证字段的凭据。
task Solution
public Object getCredentials() {
return this.authentication.getCredentials();
}
public Object getPrincipal() {
return this.authentication.getPrincipal();
}
*左右滑动查看更多
根据Solution将代码修改一下:
MultiFactorAuthentication.java
package vikingbank.web.security;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import java.util.Collections;
public class MultiFactorAuthentication extends AbstractAuthenticationToken {
private final Authentication authentication;
public MultiFactorAuthentication(Authentication authentication) {
// 调用父类构造函数,并传入一个空的权限列表
super(Collections.emptyList());
this.authentication = authentication;
}
public Authentication getAuthentication() {
// 获取内部的Authentication对象
return this.authentication;
}
public Object getPrincipal() {
// 获取认证对象的主体信息
return this.authentication.getPrincipal();
}
public Object getCredentials() {
// 获取认证对象的凭证信息
return this.authentication.getCredentials();
}
// 可以添加其他自定义方法或重写父类的方法
}
*左右滑动查看更多
步骤一提交通过。
分析一下:
该代码是一个名为MultiFactorAuthentication的自定义身份验证类,继承自AbstractAuthenticationToken。在该类中,使用传入的Authentication对象来进行身份验证。构造函数中调用了父类的构造函数,并传入一个空的权限列表。
getAuthentication()方法用于获取内部的Authentication对象,以便在需要的时候可以访问其中的信息。getPrincipal()方法返回认证对象的主体信息,getCredentials()方法返回认证对象的凭证信息。你可以根据需要添加其他自定义方法或重写父类的方法来扩展或自定义身份验证类的功能。
4、步骤二 配置安全链
请转到 configMfaConfig.java 文件。SecurityFilterChain - bean 已经部分设置好了。它包含了一个内联的 `successHandler`,用于处理登录表单成功后的操作。缺少的是,在成功验证用户名和密码后,用户需要被重定向到 `auth/verify` 端点,以便他们可以输入验证码。
4.1 task1
- 创建一个
SimpleUrlAuthenticationSuccessHandler的实例,在成功登录表单认证后将用户重定向到另一个端点。
- 将/auth/verify作为 URL 传递给它。
MfaConfig.java
package vikingbank.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
public class MfaConfig {
public SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/bankaccounts").authenticated()
.requestMatchers("/auth/setup").authenticated()
.anyRequest().permitAll())
.formLogin((form) -> form
.successHandler((request, response, authentication) -> {
//implement successHandler
}));
return http.build();
}
}
*左右滑动查看更多
没有Solution,根据要求改下代码:
MfaConfig.java
package vikingbank.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
public class MfaConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 配置请求的授权规则
http.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/bankaccounts").authenticated() // 对于"/bankaccounts"的请求需要身份认证
.requestMatchers("/auth/setup").authenticated() // 对于"/auth/setup"的请求需要身份认证
.anyRequest().permitAll()) // 其他请求允许所有访问
.formLogin((form) -> form
.successHandler(successHandler())); // 配置表单登录,并设置成功处理器
return http.build();
}
public SimpleUrlAuthenticationSuccessHandler successHandler() {
// 创建一个简单的URL认证成功处理器
SimpleUrlAuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();
successHandler.setDefaultTargetUrl("/auth/verify"); // 设置默认的认证成功后的跳转URL为"/auth/verify"
return successHandler;
}
}
*左右滑动查看更多
4.2 task2
- 创建一个新的 `MultiFactorAuthentication` 实例,并将验证参数传递给它。
- 将验证信息设置到 `SecurityContext` 中,并传递该实例。
Hint Solution
SecurityContextHolder.getContext()
*左右滑动查看更多
按照要求改一下:
MfaConfig.java
package vikingbank.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import vikingbank.web.security.MultiFactorAuthentication;
public class MfaConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 配置请求的授权规则
http.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/bankaccounts").authenticated() // 对于"/bankaccounts"的请求需要身份认证
.requestMatchers("/auth/setup").authenticated() // 对于"/auth/setup"的请求需要身份认证
.anyRequest().permitAll()) // 其他请求允许所有访问
.formLogin((form) -> form
.successHandler(successHandler())); // 配置表单登录,并设置成功处理器
return http.build();
}
public SimpleUrlAuthenticationSuccessHandler successHandler() {
// 创建一个简单的URL认证成功处理器
SimpleUrlAuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();
successHandler.setDefaultTargetUrl("/auth/verify"); // 设置默认的认证成功后的跳转URL为"/auth/verify"
// 在认证成功后将当前身份验证对象替换为自定义的多因素身份验证对象
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
MultiFactorAuthentication mfaAuthentication = new MultiFactorAuthentication(authentication);
SecurityContextHolder.getContext().setAuthentication(mfaAuthentication);
return successHandler;
}
}
*左右滑动查看更多
4.3 task3
调用 `SimpleUrlAuthenticationSuccessHandler` 实例上的 `onAuthenticationSuccess` 方法。这将执行实际的重定向操作,并清除任何剩余的会话数据。
step Solution
var successHandler = new SimpleUrlAuthenticationSuccessHandler("/auth/verify");
SecurityContextHolder.getContext().setAuthentication(new MultiFactorAuthentication(authentication));
successHandler.onAuthenticationSuccess(request, response, authentication);
*左右滑动查看更多
按照要求改一下:
MfaConfig.java
package vikingbank.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import vikingbank.web.security.MultiFactorAuthentication;
import java.io.IOException;
public class MfaConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 配置请求的授权规则
http.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/bankaccounts").authenticated() // 对于"/bankaccounts"的请求需要身份认证
.requestMatchers("/auth/setup").authenticated() // 对于"/auth/setup"的请求需要身份认证
.anyRequest().permitAll()) // 其他请求允许所有访问
.formLogin((form) -> form
.successHandler(successHandler())); // 配置表单登录,并设置成功处理器
return http.build();
}
public AuthenticationSuccessHandler successHandler() {
// 创建自定义的认证成功处理器
return new CustomAuthenticationSuccessHandler("/auth/verify");
}
// 自定义的认证成功处理器,继承自SimpleUrlAuthenticationSuccessHandler
private static class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
public CustomAuthenticationSuccessHandler(String defaultTargetUrl) {
setDefaultTargetUrl(defaultTargetUrl);
}
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// 在认证成功后将当前身份验证对象替换为自定义的多因素身份验证对象
SecurityContextHolder.getContext().setAuthentication(new MultiFactorAuthentication(authentication));
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
*左右滑动查看更多
导包的时候需要注意的是项目使用的是Jakarta Servlet规范,应该使用jakarta.servlet包而不是javax.servlet包。
分析一下:
该代码是一个名为MfaConfig的Spring Security配置类,使用@Configuration和@EnableWebSecurity注解进行配置。在该类中,使用HttpSecurity对象来配置请求的授权规则和表单登录。securityFilterChain()方法配置了请求的授权规则,其中对"/bankaccounts"和"/auth/setup"的请求要求进行身份认证,而其他请求允许所有访问。
同时,通过formLogin()方法配置了表单登录,并设置了认证成功后的处理器为successHandler()方法返回的自定义认证成功处理器。successHandler()方法创建并返回一个自定义的认证成功处理器,该处理器继承自SimpleUrlAuthenticationSuccessHandler,并设置默认的跳转URL为"/auth/verify"。
在认证成功后,将当前的身份验证对象替换为自定义的多因素身份验证对象,通过创建MultiFactorAuthentication对象并设置为SecurityContextHolder中的认证对象。这样可以在后续的请求中使用多因素身份验证对象进行授权和认证。
5、 步骤三 实现验证(verify)端点
现在让我们来看一下 controllersAuthController.java 文件。这个控制器包含了所有需要用于 MFA 的端点。滚动到 `verify` 方法。正如你在之前的步骤中所记得的,这个端点用于提供一个视图,让用户输入他们的身份验证代码。然而,MFA 是可选的,当禁用 MFA 时,应用程序不应检查基于时间的一次性密码(TOTP),并授予用户访问权限。
MfaConfig.java
package vikingbank.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import vikingbank.web.security.MultiFactorAuthentication;
import java.io.IOException;
public class MfaConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 配置请求的授权规则
http.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/bankaccounts").authenticated() // 对于"/bankaccounts"的请求需要身份认证
.requestMatchers("/auth/setup").authenticated() // 对于"/auth/setup"的请求需要身份认证
.anyRequest().permitAll()) // 其他请求允许所有访问
.formLogin((form) -> form
.successHandler(successHandler())); // 配置表单登录,并设置成功处理器
return http.build();
}
public AuthenticationSuccessHandler successHandler() {
// 创建自定义的认证成功处理器
return new CustomAuthenticationSuccessHandler("/auth/verify");
}
// 自定义的认证成功处理器,继承自SimpleUrlAuthenticationSuccessHandler
private static class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
public CustomAuthenticationSuccessHandler(String defaultTargetUrl) {
setDefaultTargetUrl(defaultTargetUrl);
}
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// 在认证成功后将当前身份验证对象替换为自定义的多因素身份验证对象
SecurityContextHolder.getContext().setAuthentication(new MultiFactorAuthentication(authentication));
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
*左右滑动查看更多
5.1 task1
- 在验证方法中,添加MultiFactorAuthentication 作为参数。
- 通过VikingbankUserService 获取用户和MFA 类的主体。
Hint
vikingBankUserService.fetchVikingBankUser(principal)
*左右滑动查看更多
5.2 task2
- 当用户未启用MFA 时:将SecurityContext 更改为MultiFactorAuthentication 中存储的原始身份验证。
- 将用户重定向到/bankaccounts 端点。
Task Solution
if (!user.hasMfa()) {
SecurityContextHolder.getContext().setAuthentication(mfa.getAuthentication());
return "redirect:/bankaccounts";
}
*左右滑动查看更多
Step Solution
public String verify(MultiFactorAuthentication mfa) {
var user = this.vikingBankUserService.fetchVikingBankUser(mfa.getPrincipal());
if (!user.hasMfa()) {
SecurityContextHolder.getContext().setAuthentication(mfa.getAuthentication());
return "redirect:/bankaccounts";
}
return "auth/verify";
}
*左右滑动查看更多
Add the imports:
import org.springframework.security.core.context.SecurityContextHolder;
import vikingbank.web.security.MultiFactorAuthentication;
*左右滑动查看更多
将task1和task2整理得到:
AuthController.java
package vikingbank.web.controller;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import vikingbank.web.security.MultiFactorAuthentication;
import vikingbank.web.services.MfaService;
import vikingbank.web.services.VikingBankUserService;
import java.security.Principal;
public class AuthController {
private final MfaService mfaService;
private final VikingBankUserService vikingBankUserService;
public AuthController(MfaService mfaService, VikingBankUserService vikingBankUserService) {
this.mfaService = mfaService;
this.vikingBankUserService = vikingBankUserService;
}
// 处理GET请求"/auth/verify",用于验证多因素身份认证
public String verify(MultiFactorAuthentication mfa) {
// 获取当前身份验证对象对应的用户信息
var user = this.vikingBankUserService.fetchVikingBankUser(mfa.getPrincipal());
if (!user.hasMfa()) {
// 如果用户未设置多因素身份认证,将多因素身份验证对象设置为当前认证对象,并重定向到"/bankaccounts"
SecurityContextHolder.getContext().setAuthentication(mfa.getAuthentication());
return "redirect:/bankaccounts";
}
// 如果用户已设置多因素身份认证,则返回认证页面"auth/verify"
return "auth/verify";
}
// 处理POST请求"/auth/verify",用于提交多因素身份认证的验证码
public String postVerify( Integer code) throws Exception {
// TODO: 处理多因素身份认证的验证码
return "auth/verify";
}
// 处理GET请求"/auth/setup",用于设置多因素身份认证
public String setup(Model model) {
// 添加QR码到模型中,用于展示给用户进行多因素身份认证的设置
model.addAttribute("qrCode", "");
return "auth/setup";
}
// 处理POST请求"/auth/setup",用于提交多因素身份认证的设置
public String postSetup( Boolean isEnabled, Model model, Principal principal) {
// TODO: 处理多因素身份认证的设置
return "auth/setup";
}
}
*左右滑动查看更多
提交通过。
分析一下:
该代码是一个名为 AuthController 的控制器类,用于处理与身份认证相关的请求。其中包含了多个处理请求的方法:
- verify()方法用于验证多因素身份认证。根据用户是否已设置多因素身份认证来决定是否重定向到银行账户页面或返回认证页面。
- postVerify()方法用于提交多因素身份认证的验证码,但目前该方法仅返回认证页面。
- setup()方法用于设置多因素身份认证,返回包含QR码的设置页面。
- postSetup()方法用于提交多因素身份认证的设置,但目前该方法仅返回设置页面。
该代码使用了Spring MVC框架,通过@Controller注解将类标识为控制器,并使用@GetMapping和@PostMapping注解来映射HTTP GET和POST请求的路径。在构造函数中注入了MfaService和VikingBankUserService两个依赖对象。
在verify()方法中,通过MultiFactorAuthentication对象获取当前身份验证对象的主体(Principal),然后使用VikingBankUserService对象获取对应的用户信息。如果用户未设置多因素身份认证,则将多因素身份验证对象设置为当前认证对象,并重定向到/bankaccounts页面;否则返回认证页面。
在postVerify()方法中,暂时没有实现多因素身份认证验证码的处理,只是返回认证页面。
在setup()方法中,将QR码添加到模型中,用于展示给用户进行多因素身份认证的设置,然后返回设置页面。
在postSetup()方法中,暂时没有实现多因素身份认证设置的处理,只是返回设置页面。
需要注意的是,部分方法中存在TODO注释,表示该部分代码需要根据具体需求进行实现。
6、步骤四 实施设置端点
接下来是完成设置(setup)端点。为了使用MFA,用户需要将其添加到所选的MFA应用程序中,例如Google Authenticator。为了实现这一点,应用程序必须生成一个供应URI,并将其显示给用户(通常以QR码的形式)。在应用程序中,当用户提交选择启用MFA时,这个过程应该发生。本实验使用了two-factor-auth库的TOTP实现。
6.1 task1
滚动到postSetup方法并进行以下操作:
- 获取用户(类似于verify端点)。
- 借助VikingBankUserService将用户的选择保存到数据库中。
Task Solution
var user = this.vikingBankUserService.fetchVikingBankUser(principal);
this.vikingBankUserService.saveMfaSetting(isEnabled, user);
*左右滑动查看更多
直接加进去:
AuthController.java
package vikingbank.web.controller;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import vikingbank.web.security.MultiFactorAuthentication;
import vikingbank.web.services.MfaService;
import vikingbank.web.services.VikingBankUserService;
import java.security.Principal;
public class AuthController {
private final MfaService mfaService;
private final VikingBankUserService vikingBankUserService;
public AuthController(MfaService mfaService, VikingBankUserService vikingBankUserService) {
this.mfaService = mfaService;
this.vikingBankUserService = vikingBankUserService;
}
public String verify(MultiFactorAuthentication mfa) {
var user = this.vikingBankUserService.fetchVikingBankUser(mfa.getPrincipal());
if (!user.hasMfa()) {
SecurityContextHolder.getContext().setAuthentication(mfa.getAuthentication());
return "redirect:/bankaccounts"; // 如果用户没有多因素认证,将其认证信息设置到 SecurityContextHolder,并重定向到 "/bankaccounts"
}
return "auth/verify"; // 否则,显示多因素认证页面
}
public String postVerify( Integer code) throws Exception {
return "auth/verify"; // 处理多因素认证提交的表单数据
}
public String setup(Model model) {
model.addAttribute("qrCode", "");
return "auth/setup"; // 显示多因素认证设置页面
}
public String postSetup( Boolean isEnabled, Model model, Principal principal) {
var user = this.vikingBankUserService.fetchVikingBankUser(principal);
this.vikingBankUserService.saveMfaSetting(isEnabled, user); // 保存多因素认证设置
return "auth/setup"; // 返回多因素认证设置页面
}
}
*左右滑动查看更多
6.2 task2
当用户启用 MFA 时:
- 需要生成一个新的 TOTP 密钥。该密钥必须是 base32 编码的。
- 需要创建一个 QR 码的 URL。使用 TimeBasedOneTimePasswordUtil 的 qrImageUrl 方法。
- 作为第一个参数,传入一个可识别的密钥 ID(issuer),例如 VikingBank。
- 作为第二个参数,传入之前生成的密钥。
- 为了在前端显示 QR 码,将一个名为 qrCode 的属性添加到模型中,并将 QR 码的 URL 设置为其值。
- 使用 VikingBankUserService 的方法将 TOTP 密钥保存到数据库中。
Task Solution
public String postSetup( Boolean isEnabled, Model model, Principal principal) {
var user = this.vikingBankUserService.fetchVikingBankUser(principal);
this.vikingBankUserService.saveMfaSetting(isEnabled, user);
if (isEnabled) {
var secret = TimeBasedOneTimePasswordUtil.generateBase32Secret();
model.addAttribute("qrCode", TimeBasedOneTimePasswordUtil.qrImageUrl("VikingBank", secret));
this.vikingBankUserService.saveTotpSecret(secret, user);
}
return "auth/setup";
}
import com.j256.twofactorauth.TimeBasedOneTimePasswordUtil;
*左右滑动查看更多
加进去:
AuthController.java
package vikingbank.web.controller;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import vikingbank.web.security.MultiFactorAuthentication;
import vikingbank.web.services.MfaService;
import vikingbank.web.services.VikingBankUserService;
import java.security.Principal;
import com.j256.twofactorauth.TimeBasedOneTimePasswordUtil;
public class AuthController {
private final MfaService mfaService;
private final VikingBankUserService vikingBankUserService;
public AuthController(MfaService mfaService, VikingBankUserService vikingBankUserService) {
this.mfaService = mfaService;
this.vikingBankUserService = vikingBankUserService;
}
public String verify(MultiFactorAuthentication mfa) {
var user = this.vikingBankUserService.fetchVikingBankUser(mfa.getPrincipal());
if (!user.hasMfa()) {
SecurityContextHolder.getContext().setAuthentication(mfa.getAuthentication());
return "redirect:/bankaccounts"; // 如果用户没有多因素认证,将其认证信息设置到 SecurityContextHolder,并重定向到 "/bankaccounts"
}
return "auth/verify"; // 否则,显示多因素认证页面
}
public String postVerify( Integer code) throws Exception {
return "auth/verify"; // 处理多因素认证提交的表单数据
}
public String setup(Model model) {
model.addAttribute("qrCode", "");
return "auth/setup"; // 显示多因素认证设置页面
}
public String postSetup( Boolean isEnabled, Model model, Principal principal) {
var user = this.vikingBankUserService.fetchVikingBankUser(principal);
this.vikingBankUserService.saveMfaSetting(isEnabled, user); // 保存多因素认证设置
if (isEnabled) {
var secret = TimeBasedOneTimePasswordUtil.generateBase32Secret();
model.addAttribute("qrCode", TimeBasedOneTimePasswordUtil.qrImageUrl("VikingBank", secret));
this.vikingBankUserService.saveTotpSecret(secret, user); // 生成并保存基于时间的一次性密码(TOTP)的密钥
}
return "auth/setup"; // 返回多因素认证设置页面
}
}
*左右滑动查看更多
提交通过。
分析一下:
这段代码是一个Spring MVC的控制器类,用于处理身份验证相关的请求。它包含以下方法:
- verify()方法处理GET请求/auth/verify,用于验证多因素认证。如果用户没有启用多因素认证,则将其认证信息设置到SecurityContextHolder中,并重定向到/bankaccounts页面。否则,显示多因素认证页面。
- postVerify()方法处理POST请求/auth/verify,用于处理多因素认证的提交表单数据。
- setup()方法处理GET请求/auth/setup,用于显示多因素认证设置页面。
- postSetup()方法处理POST请求/auth/setup,用于保存多因素认证设置。如果启用多因素认证,则生成基于时间的一次性密码(TOTP)的密钥。
7、步骤五 实施 Mfa 服务
现在用户可以设置MFA了!接下来,浏览到servicesMfaService.java以实现下一步:验证提交的代码。
然而,当MFA应用程序为用户返回代码时,这些代码仍然需要在后端进行验证。浏览到servicesMfaService.java,将在其中完成此操作。由于RFC建议如此,TOTP只能使用一次。
MfaService.java源码:
MfaService.java
package vikingbank.web.services;
import org.springframework.stereotype.Service;
import vikingbank.web.security.MultiFactorAuthentication;
import java.security.GeneralSecurityException;
public class MfaService {
private final VikingBankUserService vikingBankUserService;
public MfaService(VikingBankUserService vikingBankUserService) {
this.vikingBankUserService = vikingBankUserService;
}
public boolean verifyCode(MultiFactorAuthentication authentication, Integer code) throws GeneralSecurityException {
return false;
}
}
*左右滑动查看更多
7.1 task1
在 verifyCode 方法中:与前面的步骤类似,获取用户。
按照要求修改代码:
MfaService.java
package vikingbank.web.services;
import org.springframework.stereotype.Service;
import com.j256.twofactorauth.TimeBasedOneTimePasswordUtil;
import vikingbank.web.security.MultiFactorAuthentication;
import java.security.GeneralSecurityException;
public class MfaService {
private final VikingBankUserService vikingBankUserService;
public MfaService(VikingBankUserService vikingBankUserService) {
this.vikingBankUserService = vikingBankUserService;
}
/**
* 验证多因素认证代码是否正确
*
* @param authentication 多因素认证对象
* @param code 用户输入的认证代码
* @return 如果认证代码正确,则返回true;否则返回false
* @throws GeneralSecurityException 当发生安全异常时抛出
*/
public boolean verifyCode(MultiFactorAuthentication authentication, Integer code) throws GeneralSecurityException {
var user = vikingBankUserService.fetchVikingBankUser(authentication.getPrincipal());
String secret = vikingBankUserService.getTotpSecret(user);
return TimeBasedOneTimePasswordUtil.validateCurrentNumber(secret, code, 0);
}
}
*左右滑动查看更多
7.2 task2
创建一个检查来验证提交的令牌。调用TimeBasedOneTimePasswordUtil类的validateCurrentNumber函数来实现此功能。该函数有三个参数:
- 密钥:传入用户的TOTP密钥。
- 提供的代码。
- TOTP有效的时间窗口:推荐的时间是30秒。
Task Solution
if (TimeBasedOneTimePasswordUtil.validateCurrentNumber(user.getTotpSecret(), Integer.parseInt(code), 30000)) {
return true;
}
return false;
*左右滑动查看更多
根据要求改一下:
java
public boolean verifyCode(MultiFactorAuthentication authentication, Integer code) throws GeneralSecurityException {
var user = vikingBankUserService.fetchVikingBankUser(authentication.getPrincipal());
String secret = vikingBankUserService.getTotpSecret(user);
long windowMillis = 30000; // 30秒的时间窗口,单位为毫秒
return TimeBasedOneTimePasswordUtil.validateCurrentNumber(secret, code, windowMillis);
}
*左右滑动查看更多
7.3 task3
用户实体有一个字段用于跟踪最后提交的代码。根据RFC的建议,一次性密码(TOTP)只能使用一次。当提供的代码有效时,检查它是否是最后使用的代码。如果不是最后使用的代码:
- 将传递的代码保存为新的最后使用的代码。
- 返回true。
Task Solution
// pseudocode
if (supplied code is valid) {
if(user.lastUsedCode is not equal to submitted code) {
//save last used token to user entity
return true;
}
return false;
}
*左右滑动查看更多
import com.j256.twofactorauth.TimeBasedOneTimePasswordUtil;
import java.util.Objects;
Step Solution
public boolean verifyCode(MultiFactorAuthentication authentication, Integer code) throws GeneralSecurityException {
var user = this.vikingBankUserService.fetchVikingBankUser(authentication.getPrincipal());
if (TimeBasedOneTimePasswordUtil.validateCurrentNumber(user.getTotpSecret(), code, 30000)) {
if (!Objects.equals(user.getLastUsedCode(), code)) {
this.vikingBankUserService.saveLastUsedCode(code, user);
return true;
}
return false;
}
return false;
}
*左右滑动查看更多
修改一下:
MfaService.java
package vikingbank.web.services;
import org.springframework.stereotype.Service;
import com.j256.twofactorauth.TimeBasedOneTimePasswordUtil;
import vikingbank.web.security.MultiFactorAuthentication;
import java.security.GeneralSecurityException;
import java.util.Objects;
public class MfaService {
private final VikingBankUserService vikingBankUserService;
public MfaService(VikingBankUserService vikingBankUserService) {
this.vikingBankUserService = vikingBankUserService;
}
/**
* 验证多因素认证代码是否正确
*
* @param authentication 多因素认证对象
* @param code 用户输入的认证代码
* @return 如果认证代码正确,则返回true;否则返回false
* @throws GeneralSecurityException 当发生安全异常时抛出
*/
public boolean verifyCode(MultiFactorAuthentication authentication, Integer code) throws GeneralSecurityException {
var user = this.vikingBankUserService.fetchVikingBankUser(authentication.getPrincipal());
if (TimeBasedOneTimePasswordUtil.validateCurrentNumber(user.getTotpSecret(), code, 30000)) {
if (!Objects.equals(user.getLastUsedCode(), code)) {
this.vikingBankUserService.saveLastUsedCode(code, user);
return true;
}
}
return false;
}
}
*左右滑动查看更多
提交通过。
分析一下:
这段代码是一个MfaService类,用于提供多因素认证相关的服务。它包含以下方法:
- verifyCode()方法用于验证用户输入的多因素认证代码是否正确。它接收一个MultiFactorAuthentication对象和用户输入的认证代码作为参数,并返回一个布尔值来表示认证代码的正确性。如果认证代码正确,返回true;否则返回false。该方法使用VikingBankUserService来获取用户的密钥,并使用TimeBasedOneTimePasswordUtil类来验证认证代码的正确性。
8、步骤六 实施验证后端点
快完成了!基于时间的一次性密码现在可以正确验证。剩下要做的就是实现 post 方法。前往controllersAuthController.java 中的postVerify 开始。
8.1 task1
添加 MultiFactorAuthentication 作为 postVerify 方法的参数。
8.2 task2
创建调用 MfaService 的 verifyCode 的检查。当服务返回肯定结果时:
- 设置SecurityContext的认证,并通过MFA参数的认证。
- 将用户重定向到 /bankaccounts 端点。
8.3 task3
当提供的代码无效时,将用户重定向回身份验证/验证端点以重试。
step Solution
public String postVerify( Integer code, MultiFactorAuthentication mfa) throws Exception {
if (this.mfaService.verifyCode(mfa, code)) {
SecurityContextHolder.getContext().setAuthentication(mfa.getAuthentication());
return "redirect:/bankaccounts";
}
return "auth/verify";
}
*左右滑动查看更多
直接改一下代码即可:
AuthController.java
package vikingbank.web.controller;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import vikingbank.web.security.MultiFactorAuthentication;
import vikingbank.web.services.MfaService;
import vikingbank.web.services.VikingBankUserService;
import java.security.Principal;
import com.j256.twofactorauth.TimeBasedOneTimePasswordUtil;
public class AuthController {
private final MfaService mfaService;
private final VikingBankUserService vikingBankUserService;
public AuthController(MfaService mfaService, VikingBankUserService vikingBankUserService) {
this.mfaService = mfaService;
this.vikingBankUserService = vikingBankUserService;
}
/**
* 显示多因素认证页面
*
* @param mfa 多因素认证对象
* @return 如果用户未启用多因素认证,则重定向到银行账户页面;否则返回多因素认证页面
*/
public String verify(MultiFactorAuthentication mfa) {
var user = this.vikingBankUserService.fetchVikingBankUser(mfa.getPrincipal());
if (!user.hasMfa()) {
SecurityContextHolder.getContext().setAuthentication(mfa.getAuthentication());
return "redirect:/bankaccounts";
}
return "auth/verify";
}
/**
* 处理提交的多因素认证代码
*
* @param code 多因素认证代码
* @param mfa 多因素认证对象
* @return 如果代码验证通过,则设置认证信息并重定向到银行账户页面;否则返回多因素认证页面
* @throws Exception 验证过程中可能发生的异常
*/
public String postVerify( Integer code, MultiFactorAuthentication mfa) throws Exception {
if (this.mfaService.verifyCode(mfa, code)) {
SecurityContextHolder.getContext().setAuthentication(mfa.getAuthentication());
return "redirect:/bankaccounts";
}
return "auth/verify";
}
/**
* 显示多因素认证设置页面
*
* @param model 数据模型
* @return 多因素认证设置页面
*/
public String setup(Model model) {
model.addAttribute("qrCode", "");
return "auth/setup";
}
/**
* 处理提交的多因素认证设置
*
* @param isEnabled 启用状态
* @param model 数据模型
* @param principal 当前用户的主体对象
* @return 多因素认证设置页面
*/
public String postSetup( Boolean isEnabled, Model model, Principal principal) {
var user = this.vikingBankUserService.fetchVikingBankUser(principal);
this.vikingBankUserService.saveMfaSetting(isEnabled, user);
if (isEnabled) {
var secret = TimeBasedOneTimePasswordUtil.generateBase32Secret();
model.addAttribute("qrCode", TimeBasedOneTimePasswordUtil.qrImageUrl("VikingBank", secret));
this.vikingBankUserService.saveTotpSecret(secret, user);
}
return "auth/setup";
}
}
*左右滑动查看更多
提交通过。
分析一下:
这段代码是一个控制器类,用于处理认证相关的请求。它包含以下方法:
- verify()方法用于显示多因素认证页面。如果用户未启用多因素认证,则重定向到银行账户页面。
- postVerify()方法用于处理提交的多因素认证代码。它通过调用MfaService的verifyCode()方法验证代码的正确性。如果验证通过,则设置认证信息并重定向到银行账户页面。
- setup()方法用于显示多因素认证设置页面。
- postSetup()方法用于处理提交的多因素认证设置。它根据用户选择的启用状态进行相应的处理,并生成二维码供用户扫描。最后返回多因素认证设置页面。
总结
通过实施多因素认证(MFA)过程,用户必须提交两个(或更多)证据来证明其身份。这将减少攻击者绕过访问控制并访问其他用户账户的机会。本实验介绍了基于时间的一次性密码(TOTP)认证方法。
(未完待续)
插播一条招聘信息
一、安全研究工程师实习生(24/25届)
工作地点:深圳
岗位职责:
1、具有较强的责任感、具备能够独立的开展工作的能力、自学能力强、做事踏实认真;
2、对防御对抗、反溯源、攻击利用等相关红队工具进行研究和开发;
3、熟悉OWASP TOP 10,具有网络安全、系统安全、Web安全等方面的理论基础;
4、熟悉常见编程语言中的一种(Java、Python、PHP、GO),并能够熟练写出针对性的测试脚本;
5、参与区域内网渗透测试、代码审计、红蓝对抗活动、最新漏洞动态跟踪及复现、风险评估、客户培训等工作;
6、主要参与新服务、新技术创新服务的研究;
7、根据ATT&CK框架梳理研究相关TPPs,并形成对应的检测规则。
加分项:
1、具有渗透测试经验或逆向分析能力或溯源分析能力,曾经参与过大型的红蓝对抗项目;
2、熟悉Java、Python、PHP、GO等编程,并有良好的编程习惯和丰富的代码经验;
3、具备钻研精神,愿意在安全领域做出技术突破;
4、具有较强的责任感、具备能够独立的开展工作的能力、自学能力强、做事踏实认真;
二、代码审计工程师实习生(24/25届)
工作地点:深圳
岗位职责:
1、跟踪和分析业界最新安全漏洞。
2、挖掘Java、PHP程序中未知的安全漏洞和代码缺陷,并对漏洞进行验证,编制安全加固报告;
3、主要参与新服务、新技术创新服务的研究;
任职要求:
1、对JAVA/PHP编程有较深入的了解,具备较强的Java/PHP代码审计能力,有丰富实战能力;
2、熟悉JAVA/PHP主流框架,具备有一定的编程能力;
3、深入理解常见安全漏洞产生原理及防范方法;
4、熟练掌握源代码测试工具及测试流程,有CNVD、CNNVD等漏洞证书、CVE或CTF比赛获奖者者优先。
5、熟悉主流的源代码审计工具;
6、思路清晰,具有优秀的分析、解决问题的能力,有良好的学习能力及团队协作能力;
7、具备较强的沟通能力、抗压能力,团队合作精神及钻研精神。
简历投递可扫描本文末二维码添加小编微信,或直接发送至邮箱[email protected]
原文始发于微信公众号(安恒信息安全服务):九维团队-绿队(改进)| Java Spring编码安全系列之不恰当的身份验证
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论