Spring Security 实现手机验证码登录
思路:参考用户名密码登录过滤器链,重写认证和授权
示例如下(该篇示例以精简为主,演示主要实现功能,全面完整版会在以后的博文中发出):
由于涉及内容较多,建议先复制到本地工程中,然后在细细研究。
1. 新建Maven项目 sms-code-validate
2. pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.java</groupId> <artifactId>sms-code-validate</artifactId> <version>1.0.0</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> </parent> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>2.0.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.11</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <!-- 热部署 --> <dependency> <groupId>org.springframework</groupId> <artifactId>springloaded</artifactId> <version>1.2.8.RELEASE</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
3. 启动类 SmsCodeStarter.java
package com.java; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * <blockquote><pre> * * 主启动类 * * </pre></blockquote> * * @author Logan * */ @SpringBootApplication public class SmsCodeStarter { public static void main(String[] args) { SpringApplication.run(SmsCodeStarter.class, args); } }
4. ValidateCode.java
package com.java.validate.code; import java.time.LocalDateTime; /** * 验证码封装类 * * @author Logan * */ public class ValidateCode { /** * 验证码 */ private String code; /** * 过期时间 */ private LocalDateTime expireTime; /** * 指定验证码和有效分钟数的构造方法 * * @param code 验证码 * @param validityMinutes 有效分钟数 */ public ValidateCode(String code, int validityMinutes) { this.code = code; this.expireTime = LocalDateTime.now().plusMinutes(validityMinutes); } /** * 指定验证码和过期时间的构造方法 * * @param code 验证码 * @param expireTime 过期时间 */ public ValidateCode(String code, LocalDateTime expireTime) { this.code = code; this.expireTime = expireTime; } public String getCode() { return code; } public LocalDateTime getExpireTime() { return expireTime; } }
5. CodeGenerator.java
package com.java.validate.generator; import org.apache.commons.lang3.RandomStringUtils; import com.java.validate.code.ValidateCode; /** * 验证码生成器 * * @author Logan * */ public class CodeGenerator { /** * 验证码生成方法 * * @param length 验证码长度 * @param validityMinutes 过期分钟数 * @return */ public static ValidateCode generate(int length, int validityMinutes) { String code = RandomStringUtils.randomNumeric(length); return new ValidateCode(code, validityMinutes); } }
6. ValidateCodeSender.java
package com.java.validate.sender; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; /** * 验证码发送器 * * @author Logan * */ @Component public class ValidateCodeSender { /** * 模拟发送手机验证码,此处发回浏览器,实际情况根据短信服务商做调整 * * @param response HTTP响应对象 * @param mobile 手机号 * @param code 验证码 */ public void sendSmsCode(HttpServletResponse response, String mobile, String code) { System.out.println(String.format("模拟向手机号【%s】发送验证码【%s】", mobile, code)); write(response, "验证码为:" + code); } /** * 发送HTTP响应信息 * * @param response HTTP响应对象 * @param message 信息内容 */ private void write(HttpServletResponse response, String message) { response.setContentType("text/html; charset=UTF-8"); try ( PrintWriter writer = response.getWriter(); ) { writer.write(message); writer.flush(); } catch (IOException e) { e.printStackTrace(); } } }
7. ValidateCodeFilter.java
package com.java.validate.filter; import java.io.IOException; import java.io.PrintWriter; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.ServletRequestUtils; import org.springframework.web.filter.OncePerRequestFilter; import com.java.controller.ValidateCodeController; import com.java.validate.code.ValidateCode; /** * 校验验证码过滤器 * * @author Logan * @createDate 2019-02-07 * */ @Component public class ValidateCodeFilter extends OncePerRequestFilter { /** * 需要校验短信验证码的请求 */ private List<String> smsCodeUrls = new ArrayList<>(); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { /** * 如果需要校验短信验证码的请求集合中,包含当前请求,则进行短信验证码校验 */ if (smsCodeUrls.contains(request.getRequestURI())) { if (smsCodeValid(request, response)) { // 校验通过,继续向后执行 filterChain.doFilter(request, response); } } // 其它请求,直接放过 else { filterChain.doFilter(request, response); } } @Override protected void initFilterBean() throws ServletException { // 初始化添加需要校验的请求到集合中,可由配置文件中配置,此处为了简洁,直接添加 smsCodeUrls.add("/login/mobile"); } /** * 短信验证码是否有效 * * @param request HTTP请求对象 * @param response HTTP响应对象 * @return 有效,返回true;无效,返回false * @throws ServletRequestBindingException */ private boolean smsCodeValid(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException { String smsCode = ServletRequestUtils.getStringParameter(request, "smsCode"); ValidateCode validateCode = (ValidateCode) request.getSession().getAttribute(ValidateCodeController.SESSION_CODE_KEY); if (StringUtils.isBlank(smsCode)) { write(response, "验证码不能为空!"); return false; } else if (null == validateCode) { write(response, "验证码不存在!"); return false; } else if (LocalDateTime.now().isAfter(validateCode.getExpireTime())) { write(response, "验证码已过期!"); return false; } else if (!StringUtils.equals(smsCode, validateCode.getCode())) { write(response, "验证码不正确!"); return false; } // 验证成功,移除Session中验证码 request.getSession().removeAttribute(ValidateCodeController.SESSION_CODE_KEY); return true; } /** * 发送HTTP响应信息 * * @param response HTTP响应对象 * @param message 信息内容 */ private void write(HttpServletResponse response, String message) { response.setContentType("text/html; charset=UTF-8"); try ( PrintWriter writer = response.getWriter(); ) { writer.write(message); writer.flush(); } catch (IOException e) { e.printStackTrace(); } } }
8. 自定义UserDetailsService实现类,具体逻辑根据实际情况调整。
SecurityUserDetailsService.java
package com.java.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; /** * UserDetailsService实现类 * * @author Logan * */ @Component public class SecurityUserDetailsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 数据库存储密码为加密后的密文(明文为123456) String password = passwordEncoder.encode("123456"); System.out.println("username: " + username); System.out.println("password: " + password); // 模拟查询数据库,获取属于Admin和Normal角色的用户 User user = new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("Admin,Normal")); return user; } }
9. 获取主机信息接口,模拟演示功能需要
HostController.java
package com.java.controller; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.HashMap; import java.util.Map; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HostController { @GetMapping("/getHostMessage") public Map<String, Object> getHostMessage() { Map<String, Object> map = new HashMap<>(); try { InetAddress serverHost = InetAddress.getLocalHost(); map.put("hostname", serverHost.getHostName()); map.put("hostAddress", serverHost.getHostAddress()); } catch (UnknownHostException e) { e.printStackTrace(); map.put("msg", e.getMessage()); } return map; } }
10. 验证码生成接口,可扩展集成图片验证码,后续博文中会发出。
ValidateCodeController.java
package com.java.controller; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.java.validate.code.ValidateCode; import com.java.validate.generator.CodeGenerator; import com.java.validate.sender.ValidateCodeSender; /** * 创建验证码接口 * * @author Logan * */ @RestController @RequestMapping("/code") public class ValidateCodeController { /** * 验证码存放Session中的key */ public static final String SESSION_CODE_KEY = "code"; /** * 验证码长度,可以提取到配置中,此处只做演示,简单处理 */ private int length = 6; /** * 过期分钟数,可以提取到配置中,此处只做演示,简单处理 */ private int validityMinutes = 30; /** * 验证码发送器 */ @Autowired private ValidateCodeSender validateCodeSender; /** * 创建短信验证码接口 * * @param request 请求对象 * @param response 响应对象 * @param mobile 手机号 */ @GetMapping("/sms") public void createSmsCode(HttpServletRequest request, HttpServletResponse response, String mobile) { ValidateCode validateCode = CodeGenerator.generate(length, validityMinutes); // 存储验证码到Session中,登录时验证 request.getSession().setAttribute(SESSION_CODE_KEY, validateCode); // 调用验证码发送器发送短信验证码 validateCodeSender.sendSmsCode(response, mobile, validateCode.getCode()); } }
11. AuthenticationSuccessHandler.java
package com.java.authentication.handler; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.stereotype.Component; /** * 授权成功处理器 * * @author Logan * @createDate 2019-02-07 * */ @Component public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { // 设置默认跳转页面,当没有重定向页面时(例如:直接访问登录页面),此配置生效 super.setDefaultTargetUrl("/main.html"); super.onAuthenticationSuccess(request, response, authentication); } }
12. SmsCodeAuthenticationToken.java
package com.java.authentication.mobile; import java.util.Collection; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityCoreVersion; /** * <pre> * * 短信验证码Token,封装短信验证码登录信息。 * * 参照{@link org.springframework.security.authentication.UsernamePasswordAuthenticationToken} * * </pre> * * @author Logan * */ public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; // ~ Instance fields // ================================================================================================ private final Object principal; // ~ Constructors // =================================================================================================== /** * This constructor can be safely used by any code that wishes to create a * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()} * will return <code>false</code>. * */ public SmsCodeAuthenticationToken(String mobile) { super(null); this.principal = mobile; super.setAuthenticated(false); } /** * This constructor should only be used by <code>AuthenticationManager</code> or * <code>AuthenticationProvider</code> implementations that are satisfied with * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>) * authentication token. * * @param mobile * @param authorities */ public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; super.setAuthenticated(true); // must use super, as we override } // ~ Methods // ======================================================================================================== public Object getCredentials() { return null; } public Object getPrincipal() { return this.principal; } }
13. SmsCodeAuthenticationFilter.java
package com.java.authentication.mobile; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.Assert; /** * <pre> * * 短信验证码过滤器, * * 参照{@link org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter} * * </pre> * * @author Logan * */ public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // ~ Static fields/initializers // ===================================================================================== public static final String MOBILE_KEY = "mobile"; private String mobileParameter = MOBILE_KEY; private boolean postOnly = true; // ~ Constructors // =================================================================================================== public SmsCodeAuthenticationFilter() { super(new AntPathRequestMatcher("/login/mobile", "POST")); } // ~ Methods // ======================================================================================================== public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String mobile = StringUtils.trimToEmpty(request.getParameter(mobileParameter)); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } /** * Provided so that subclasses may configure what is put into the * authentication request's details property. * * @param request that an authentication request is being created for * @param authRequest the authentication request object that should have its * details set */ protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } /** * Sets the parameter name which will be used to obtain the mobile from the * login request. * * @param mobileParameter the parameter name. Defaults to "mobile". */ public void setMobileParameter(String mobileParameter) { Assert.hasText(mobileParameter, "mobile parameter must not be empty or null"); this.mobileParameter = mobileParameter; } /** * Defines whether only HTTP POST requests will be allowed by this filter. * If set to true, and an authentication request is received which is not a * POST request, an exception will be raised immediately and authentication * will not be attempted. The <tt>unsuccessfulAuthentication()</tt> method * will be called as if handling a failed authentication. * <p> * Defaults to <tt>true</tt> but may be overridden by subclasses. */ public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getMobileParameter() { return mobileParameter; } }
14. SmsCodeAuthenticationProvider.java
package com.java.authentication.mobile; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; /** * 短信验证码授权认证类 * * @author Logan * @createDate 2019-02-07 * */ public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 未认证Token SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication; UserDetails user = userDetailsService.loadUserByUsername((String) token.getPrincipal()); if (null == user) { throw new InternalAuthenticationServiceException("未绑定用户!"); } // 已认证的Token SmsCodeAuthenticationToken authenticationToken = new SmsCodeAuthenticationToken(user, user.getAuthorities()); // 复制之前的请求信息到认证后的Token中 authenticationToken.setDetails(token.getDetails()); return authenticationToken; } @Override public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } }
15. ApplicationContextConfig.java
package com.java.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * 配置文件类 * * @author Logan * */ @Configuration public class ApplicationContextConfig { /** * <blockquote><pre> * * 配置密码编码器,Spring Security 5.X必须配置,否则登录时报空指针异常 * * </pre></blockquote> * * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
16. RepositoryConfig.java
package com.java.config; import javax.sql.DataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; /** * 数据库相关配置 * * @author Logan * */ @Configuration public class RepositoryConfig { @Bean public PersistentTokenRepository tokenRepository(DataSource dataSource) { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); // tokenRepository.setCreateTableOnStartup(true); // 第一次启动时可使用此功能自动创建表,第二次要关闭,否则表已存在会启动报错 return tokenRepository; } }
17. SmsCodeSecurityConfig.java
package com.java.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.stereotype.Component; import com.java.authentication.handler.AuthenticationSuccessHandler; import com.java.authentication.mobile.SmsCodeAuthenticationFilter; import com.java.authentication.mobile.SmsCodeAuthenticationProvider; /** * 短信验证码安全配置,串联自定义短信验证码验证流程 * * @author Logan * @createDate 2019-02-07 * */ @Component public class SmsCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private UserDetailsService userDetailsService; @Autowired private AuthenticationSuccessHandler successHandler; @Override public void configure(HttpSecurity http) throws Exception { // 此处采用new的方式,而不是@Component和@Autowired结合,目的为了方便安装和卸载,可重用可移植性强 SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); // 设置AuthenticationManager smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(successHandler); // 短信验证码认证Provider类 SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService); // 设置短信验证码认证Provider类到AuthenticationManager管理集合中 http.authenticationProvider(smsCodeAuthenticationProvider) // 设置短信验证码在用户名密码验证过滤器之后验证 .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
18. LoginConfig.java
package com.java.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import com.java.validate.filter.ValidateCodeFilter; /** * 登录相关配置 * * @author Logan * */ @Configuration public class LoginConfig extends WebSecurityConfigurerAdapter { @Autowired private PersistentTokenRepository tokenRepository; @Autowired private UserDetailsService userDetailsService; @Autowired private ValidateCodeFilter validateCodeFilter; @Autowired private SmsCodeSecurityConfig smsCodeSecurityConfig; @Override protected void configure(HttpSecurity http) throws Exception { http.apply(smsCodeSecurityConfig) // 设置验证码过滤器到过滤器链中,在UsernamePasswordAuthenticationFilter之前执行 .and().addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 设置自定义表单登录页面 .formLogin().loginPage("/login.html") // 设置登录验证请求地址为自定义登录页配置action ("/login/form") .loginProcessingUrl("/login/form") // 设置默认登录成功跳转页面 .defaultSuccessUrl("/main.html") /* 授权请求设置 */ .and().authorizeRequests() // 设置不需要授权的请求 .antMatchers("/js/*", "/code/*", "/login.html").permitAll() // 其它任何请求都需要验证权限 .anyRequest().authenticated() /* 记住我功能设置 */ .and().rememberMe().tokenRepository(tokenRepository) // 【记住我功能】有效期为两周 .tokenValiditySeconds(3600 * 24 * 14) // 设置UserDetailsService .userDetailsService(userDetailsService) // 暂时停用csrf,否则会影响验证 .and().csrf().disable(); } }
19. src/main/resources 下配置文件如下
20. application.properties
spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://192.168.32.10:3306/security?useUnicode=true&characterEncoding=UTF-8 spring.datasource.username=root spring.datasource.password=123456 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
21. login.html
<!DOCTYPE html> <html> <head> <title>登录</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <script type="text/javascript" src="js/jquery-3.3.1.min.js"></script> <script> function sendSmsCode() { var mobile = $("#mobile").val().trim(); // 简单校验由11位数字组成 var reg = /^\d{11}$/; if(reg.test(mobile)) { $.ajax({ type: "get", url: "/code/sms?mobile=" + mobile, async: true, success: function(data) { alert(data); } }); } else { alert("手机号输入格式错误"); } } </script> </head> <body> <!--登录框--> <div align="center"> <h2>用户自定义登录页面</h2> <fieldset style="width: 390px;"> <legend>表单登录框</legend> <form action="/login/form" method="post"> <table> <tr> <th>用户名:</th> <td><input name="username" /> </td> </tr> <tr> <th>密码:</th> <td><input type="password" name="password" /> </td> </tr> <tr> <th>记住我:</th> <td><input type="checkbox" name="remember-me" value="true" checked="checked" /></td> </tr> <tr> <th></th> <td></td> </tr> <tr> <td colspan="2" align="center"><button type="submit">登录</button></td> </tr> </table> </form> </fieldset> <fieldset style="width: 390px;margin-top: 30px;"> <legend>手机验证码登录框</legend> <form action="/login/mobile" method="post"> <table> <tr> <th>手机号:</th> <td><input id="mobile" name="mobile" value="13166668888" /></td> </tr> <tr> <th>验证码:</th> <td> <input id="smsCode" name="smsCode" /> <button type="button" onclick="sendSmsCode()">发送手机验证码</button> </td> </tr> <tr> <td colspan="2" align="center"><button type="submit">登录</button></td> </tr> </table> </form> </fieldset> </div> </body> </html>
22. main.html
<!DOCTYPE html> <html> <head> <title>首页</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <script type="text/javascript" src="js/jquery-3.3.1.min.js"></script> <script> function getHostMessage() { $.ajax({ type: "get", url: "/getHostMessage", async: true, success: function(data) { $("#msg").val(JSON.stringify(data)); } }); } </script> </head> <body> <div> <h2>首页</h2> <table> <tr> <td><button onclick="getHostMessage()">获取主机信息</button></td> </tr> </table> </div> <!--响应内容--> <div> <textarea id="msg" style="width: 800px;height: 800px;"></textarea> </div> </body> </html>
23. js/jquery-3.3.1.min.js 可在官网下载
https://code.jquery.com/jquery-3.3.1.min.js
24. 创建数据库
DROP DATABASE IF EXISTS security; CREATE DATABASE security; USE security; create table persistent_logins ( username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null );
25. 运行 SmsCodeStarter.java , 启动测试
浏览器输入首页 http://localhost:8080/main.html
地址栏自动跳转到登录页面,如下:
表单登录可自行研究,只讲解手机验证码登录过程
单击【发送手机验证码】按钮,控制台和浏览器都会显示生成验证码。
输入正确的手机验证码,单击【登录】按钮。跳转到首页,如下所示:
获取主机信息接口功能调用正常。
验证码输入错误情况可自习研究。
搭建完成!
.