Spring Security添加二次認(rèn)證的項(xiàng)目實(shí)踐
一、 簡(jiǎn)介
1 Spring Security概述
Spring Security是一個(gè)基于Spring框架的安全框架,用于為Java應(yīng)用程序提供身份驗(yàn)證和授權(quán)服務(wù)。
2 二次認(rèn)證的必要性
傳統(tǒng)的用戶名和密碼驗(yàn)證方式存在被破解的風(fēng)險(xiǎn),因此在用戶登錄后需要進(jìn)行二次認(rèn)證,增強(qiáng)身份驗(yàn)證的安全性。
二、 Spring Security實(shí)現(xiàn)二次認(rèn)證的方式
1 使用已有二次認(rèn)證服務(wù)
1.1 集成Google Authenticator
Google Authenticator是一款基于TOTP算法的開(kāi)源軟件,用戶可以將其安裝在智能手機(jī)上,以便進(jìn)行二次認(rèn)證。Spring Security可以通過(guò)Google Authenticator進(jìn)行二次認(rèn)證。下面是一個(gè)簡(jiǎn)單的示例。
1.1.1 在pom.xml中添加依賴
<dependency> <groupId>com.warrenstrange</groupId> <artifactId>googleauth</artifactId> <version>1.0.0</version> </dependency>
1.1.2 實(shí)現(xiàn)Google Authenticator
@Service public class GoogleAuthenticatorService { private final GoogleAuthenticator gAuth = new GoogleAuthenticator(); /** 獲取密鑰 **/ public String createSecret() { final GoogleAuthenticatorKey gak = gAuth.createCredentials(); return gak.getKey(); } /** 驗(yàn)證身份 **/ public boolean authorize(final String secret, final int otp) { return gAuth.authorize(secret, otp); } /** 獲取二維碼 **/ public String getQR(final String secret, final String account) { final String format = "otpauth://totp/%s?secret=%s&issuer=%s"; return String.format(format, account, secret, account); } }
在上述代碼中創(chuàng)建了一個(gè)GoogleAuthenticatorService類,用于實(shí)現(xiàn)Google Authenticator的相關(guān)功能。其中,createSecret()方法用于創(chuàng)建一個(gè)新的密鑰,authorize()方法用于驗(yàn)證一個(gè)TOTP是否有效,getQR()方法用于生成一個(gè)TOTP的二維碼。
1.1.3 在Spring Security中配置Google Authenticator
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private GoogleAuthenticatorService googleAuthenticatorService; @Override protected void configure(final HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/user/**").authenticated() .antMatchers("/admin/**").hasAnyRole("ADMIN") .and() .formLogin() .and() .addFilterBefore(buildGoogleAuthFilter(), UsernamePasswordAuthenticationFilter.class); } private GoogleAuthFilter buildGoogleAuthFilter() throws Exception { final GoogleAuthFilter filter = new GoogleAuthFilter("/check-google-auth"); filter.setSecretProvider((request, username) -> { final String secret = userService.getSecret(username); // 獲取用戶的密鑰 return secret != null ? secret : ""; }); filter.setGoogleAuthenticator(googleAuthenticatorService.getGoogleAuthenticator()); return filter; } }
在上述代碼中創(chuàng)建了一個(gè)SecurityConfig類,并在其中定義了一個(gè)GoogleAuthFilter過(guò)濾器用于添加二次認(rèn)證功能。在該過(guò)濾器中,我們通過(guò)setSecretProvider()方法獲取用戶的密鑰,然后通過(guò)setGoogleAuthenticator()方法設(shè)置Google Authenticator的實(shí)例。
1.2 集成Authy
Authy是一款基于TOTP算法的二次認(rèn)證服務(wù)提供商,可以為Spring Security提供二次認(rèn)證服務(wù)。下面是一個(gè)簡(jiǎn)單的示例。
1.2.1 在pom.xml中添加依賴
<dependency> <groupId>com.authy</groupId> <artifactId>authy-client</artifactId> <version>1.2</version> </dependency>
1.2.2 實(shí)現(xiàn)Authy
@Service public class AuthyService { /** Authy API Key **/ private final static String AUTHY_API_KEY = "your-authy-api-key"; /** Authy客戶端 **/ private final AuthyApiClient authyApiClient = new AuthyApiClient(AUTHY_API_KEY); /** 注冊(cè)用戶 **/ public User createUser(final String email, final String countryCode, final String phone) throws Exception { final Users users = authyApiClient.getUsers(); final User user = users.createUser(email, phone, countryCode); return user; } /** 發(fā)送驗(yàn)證碼 **/ public void sendVerification(final String userId, final String via) throws Exception { final Tokens tokens = authyApiClient.getTokens(); tokens.requestSms(Integer.valueOf(userId), via); } /** 驗(yàn)證驗(yàn)證碼 **/ public boolean verifyToken(final String userId, final int token) throws Exception { final Tokens tokens = authyApiClient.getTokens(); final TokenVerification tokenVerification = tokens.verify(Integer.valueOf(userId), token); return tokenVerification.isOk(); } }
在上述代碼中創(chuàng)建了一個(gè)AuthyService類,用于與Authy客戶端進(jìn)行交互。其中,createUser()方法用于注冊(cè)一個(gè)新用戶,sendVerification()方法用于向用戶發(fā)送驗(yàn)證碼,verifyToken()方法用于驗(yàn)證驗(yàn)證碼是否有效。
1.2.3 在Spring Security中配置Authy
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthyService authyService; @Override protected void configure(final HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/user/**").authenticated() .antMatchers("/admin/**").hasAnyRole("ADMIN") .and() .formLogin() .and() .addFilterBefore(buildAuthyFilter(), UsernamePasswordAuthenticationFilter.class); } private AuthyFilter buildAuthyFilter() throws Exception { final AuthyFilter filter = new AuthyFilter("/check-authy"); filter.setAuthyService(authyService); return filter; } }
在上述代碼中,創(chuàng)建了一個(gè)SecurityConfig類,并在其中定義了一個(gè)AuthyFilter過(guò)濾器用于添加二次認(rèn)證功能。在該過(guò)濾器中,我們通過(guò)setAuthyService()方法設(shè)置AuthyService的實(shí)例。
2 自己開(kāi)發(fā)二次認(rèn)證模塊
2.1 實(shí)現(xiàn)二次認(rèn)證功能
自己開(kāi)發(fā)二次認(rèn)證模塊意味著我們需要實(shí)現(xiàn)自己的TOTP算法。下面是一個(gè)簡(jiǎn)單的例子。
@Service public class TotpService { private final static int WINDOW_SIZE = 3; private final static int CODE_DIGITS = 6; private final static String HMAC_ALGORITHM = "HmacSHA1"; private static final int[] DIGITS_POWER = // 0 1 2 3 4 5 6 7 8 {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 }; /** 獲取密鑰 **/ public String createSecret() { final SecureRandom random = new SecureRandom(); final byte[] bytes = new byte[64]; random.nextBytes(bytes); return Base32Utils.encode(bytes); } /** 生成驗(yàn)證碼 **/ public int generateCode(final String secret) throws Exception { final long timeIndex = System.currentTimeMillis() / 30000L; // 30秒 final byte[] keyBytes = Base32Utils.decode(secret); final byte[] data = new byte[8]; for (int i = 7; i >= 0; i--) { data[i] = (byte) (timeIndex & 0xff); timeIndex >>= 8; } final SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_ALGORITHM); final Mac mac = Mac.getInstance(HMAC_ALGORITHM); mac.init(signingKey); final byte[] hmac = mac.doFinal(data); int offset = hmac[hmac.length - 1] & 0xf; int binCode = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | (hmac[offset + 3] & 0xff); return binCode % DIGITS_POWER[CODE_DIGITS]; } /** 驗(yàn)證身份 **/ public boolean authorize(final String secret, final int otp, final int tolerance) throws Exception { final long timeIndex = System.currentTimeMillis() / 30000L; // 30秒 final byte[] keyBytes = Base32Utils.decode(secret); for (int i = -tolerance; i <= tolerance; i++) { final long ti = timeIndex + i; final byte[] data = new byte[8]; for (int j = 7; j >= 0; j--) { data[j] = (byte) (ti & 0xff); ti >>= 8; } final SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_ALGORITHM); final Mac mac = Mac.getInstance(HMAC_ALGORITHM); mac.init(signingKey); final byte[] hmac = mac.doFinal(data); int offset = hmac[hmac.length - 1] & 0xf; int binCode = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | (hmac[offset + 3] & 0xff); if (binCode % DIGITS_POWER[CODE_DIGITS] == otp) { return true; } } return false; } }
在上述代碼中創(chuàng)建了一個(gè)TotpService類,用于實(shí)現(xiàn)二次認(rèn)證功能。其中,createSecret()方法用于創(chuàng)建一個(gè)新的密鑰,generateCode()方法用于生成一個(gè)TOTP驗(yàn)證碼,authorize()方法用于驗(yàn)證TOTP是否有效。
2.2 在Spring Security中配置自己的二次認(rèn)證模塊
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private TotpService totpService; @Override protected void configure(final HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/user/**").authenticated() .antMatchers("/admin/**").hasAnyRole("ADMIN") .and() .formLogin() .and() .addFilterBefore(buildTotpFilter(), UsernamePasswordAuthenticationFilter.class); } private TotpFilter buildTotpFilter() throws Exception { final TotpFilter filter = new TotpFilter("/check-totp"); filter.setTotpService(totpService); return filter; } }
在上述代碼中創(chuàng)建了一個(gè)SecurityConfig類,并在其中定義了一個(gè)TotpFilter過(guò)濾器用于添加二次認(rèn)證功能。在該過(guò)濾器中,我們通過(guò)setTotpService()方法設(shè)置TotpService的實(shí)例。
三、實(shí)現(xiàn)過(guò)程
1 集成Google Authenticator
1.1 安裝并配置Google Authenticator
首先需要在服務(wù)器端安裝和配置Google Authenticator。具體步驟可以按照以下操作進(jìn)行:
在Linux系統(tǒng)下,使用以下命令安裝Google Authenticator:
sudo apt-get install libpam-google-authenticator -y
創(chuàng)建Google Authenticator配置文件,使用以下命令:
google-authenticator
在進(jìn)行配置時(shí)需要解答一系列問(wèn)題,例如:
- 你是否要對(duì)已映射的用戶強(qiáng)制啟用谷歌身份驗(yàn)證?
- 你是否要對(duì)所有新用戶強(qiáng)制啟用谷歌身份驗(yàn)證?
- 是否允許谷歌身份驗(yàn)證用于SSH登錄?
- 是否允許網(wǎng)頁(yè)版的Google Authenticator?
- 是否在身份驗(yàn)證時(shí)使用時(shí)間戳窗口?
一般情況下可以選擇默認(rèn)值。最后會(huì)得到一個(gè)二維碼和一個(gè)密鑰,需要記錄下來(lái)用于后面的配置。
配置PAM服務(wù),編輯/etc/pam.d/sshd文件,添加以下命令:
auth required pam_google_authenticator.so nullok
這個(gè)命令將強(qiáng)制啟用Google Authenticator身份驗(yàn)證并防止用戶跳過(guò)身份驗(yàn)證。如果你只想讓一部分用戶使用Google Authenticator進(jìn)行身份驗(yàn)證,可以使用下面的命令:
auth [success=done new_authtok_reqd=done default=ignore] pam_google_authenticator.so nullok
1.2 在Spring Security中配置Google Authenticator
在將Google Authenticator與Spring Security集成時(shí)需要完成以下步驟:
添加依賴
我們需要在項(xiàng)目中添加以下依賴項(xiàng):
<dependency> <groupId>com.warrenstrange</groupId> <artifactId>googleauth</artifactId> <version>0.8.1</version> </dependency>
這是一個(gè)開(kāi)源的Google Authenticator實(shí)現(xiàn)庫(kù),我們將使用它來(lái)生成TOTP(基于時(shí)間的一次性密碼)。
實(shí)現(xiàn)自定義的Spring Security過(guò)濾器
我們需要自定義一個(gè)Spring Security過(guò)濾器,用于驗(yàn)證用戶的TOTP是否合法。過(guò)濾器的核心代碼如下所示:
public class TotpFilter extends OncePerRequestFilter { // 處理Totp認(rèn)證請(qǐng)求的Handler private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler("/login?error"); // 用于獲取用戶通過(guò)表單提交的Totp碼 private String totpParameter = "totpCode"; // 處理Totp認(rèn)證請(qǐng)求的URL private String authenticationUrl = "/authenticate/totp"; // 用于生成Totp驗(yàn)證碼 private GoogleAuthenticator gAuth; // 構(gòu)造函數(shù),傳入處理Totp認(rèn)證請(qǐng)求的URL public TotpFilter(String authenticationUrl) { gAuth = new GoogleAuthenticator(); setFilterProcessesUrl(authenticationUrl); } @Override public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { try { // 獲取表單中提交的Totp碼 String totpCode = request.getParameter(totpParameter); if (StringUtils.isBlank(totpCode)) { throw new TotpException("Totp code is missing."); } // 獲取當(dāng)前用戶 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !(authentication.getPrincipal() instanceof User)) { throw new TotpException("User not authenticated."); } // 生成Totp驗(yàn)證碼 User user = (User) authentication.getPrincipal(); int code = Integer.parseInt(totpCode); boolean isCodeValid = gAuth.authorize(user.getTotpSecret(), code); if (isCodeValid) { // 驗(yàn)證通過(guò),繼續(xù)執(zhí)行過(guò)濾鏈 chain.doFilter(request, response); } else { // 驗(yàn)證失敗,跳轉(zhuǎn)到錯(cuò)誤頁(yè)面 failureHandler.onAuthenticationFailure(request, response, new BadCredentialsException("Invalid totp code.")); } } catch (TotpException e) { // 驗(yàn)證失敗,跳轉(zhuǎn)到錯(cuò)誤頁(yè)面 failureHandler.onAuthenticationFailure(request, response, new BadCredentialsException(e.getMessage())); } } }
這個(gè)過(guò)濾器的核心邏輯是從請(qǐng)求中獲取用戶提交的TOTP碼,并使用Google Authenticator庫(kù)驗(yàn)證TOTP碼是否合法。如果TOTP碼合法,則繼續(xù)執(zhí)行過(guò)濾器鏈,否則跳轉(zhuǎn)到錯(cuò)誤頁(yè)面。
在Spring Security配置中注冊(cè)自定義過(guò)濾器
我們需要在Spring Security配置中注冊(cè)自定義過(guò)濾器:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(new TotpFilter("/authenticate/totp"), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers("/user/**").hasAnyRole("USER", "ADMIN") .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .formLogin().defaultSuccessUrl("/home") .and() .logout().logoutSuccessUrl("/login?logout"); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password("password").roles("USER") .and() .withUser("admin").password("password").roles("ADMIN"); } }
在上述代碼中通過(guò)addFilterBefore()方法注冊(cè)了我們所實(shí)現(xiàn)的自定義Totp過(guò)濾器。這個(gè)過(guò)濾器將在Spring Security的UsernamePasswordAuthenticationFilter之前執(zhí)行,用于驗(yàn)證用戶輸入的TOTP碼是否合法。
1.3 測(cè)試Google Authenticator的集成
在進(jìn)行Google Authenticator集成測(cè)試時(shí)需要完成以下步驟:
使用Google Authenticator手機(jī)應(yīng)用程序掃描生成二維碼的圖像。掃描后,你將獲得一個(gè)六位數(shù)字的二次認(rèn)證Totp碼。
在登錄時(shí),除了用戶名和密碼外,還需要輸入TOTP碼。
如果TOTP碼是有效的,將會(huì)自動(dòng)跳轉(zhuǎn)到指定的首頁(yè)。
2 集成Authy
2.1 注冊(cè)并配置Authy
首先需要在Authy網(wǎng)站上注冊(cè)一個(gè)賬戶。然后需要?jiǎng)?chuàng)建一個(gè)新的應(yīng)用程序,獲取應(yīng)用程序的Api Key和App Id,這兩個(gè)值后面需要在代碼中使用。
2.2 在Spring Security中配置Authy
在沒(méi)有使用Google Authenticator身份驗(yàn)證的情況下,Authy可以實(shí)現(xiàn)二次身份驗(yàn)證。我們可以按照以下步驟來(lái)將Authy與Spring Security集成:
添加Authy SDK
我們需要添加Authy的java SDK作為項(xiàng)目的依賴:
<dependency> <groupId>com.authy</groupId> <artifactId>authy-java</artifactId> <version>3.0.0</version> </dependency>
自定義Spring Security過(guò)濾器
我們需要自定義一個(gè)Spring Security過(guò)濾器,用于驗(yàn)證用戶的Authy TOTP是否合法。過(guò)濾器的核心代碼如下所示:
/** * 過(guò)濾器,用于校驗(yàn)Authy TOTP驗(yàn)證碼是否合法。 */ public class AuthyTotpFilter extends OncePerRequestFilter { // 處理Totp認(rèn)證請(qǐng)求的Handler private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler("/login?error"); // 用于獲取用戶通過(guò)表單提交的Totp碼 private String totpParameter = "totpCode"; // 處理Authy認(rèn)證請(qǐng)求的URL private String authenticationUrl = "/authenticate/authy"; // Authy SDK的實(shí)例 private AuthyApiClient authyApiClient = new AuthyApiClient( System.getProperty("authy.api.key"), System.getProperty("authy.api.url")); /** * 構(gòu)造函數(shù),需要傳入處理Totp認(rèn)證請(qǐng)求的URL。 */ public AuthyTotpFilter(String authenticationUrl) { setFilterProcessesUrl(authenticationUrl); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { // 獲取表單中提交的Authy TOTP碼 String token = request.getParameter(totpParameter); if (StringUtils.isBlank(token)) { throw new AuthyException("Authy token is missing."); } // 獲取當(dāng)前用戶 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !(authentication.getPrincipal() instanceof User)) { throw new AuthyException("User not authenticated."); } // 獲取用戶綁定的Authy設(shè)備 User user = (User) authentication.getPrincipal(); String authyId = user.getAuthyId(); if (StringUtils.isBlank(authyId)) { throw new AuthyException("Authy ID not found."); } // 驗(yàn)證Authy TOTP碼 StartTokenVerificationResponse response1 = authyApiClient.getTokens().verify(authyId, token, true); if (response1.isValid()) { // 認(rèn)證通過(guò),繼續(xù)執(zhí)行過(guò)濾鏈 filterChain.doFilter(request, response); } else { // 認(rèn)證失敗,跳轉(zhuǎn)到錯(cuò)誤頁(yè)面 failureHandler.onAuthenticationFailure(request, response, new BadCredentialsException("Invalid authy token.")); } } catch (AuthyException e) { // 認(rèn)證失敗,跳轉(zhuǎn)到錯(cuò)誤頁(yè)面 failureHandler.onAuthenticationFailure(request, response, new BadCredentialsException(e.getMessage())); } } }
這個(gè)過(guò)濾器的核心邏輯是從表單中獲取用戶的Authy的TOTP碼,并通過(guò)AuthyApiClient類進(jìn)行驗(yàn)證。如果驗(yàn)證通過(guò),則繼續(xù)執(zhí)行過(guò)濾器鏈,否則跳轉(zhuǎn)到錯(cuò)誤頁(yè)面。
在Spring Security配置中注冊(cè)自定義過(guò)濾器
我們需要在Spring Security配置中注冊(cè)自定義的過(guò)濾器:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(new AuthyTotpFilter("/authenticate/authy"), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers("/user/**").hasAnyRole("USER", "ADMIN") .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .formLogin().defaultSuccessUrl("/home") .and() .logout().logoutSuccessUrl("/login?logout"); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("user").password("password").roles("USER") .and() .withUser("admin").password("password").roles("ADMIN"); } }
在上述代碼中通過(guò)addFilterBefore()方法注冊(cè)了我們所實(shí)現(xiàn)的自定義AuthyTotp過(guò)濾器。這個(gè)過(guò)濾器將在Spring Security的UsernamePasswordAuthenticationFilter之前執(zhí)行,用于驗(yàn)證用戶輸入的Authy TOTP碼是否合法。
2.3 測(cè)試Authy的集成
在進(jìn)行Authy集成測(cè)試時(shí)需要完成以下步驟:
首先需要獲取用戶的Authy ID,這個(gè)ID是在用戶注冊(cè)時(shí)與Authy相關(guān)的。你可以通過(guò)短信、電話、電話回?fù)芎虯uthy手機(jī)應(yīng)用等途徑獲取這個(gè)ID。
在Authy手機(jī)應(yīng)用中可以看到一個(gè)數(shù)字碼,用于確認(rèn)設(shè)備。輸入這個(gè)數(shù)字碼后,你可以得到一個(gè)六位數(shù)字的二次認(rèn)證Totp碼。
在登錄時(shí)除了用戶名和密碼外還需要輸入Authy TOTP碼。
如果Authy TOTP碼是有效的,將會(huì)自動(dòng)跳轉(zhuǎn)到指定的首頁(yè)。
3 自己開(kāi)發(fā)二次認(rèn)證模塊
當(dāng)系統(tǒng)需求與現(xiàn)有的二次認(rèn)證方式不匹配時(shí),我們可以自己開(kāi)發(fā)二次認(rèn)證模塊?,F(xiàn)在,我們打算實(shí)現(xiàn)一個(gè)基于手機(jī)短信的二次認(rèn)證模塊,并將其集成到一個(gè)基于Spring Security的Java Web應(yīng)用程序中。
3.1 實(shí)現(xiàn)基于手機(jī)短信的二次認(rèn)證
我們的基于手機(jī)短信的二次認(rèn)證模塊,需要完成以下任務(wù):
- 接收用戶提交的手機(jī)號(hào)碼和用戶ID
- 對(duì)用戶的手機(jī)號(hào)碼進(jìn)行驗(yàn)證,并向該手機(jī)號(hào)碼發(fā)送短信驗(yàn)證碼
- 用戶提交短信驗(yàn)證碼,并進(jìn)行驗(yàn)證
我們使用Twilio API實(shí)現(xiàn)短信驗(yàn)證碼的發(fā)送和驗(yàn)證過(guò)程。Twilio是一個(gè)使用簡(jiǎn)單的云通訊平臺(tái),可以方便地發(fā)送短信和語(yǔ)音信息。我們可以使用Twilio提供的REST API來(lái)發(fā)送和驗(yàn)證短信驗(yàn)證碼,并在Java Web應(yīng)用程序中進(jìn)行封裝。
首先在網(wǎng)站上注冊(cè)賬號(hào)獲取API Key和API Secret,用于進(jìn)行短信驗(yàn)證碼的發(fā)送和驗(yàn)證。
其次在Java Web應(yīng)用程序中需要使用Twilio提供的Java庫(kù),引入以下代碼:
import com.twilio.Twilio; import com.twilio.rest.api.v2010.account.Message; import com.twilio.type.PhoneNumber; public class TwilioSMSVerifier { private static final String TWILIO_ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; private static final String TWILIO_AUTH_TOKEN = "your_auth_token"; private static final String TWILIO_PHONE_NUMBER = "+1415XXXXXXX"; public static void sendMessage(String toPhoneNumber, String message) { Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN); Message twilioMessage = Message.creator( new PhoneNumber(toPhoneNumber), new PhoneNumber(TWILIO_PHONE_NUMBER), message ).create(); } public static boolean verifyCode(String toPhoneNumber, String code) { // 進(jìn)行驗(yàn)證碼驗(yàn)證... return true; } }
這里我們引入了Twilio庫(kù),并定義了發(fā)送短信和驗(yàn)證驗(yàn)證碼的方法。請(qǐng)注意,這里的TWILIO_ACCOUNT_SID
和TWILIO_AUTH_TOKEN
需要替換為您自己的Twilio賬戶信息。TWILIO_PHONE_NUMBER
是您的Twilio賬戶綁定的電話號(hào)碼。
接下來(lái)實(shí)現(xiàn)sendMessage
方法,調(diào)用Twilio API發(fā)送短信驗(yàn)證碼:
public static void sendMessage(String toPhoneNumber, String message) { Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN); Message twilioMessage = Message.creator( new PhoneNumber(toPhoneNumber), new PhoneNumber(TWILIO_PHONE_NUMBER), message ).create(); System.out.println("Twilio message SID: " + twilioMessage.getSid()); }
這里使用Twilio提供的Message
對(duì)象進(jìn)行短信驗(yàn)證碼的發(fā)送。我們使用PhoneNumber
對(duì)象來(lái)表示電話號(hào)碼,在Message
構(gòu)造函數(shù)中指定了to
和from
電話號(hào)碼,以及需發(fā)送的消息內(nèi)容message
。發(fā)送后,我們通過(guò)System.out
輸出Twilio消息的SID。
接下來(lái)實(shí)現(xiàn)verifyCode
方法,驗(yàn)證用戶提交的短信驗(yàn)證碼:
public static boolean verifyCode(String toPhoneNumber, String code) { Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN); List<Message> messages = Message.reader() .setTo(new PhoneNumber(toPhoneNumber)) .read(); for (Message message : messages) { String body = message.getBody(); if (body.contains(code)) { return true; } } return false; }
這里使用Twilio提供的Message.reader
對(duì)象,查詢?cè)撌謾C(jī)號(hào)碼的短信信息。我們遍歷查詢結(jié)果,判斷消息正文是否包含提交的驗(yàn)證碼。如果存在,則返回true
,否則返回false
。
3.2 在Spring Security中配置自己開(kāi)發(fā)的二次認(rèn)證模塊
現(xiàn)在已經(jīng)完成了基于手機(jī)短信的二次認(rèn)證模塊的開(kāi)發(fā)。接下來(lái),我們將其集成到基于Spring Security的Java Web應(yīng)用程序中。為此,我們需要在Spring Security中添加一個(gè)新的認(rèn)證提供程序,并將我們的驗(yàn)證碼驗(yàn)證邏輯集成在其中。
我們可以通過(guò)繼承AbstractUserDetailsAuthenticationProvider
,實(shí)現(xiàn)自己的認(rèn)證方式。我們的認(rèn)證提供程序,需要完成以下任務(wù):
- 在進(jìn)行認(rèn)證時(shí),獲取用戶提交的手機(jī)號(hào)碼和用戶ID,并向該手機(jī)號(hào)碼發(fā)送驗(yàn)證碼
- 接收用戶提交的短信驗(yàn)證碼,并驗(yàn)證其正確性
下面是我們自己開(kāi)發(fā)的二次認(rèn)證提供程序的Java代碼:
import java.util.ArrayList; import java.util.List; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; public class SMSAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { private UserDetailsService userDetailsService; private int codeLength; // 驗(yàn)證碼長(zhǎng)度 private int codeExpiration; // 驗(yàn)證碼有效期 public SMSAuthenticationProvider(UserDetailsService userDetailsService, int codeLength, int codeExpiration) { this.userDetailsService = userDetailsService; this.codeLength = codeLength; this.codeExpiration = codeExpiration; } @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { String code = authentication.getCredentials().toString(); SMSUserDetails smsUser = (SMSUserDetails) userDetails; if (!TwilioSMSVerifier.verifyCode(smsUser.getMobile(), code)) { throw new BadCredentialsException("Invalid verification code"); } } @Override protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { String mobile = (String) authentication.getDetails(); UserDetails loadedUser; try { loadedUser = userDetailsService.loadUserByUsername(username); } catch (UsernameNotFoundException notFound) { throw new BadCredentialsException("Account not found"); } return new SMSUserDetails(loadedUser.getUsername(), loadedUser.getPassword(), mobile, loadedUser.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); } public class SMSUserDetails implements UserDetails { private final String username; private final String password; private final String mobile; private final List<GrantedAuthority> authorities; public SMSUserDetails(String username, String password, String mobile, List<GrantedAuthority> authorities) { this.username = username; this.password = password; this.mobile = mobile; this.authorities = new ArrayList<>(authorities); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } public String getMobile() { return mobile; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } } }
首先定義了一個(gè)描述用戶詳情的內(nèi)部類SMSUserDetails
,用于存儲(chǔ)用戶ID、密碼、手機(jī)號(hào)碼和權(quán)限等信息。在retrieveUser
方法中,我們首先獲取用戶提交的手機(jī)號(hào)碼,然后調(diào)用userDetailsService
獲取用戶信息。最后,我們返回SMSUserDetails
對(duì)象,封裝用戶詳情。需要注意的是,SMSUserDetails
必須實(shí)現(xiàn)UserDetails
接口,以保證在之后的認(rèn)證過(guò)程中能夠正確地獲取用戶信息。
在additionalAuthenticationChecks
中,我們使用TwilioSMSVerifier
對(duì)象驗(yàn)證用戶提交的驗(yàn)證碼。在這里,我們獲取authentication.getCredentials()
,即用戶提交的驗(yàn)證碼,進(jìn)行驗(yàn)證。如果認(rèn)證不通過(guò),我們則拋出一個(gè)BadCredentialsException
,表示認(rèn)證失敗。
最后在supports
方法中返回支持的認(rèn)證類型。這里,我們只支持UsernamePasswordAuthenticationToken
類型的認(rèn)證。如果使用其他類型的認(rèn)證對(duì)象,Spring Security將會(huì)拋出異常。
3.3 測(cè)試自己開(kāi)發(fā)的二次認(rèn)證模塊的集成
現(xiàn)在已經(jīng)完成了基于手機(jī)短信的二次認(rèn)證模塊的開(kāi)發(fā),并將其集成到了基于Spring Security的Java Web應(yīng)用程序中。接下來(lái),我們將會(huì)對(duì)這個(gè)模塊進(jìn)行測(cè)試,驗(yàn)證其是否可以正常工作。
首先需要使用Twilio API向我們自己的手機(jī)號(hào)碼發(fā)送一條測(cè)試短信。我們可以使用以下代碼:
TwilioSMSVerifier.sendMessage("+8612345678901", "Your verification code is: 123456");
在發(fā)送短信后可以使用以下代碼,在Spring Security中進(jìn)行認(rèn)證:
@RequestMapping(value = "/login", method = RequestMethod.POST) public String login(HttpServletRequest request) { String username = request.getParameter("username"); String password = request.getParameter("password"); String mobile = request.getParameter("mobile"); String code = request.getParameter("code"); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password, Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))); authRequest.setDetails(mobile); Authentication authentication = authManager.authenticate(authRequest); SecurityContextHolder.getContext().setAuthentication(authentication); return "redirect:/home"; }
這里首先獲取用戶提交的認(rèn)證信息,包括用戶名、密碼、手機(jī)號(hào)碼和短信驗(yàn)證碼。接下來(lái),我們創(chuàng)建一個(gè)UsernamePasswordAuthenticationToken
對(duì)象,并設(shè)置其類型為ROLE_USER
。我們將手機(jī)號(hào)碼設(shè)置為該對(duì)象的詳情信息。最后,我們使用authManager
進(jìn)行認(rèn)證,并將認(rèn)證結(jié)果保存在SecurityContextHolder
中。
現(xiàn)在已經(jīng)完成了自己開(kāi)發(fā)的二次認(rèn)證模塊的集成測(cè)試。需要注意的是需要使用真實(shí)手機(jī)號(hào)碼進(jìn)行測(cè)試,以確保短信驗(yàn)證碼可以正常發(fā)送和驗(yàn)證。如果您沒(méi)有真實(shí)手機(jī)號(hào)碼,可以注冊(cè)一個(gè)Twilio賬號(hào),并使用Twilio提供的測(cè)試號(hào)碼進(jìn)行測(cè)試。
四、小結(jié)回顧
在本篇文章中詳細(xì)介紹了二次認(rèn)證的概念以及其優(yōu)缺點(diǎn)。二次認(rèn)證可以通過(guò)多種方式實(shí)現(xiàn)包括基于硬件令牌、短信驗(yàn)證碼和移動(dòng)應(yīng)用等。不同的實(shí)現(xiàn)方式各有優(yōu)缺點(diǎn),需要按照實(shí)際情況進(jìn)行選擇。
其次使用Spring Security演示了如何在一個(gè)基于Web的應(yīng)用程序中添加二次認(rèn)證。通過(guò)使用Spring Security可以輕松地將一個(gè)二次認(rèn)證模塊與我們的應(yīng)用程序集成。在這個(gè)過(guò)程中了解如何配置Spring Security以及如何使用Spring Security提供的安全特性。
最后還討論了一些可能的問(wèn)題以及解決方案。例如,提到了可能會(huì)遇到與移動(dòng)設(shè)備兼容性和惡意攻擊等問(wèn)題。為了解決這些問(wèn)題提供了一些方法和建議,例如使用較新的移動(dòng)設(shè)備和實(shí)施防范措施等。
希望大家能夠了解二次認(rèn)證的主要概念以及如何使用Spring Security實(shí)現(xiàn)二次認(rèn)證。同時(shí)也希望大家能夠了解二次認(rèn)證可能遇到的挑戰(zhàn),并能夠選擇適當(dāng)?shù)慕鉀Q方案。
4.1 二次認(rèn)證的優(yōu)缺點(diǎn)
二次認(rèn)證可以提供額外的安全保障,增強(qiáng)用戶賬號(hào)的安全性。通過(guò)二次認(rèn)證,用戶需要在登錄時(shí)進(jìn)行額外的驗(yàn)證,使得其他人無(wú)法直接登錄到該賬號(hào)中。尤其是在一些關(guān)鍵應(yīng)用場(chǎng)景中,例如銀行賬戶和電子商務(wù)平臺(tái),二次認(rèn)證可以提供更加可靠的賬號(hào)保護(hù)。
雖然二次認(rèn)證可以提供更高的安全保障,但它也可能會(huì)影響系統(tǒng)的便捷性和易用性。尤其是在一些需要頻繁登錄的應(yīng)用中,例如互聯(lián)網(wǎng)社交應(yīng)用和游戲應(yīng)用,過(guò)多的驗(yàn)證步驟可能會(huì)導(dǎo)致用戶反感。此外二次認(rèn)證實(shí)現(xiàn)的復(fù)雜度也可能會(huì)造成不必要的負(fù)擔(dān)和成本。
因此在實(shí)踐中需要根據(jù)實(shí)際情況進(jìn)行選擇。我們可以在關(guān)鍵應(yīng)用場(chǎng)景中使用二次認(rèn)證,同時(shí)在其他應(yīng)用場(chǎng)景中采用其他的驗(yàn)證方式,以提供更好的用戶體驗(yàn)和系統(tǒng)安全性的平衡。
4.2 Spring Security添加二次認(rèn)證的效果
通過(guò)使用Spring Security可以輕松地將二次認(rèn)證模塊集成到我們的應(yīng)用程序中。可以使用Spring Security提供的二次認(rèn)證過(guò)濾器,并按照要求配置我們的認(rèn)證方式。在進(jìn)行認(rèn)證時(shí)可以使用Spring Security提供的許多安全特性,以保護(hù)我們的應(yīng)用程序不受惡意攻擊。
同時(shí)通過(guò)Spring Security還可以輕松地實(shí)現(xiàn)其他的安全特性,例如用戶角色控制、會(huì)話管理和密碼加密等。這樣,我們可以構(gòu)建一個(gè)更加健壯和安全的應(yīng)用程序,并幫助用戶獲得更好的體驗(yàn)和信任感。
4.3 可能的問(wèn)題及解決方案
在實(shí)踐中能會(huì)遇到一些問(wèn)題如移動(dòng)設(shè)備兼容性、短信驗(yàn)證碼實(shí)現(xiàn)和惡意攻擊等。為了解決這些問(wèn)題,我們可以采取一些措施例如:
選擇較新版本設(shè)備。隨著移動(dòng)設(shè)備技術(shù)的不斷更新,較新的設(shè)備可能會(huì)更加支持多種驗(yàn)證方式,例如指紋識(shí)別和面部識(shí)別等。
使用多個(gè)驗(yàn)證碼驗(yàn)證方式。我們可以使用多種驗(yàn)證碼驗(yàn)證方式,例如郵件驗(yàn)證碼、電話驗(yàn)證碼和圖形驗(yàn)證碼等,以提高安全性。
實(shí)施防范措施。我們可以通過(guò)使用Web防火墻和入侵檢測(cè)系統(tǒng)等安全技術(shù),來(lái)防范惡意攻擊和入侵行為。
總之通過(guò)有效地管理和保護(hù)我們的應(yīng)用程序可以提供更高質(zhì)量的用戶體驗(yàn)和安全性。通過(guò)Spring Security,我們可以輕松地添加二次認(rèn)證模塊,并讓我們的應(yīng)用程序更加可靠和安全。
到此這篇關(guān)于Spring Security添加二次認(rèn)證的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)Spring Security二次認(rèn)證內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity?默認(rèn)登錄認(rèn)證的實(shí)現(xiàn)原理解析
- SpringSecurity實(shí)現(xiàn)權(quán)限認(rèn)證與授權(quán)的使用示例
- Spring Security內(nèi)存中認(rèn)證的實(shí)現(xiàn)
- SpringBoot整合SpringSecurity認(rèn)證與授權(quán)
- SpringSecurity+jwt+redis基于數(shù)據(jù)庫(kù)登錄認(rèn)證的實(shí)現(xiàn)
- SpringSecurity身份認(rèn)證原理解析
- springsecurity第三方授權(quán)認(rèn)證的項(xiàng)目實(shí)踐
- Spring Security實(shí)現(xiàn)身份認(rèn)證和授權(quán)的示例代碼
- SpringSecurity實(shí)現(xiàn)前后端分離登錄token認(rèn)證詳解
- Spring Security認(rèn)證機(jī)制源碼層探究
- SpringBoot security安全認(rèn)證登錄的實(shí)現(xiàn)方法
相關(guān)文章
在SpringBoot中實(shí)現(xiàn)一個(gè)訂單號(hào)生成系統(tǒng)的示例代碼
在Spring Boot中設(shè)計(jì)一個(gè)訂單號(hào)生成系統(tǒng),主要考慮到生成的訂單號(hào)需要滿足的幾個(gè)要求:唯一性、可擴(kuò)展性、以及可能的業(yè)務(wù)相關(guān)性,本文給大家介紹了幾種常見(jiàn)的解決方案及相應(yīng)的示例代碼,需要的朋友可以參考下2024-02-02Kotlin中?StateFlow?或?SharedFlow?的區(qū)別解析
Kotlin協(xié)程中的StateFlow和SharedFlow是響應(yīng)式數(shù)據(jù)流,分別用于UI狀態(tài)管理和事件通知,StateFlow有初始值,只保留最新值,適用于UI狀態(tài)管理;SharedFlow沒(méi)有初始值,可以配置緩存大小,適用于事件通知,感興趣的朋友一起看看吧2025-03-03SpringSecurity微服務(wù)實(shí)戰(zhàn)之公共模塊詳解
這篇文章主要為大家介紹了SpringSecurity微服務(wù)實(shí)戰(zhàn)之公共模塊詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08詳解如何用spring Restdocs創(chuàng)建API文檔
這篇文章將帶你了解如何用spring官方推薦的restdoc去生成api文檔。具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05Java通過(guò)Fork/Join優(yōu)化并行計(jì)算
這篇文章主要為大家詳細(xì)介紹了Java通過(guò)Fork、Join來(lái)優(yōu)化并行計(jì)算,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04Java實(shí)現(xiàn)圖書(shū)借閱系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)圖書(shū)借閱系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03