1、手机号登录是不需要密码的,通过短信验证码实现免密登录功能。
a.向手机发送手机验证码,使用第三方短信平台 SDK 发送,如: 阿里云短信服务
b.登录表单输入短信验证码
c.使用自定义短信验证码校验过滤器SmsVerifyCodeValidateFilter
d.当验证码校验通过后,进入自定义手机认证过滤器 MobileAuthenticationFilter 校验手机号是否存在
e.自定义 MobileAuthenticationToken 提供给 MobileAuthenticationFilter
f.自定义 MobileAuthenticationProvider 提供给 ProviderManager 处理
g.创建针对手机号查询用户信息的 MobileUserDetailsService ,交给 MobileAuthenticationProvider
h.自定义 MobileAuthenticationConfig 配置类将上面组件连接起来,添加到容器中
i.将 MobileAuthenticationConfig 添加到 SpringSecurityConfig 安全配置的过滤器链上。
2、跳转到短信登录认证页面,在 SpringSecurityConfig 放行手机登录与短信发送相关请求URL
/**
* 前往手机验证码登录页
*
* @return
*/
@RequestMapping("/mobile/page")
public String toMobilePage() {
return "login-mobile"; // templates/login-mobile.html
}
/**
* 资源权限配置(过滤器链):
* 1、被拦截的资源
* 2、资源所对应的角色权限
* 3、定义认证方式:httpBasic 、httpForm
* 4、定制登录页面、登录请求地址、错误处理方式
* 5、自定义 spring security 过滤器
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.httpBasic()//采用httpBasic 认证方式
/*http.formLogin()
.loginPage("/login/page")// 交给 /login/page 响应认证(登录)页面
.loginProcessingUrl("/login/form") // 登录表单提交处理Url, 默认是 /login
.usernameParameter("name") // 默认用户名的属性名是 username
.passwordParameter("pwd") // 默认密码的属性名是 password
.and()
.authorizeRequests()//认证请求
.antMatchers("/login/page").permitAll()//自定义登录页不需要认证
.anyRequest().authenticated();// 所有进入应用的HTTP请求都要进行认证*/
http
.addFilterBefore(imageVerifyCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)//将校验过滤器 imageCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面
.addFilterBefore(smsVerifyCodeValidateFilter,UsernamePasswordAuthenticationFilter.class)//将校验过滤器 smsVerifyCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面
.formLogin()
.loginPage(securityProperties.getLoginPage())// 交给 /login/page 响应认证(登录)页面
.loginProcessingUrl(securityProperties.getLoginProcessingUrl()) // 登录表单提交处理Url, 默认是 /login
.usernameParameter(securityProperties.getUsernameParameter()) // 默认用户名的属性名是 username
.passwordParameter(securityProperties.getPasswordParameter()) // 默认密码的属性名是 password
.successHandler(customAuthenticationSuccessHandler)//自定义认证成功处理器
.failureHandler(customAuthenticationFailureHandler)//自定义认证失败处理器
.and()
.authorizeRequests()//认证请求
.antMatchers(securityProperties.getLoginPage(),securityProperties.getMobilePage(),securityProperties.getImageCodeUrl(),securityProperties.getMobileCodeUrl()).permitAll()//自定义登录页不需要认证,生成图片验证码,发送短信获取验证码也不需要验证
.anyRequest().authenticated()// 所有进入应用的HTTP请求都要进行认证
.and()
.rememberMe()//记住我功能
.tokenRepository(jdbcTokenRepository())//保存登录信息
.tokenValiditySeconds(securityProperties.getTokenValiditySeconds());//记住我有效时长一周
// 将手机相关的配置绑定过滤器链上
http.apply(mobileAuthenticationConfig);
}
3、创建阿里云短信发送服务接口
上述自己去阿里云短信服务模块去申请
@Component
@ConfigurationProperties(prefix = "moblie.sms")
@Data
public class SmsProperties {
private String accessKeyId;
private String accessKeySecret;
private String signName;
private String templateCode;
private String product;
private String domain;
}
@Controller
public class SmsSendController {
@Autowired
SmsSendService smsSendService;
@RequestMapping(value = "/code/mobile",method = RequestMethod.GET)
@ResponseBody
public Result smsCodeSend(@RequestParam("mobile") String phoneNumbers, HttpServletRequest request){
return smsSendService.sendSms(phoneNumbers,request);
}
}
/**
* 短信发送接口
*/
public interface SmsSendService {
/**
* 短信发送
* @param PhoneNumbers 手机号
* @return
*/
Result sendSms(String PhoneNumbers, HttpServletRequest request);
}
/**
* 发送短信验证码
*/
@Service
public class SmsSendServiceImpl implements SmsSendService {
Logger logger= LoggerFactory.getLogger(SmsSendServiceImpl.class);
public static final String SESSION_SMS_VERIFY_CODE = "SESSION_SMS_VERIFY_CODE";
@Autowired
SmsProperties smsProperties;
@Override
public Result sendSms(String PhoneNumbers, HttpServletRequest request) {
//1.生成一个手机短信验证码
String code = RandomUtil.randomNumbers(4);
//2.将验证码发到session中
HttpSession session = request.getSession();
session.setAttribute(SESSION_SMS_VERIFY_CODE,code);
//3.发送短信
SendSmsResponse response = toSendSms(PhoneNumbers, code);
logger.info("向手机号" + PhoneNumbers + "发送的验证码为::" + code);
if (response.getCode().equals("ok")){
return Result.ok("获取验证码成功");
}else {
return Result.build(500,"获取验证码失败");
}
}
public SendSmsResponse toSendSms(String PhoneNumbers, String code) {
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
DefaultProfile.addEndpoint( "cn-hangzhou", smsProperties.getProduct(), smsProperties.getDomain());
IAcsClient acsClient = new DefaultAcsClient(profile);
//组装请求对象-具体描述见控制台-文档部分内容
SendSmsRequest request = new SendSmsRequest();
//必填:待发送手机号
request.setPhoneNumbers(PhoneNumbers);
//必填:短信签名-可在短信控制台中找到
request.setSignName(smsProperties.getSignName());
//必填:短信模板-可在短信控制台中找到
request.setTemplateCode(smsProperties.getTemplateCode());
//可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
request.setTemplateParam("{\"code\":\"" + code + "\"}");
//选填-上行短信扩展码(无特殊需求用户请忽略此字段)
//request.setSmsUpExtendCode("90997");
//可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
request.setOutId("yourOutId");
//hint 此处可能会抛出异常,注意catch
SendSmsResponse sendSmsResponse = null;
try {
sendSmsResponse = acsClient.getAcsResponse(request);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("发送短信失败");
}
return sendSmsResponse;
}
}
4、实现短信验证码校验过滤器 SmsVerifyCodeValidateFilter,校验输入的验证码与发送的短信验证是否一致。
/**
*
* 短信验证码校验过滤器
* OncePerRequestFilter: 所有请求之前被调用一次
*/
@Component("smsVerifyCodeValidateFilter")
public class SmsVerifyCodeValidateFilter extends OncePerRequestFilter {
@Autowired
SecurityProperties securityProperties;
@Autowired
CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//1.判断手机登录认证 且请求方式为post
if (securityProperties.getMobileProcessingUrl().equalsIgnoreCase(request.getRequestURI())&&request.getMethod().equalsIgnoreCase("post")){
try {
//校验验证码合法性
validate(request);
} catch (AuthenticationException e) {
//将验证失败的抛出的异常交给自定义认证失败处理器处理异常
customAuthenticationFailureHandler.onAuthenticationFailure(request,response,e);
return;
}
}
filterChain.doFilter(request,response);
}
private void validate(HttpServletRequest request) {
//先获取session中的验证码
String sessionImageCode = (String) request.getSession().getAttribute(SmsSendServiceImpl.SESSION_SMS_VERIFY_CODE);
//获取用户输入的验证码
String inputCode = request.getParameter("code");
if (StringUtils.isBlank(inputCode)){
throw new SmsVerifyCodeException("验证码不能为空");
}
if (!inputCode.equalsIgnoreCase(sessionImageCode)){
throw new SmsVerifyCodeException("验证码输入错误");
}
}
}
5、实现手机认证登录过滤器MobileAuthenticationFilter,模仿UsernamePasswordAuthenticationFilter进行改造
**
* 手机登录过滤器
* 实现同UsernamePasswordAuthenticationFilter
* 将username相关的都改成mobile,而且手机登录只有手机号,没有密码,所以去掉密码
* 相应的参数最好写成可配置的
*
*/
public class MobileAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
@Resource
SecurityProperties securityProperties;
/**
* 前端表单中的手机号码参数
*/
private String mobileParameter = "mobile";
private boolean postOnly = true;
// ~ Constructors
// ===================================================================================================
public MobileAuthenticationFilter() {
super(new AntPathRequestMatcher("/mobile/form", "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 = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
MobileAuthenticationToken authRequest = new MobileAuthenticationToken(mobile);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* Enables subclasses to override the composition of the username, such as by
* including additional values and a separator.
*
* @param request so that request attributes can be retrieved
* @return the username that will be presented in the <code>Authentication</code>
* request token to the <code>AuthenticationManager</code>
*/
@Nullable
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
/**
* 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,
MobileAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
/**
* 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 String getMobileParameter() {
return mobileParameter;
}
public void setMobileParameter(String mobileParameter) {
this.mobileParameter = mobileParameter;
}
}
6、封装手机认证Token MobileAuthenticationToken提供给上面自定义的 MobileAuthenticationFilter 使用。模仿UsernamePasswordAuthenticationToken进行改造
/**
* 封装手机认证Token
* 实现同UsernamePasswordAuthenticationToken
*/
public class MobileAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
//认证之前存放机号,认证之后放用户信息
private final Object principal;
/**
* 开始认证时,创建一个MobileAuthenticationToken实例 接收的是手机号码, 并且 标识未认证
*
* @param principal 手机号
*/
public MobileAuthenticationToken(Object principal) {
super(null);
this.principal = principal; // 手机号
setAuthenticated(false);
}
/**
* 当认证通过后,会重新创建一个新的MobileAuthenticationToken,来标识它已经认证通过,
*
* @param principal 用户信息
* @param authorities 用户权限
*/
public MobileAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;// 用户信息
super.setAuthenticated(true); // 标识已经认证通过
}
@Override
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
7、实现手机认证提供者 MobileAuthenticationProvider,提供给底层 ProviderManager 使用。
/**
* 实现手机认证提供者 MobileAuthenticationProvider提供给底层 ProviderManager 使用
*/
public class MobileAuthenticationProvider implements AuthenticationProvider {
UserDetailsService userDetailsService;
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
/**
* 认证处理:
* 1. 通过 手机号 去数据库查询用户信息(UserDeatilsService)
* 2. 再重新构建认证信息
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MobileAuthenticationToken mobileAuthenticationToken = (MobileAuthenticationToken) authentication;
// 获取用户输入的手机号
String mobile = (String) mobileAuthenticationToken.getPrincipal();
// 查询数据库
UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
//未查询到用户信息
if (userDetails == null) {
throw new AuthenticationServiceException("该手机未注册");
}
// 查询到了用户信息, 则认证通过,就重新构建 MobileAuthenticationToken 实例
MobileAuthenticationToken authenticationToken = new MobileAuthenticationToken(userDetails, userDetails.getAuthorities());
authenticationToken.setDetails(mobileAuthenticationToken.getDetails());
return authenticationToken;
}
/**
* 通过此方法,来判断 采用哪一个 AuthenticationProvider
*
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return MobileAuthenticationToken.class.isAssignableFrom(authentication);
}
}
8、手机号获取用户信息 MobileUserDetailsService
/**
* 通过手机号获取用户信息和权限信息
*/
@Component("mobileUserDetailsService")
public class MobileUserDetailsService implements UserDetailsService {
Logger logger = LoggerFactory.getLogger(MobileUserDetailsService.class);
@Override
public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
logger.info("请求的手机号是:" + mobile);
// 1. 通过手机号查询用户信息(查询数据库)
// 2. 如果有此用户,则查询用户权限
// 3. 封装用户信息
return new User(mobile, "", true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
}
}
9、自定义管理认证配置 MobileAuthenticationConfig,将上面定义的组件绑定起来,添加到容器中
/**
* 自定义管理认证配置
* 将定义的手机短信认证相关的组件组合起来,一起添加到容器中
*/
@Component("mobileAuthenticationConfig")
public class MobileAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
MobileUserDetailsService mobileUserDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
// 创建校验手机号过滤器实例
MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
// 接收 AuthenticationManager 认证管理器
mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 采用哪个成功、失败处理器
mobileAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
mobileAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
//手机登录记住我功能
mobileAuthenticationFilter.setRememberMeServices(http.getSharedObject(RememberMeServices.class));
// 为 Provider 指定明确 的mobileUserDetailsService 来查询用户信息
MobileAuthenticationProvider provider = new MobileAuthenticationProvider();
provider.setUserDetailsService(mobileUserDetailsService);
// 将 provider 绑定到 HttpSecurity 上面,
// 并且将 手机认证加到 用户名密码认证之后
http.authenticationProvider(provider).addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
10、绑定到安全配置 SpringSecurityConfig
向 SpringSecurityConfig 中注入SmsVerifyCodeValidateFilter和MobileAuthenticationConfig实例
将 SmsVerifyCodeValidateFilter实例添加到UsernamePasswordAuthenticationFilter前面
http
.addFilterBefore(imageVerifyCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)//将校验过滤器 imageCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面
.addFilterBefore(smsVerifyCodeValidateFilter,UsernamePasswordAuthenticationFilter.class)//将校验过滤器 smsVerifyCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面
在 SpringSecurityConfifig#confifigure(HttpSecurity http) 方法体最后调用 apply 添加mobileAuthenticationConfig(将手机相关的配置绑定过滤器链上)
// 将手机相关的配置绑定过滤器链上
http.apply(mobileAuthenticationConfig);
11、SpringSecurityConfig安全配置类完整代码
/**
* 安全配置类作为安全控制中心, 用于实现身份认证与授权配置功能
*/
@Configuration
@EnableWebSecurity //启动 SpringSecurity 过滤器链功能
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
SecurityProperties securityProperties;
Logger logger = LoggerFactory.getLogger(SpringSecurityConfig.class);
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
// 加密存储 明文+随机盐值
return new BCryptPasswordEncoder();
}
@Autowired
CustomUserDetailsService customUserDetailsService;
/**
* 认证管理器:
* 1、认证信息提供方式(用户名、密码、当前用户的资源权限)
* 2、可采用内存存储方式,也可能采用数据库方式等
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//基于内存存储认证信息 存储的密码必须是加密后的 不然会报错:There is no PasswordEncoder mapped for the id "null"
//auth.inMemoryAuthentication().withUser("zcc").password("123").authorities("ADMIN");
/*String password = bCryptPasswordEncoder().encode("123");
logger.info("加密后的密码:" + password);
auth.inMemoryAuthentication().withUser("zcc").password(password).authorities("ADMIN");*/
// 指定使用自定义查询用户信息来完成身份认证
auth.userDetailsService(customUserDetailsService);
}
@Autowired
CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
ImageVerifyCodeValidateFilter imageVerifyCodeValidateFilter;
@Autowired
SmsVerifyCodeValidateFilter smsVerifyCodeValidateFilter;
@Autowired
MobileAuthenticationConfig mobileAuthenticationConfig;
/**
* 记住我 功能
*/
@Autowired
DataSource dataSource;
@Bean
public JdbcTokenRepositoryImpl jdbcTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// 是否启动时自动创建表,第一次启动创建就行,后面启动把这个注释掉,不然报错已存在表
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
/**
* 资源权限配置(过滤器链):
* 1、被拦截的资源
* 2、资源所对应的角色权限
* 3、定义认证方式:httpBasic 、httpForm
* 4、定制登录页面、登录请求地址、错误处理方式
* 5、自定义 spring security 过滤器
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.httpBasic()//采用httpBasic 认证方式
/*http.formLogin()
.loginPage("/login/page")// 交给 /login/page 响应认证(登录)页面
.loginProcessingUrl("/login/form") // 登录表单提交处理Url, 默认是 /login
.usernameParameter("name") // 默认用户名的属性名是 username
.passwordParameter("pwd") // 默认密码的属性名是 password
.and()
.authorizeRequests()//认证请求
.antMatchers("/login/page").permitAll()//自定义登录页不需要认证
.anyRequest().authenticated();// 所有进入应用的HTTP请求都要进行认证*/
http
.addFilterBefore(imageVerifyCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)//将校验过滤器 imageCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面
.addFilterBefore(smsVerifyCodeValidateFilter,UsernamePasswordAuthenticationFilter.class)//将校验过滤器 smsVerifyCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面
.formLogin()
.loginPage(securityProperties.getLoginPage())// 交给 /login/page 响应认证(登录)页面
.loginProcessingUrl(securityProperties.getLoginProcessingUrl()) // 登录表单提交处理Url, 默认是 /login
.usernameParameter(securityProperties.getUsernameParameter()) // 默认用户名的属性名是 username
.passwordParameter(securityProperties.getPasswordParameter()) // 默认密码的属性名是 password
.successHandler(customAuthenticationSuccessHandler)//自定义认证成功处理器
.failureHandler(customAuthenticationFailureHandler)//自定义认证失败处理器
.and()
.authorizeRequests()//认证请求
.antMatchers(securityProperties.getLoginPage(),securityProperties.getMobilePage(),securityProperties.getImageCodeUrl(),securityProperties.getMobileCodeUrl()).permitAll()//自定义登录页不需要认证,生成图片验证码,发送短信获取验证码也不需要验证
.anyRequest().authenticated()// 所有进入应用的HTTP请求都要进行认证
.and()
.rememberMe()//记住我功能
.tokenRepository(jdbcTokenRepository())//保存登录信息
.tokenValiditySeconds(securityProperties.getTokenValiditySeconds());//记住我有效时长一周
// 将手机相关的配置绑定过滤器链上
http.apply(mobileAuthenticationConfig);
}
/**
* 放行静态资源(js css 等)
*
* @param web
*/
@Override
public void configure(WebSecurity web) {
//web.ignoring().antMatchers("/dist/**", "/modules/**", "/plugins/**");
web.ignoring().antMatchers(securityProperties.getStaticPaths());
}
}
12、测试
记住我功能测试:(记住我 的 input 标签的 name="remember-me")