短信登录开发
上图,可见要自己扩展的明白原理流程;
短信登录和 用户名密码登录的逻辑不同(现在也不知道为什么不同,跟着走吧),不能和之前的写在一起
这里模仿它的原理进行另外一条线,加入短信登录的认证(注意不是验证);不是验证发送的短信验证码;
之前写的图形验证码是在 UsernamePasswordAuthenticationFilter前增加了我们自己的图形验证过滤器,
验证成功之后再交给用户名和密码进行认证,调用userDetailsService进行匹配验证;
最后通过的话,会进入Authentication已认证流程;
短信认证的思路和上面一样:
-
SmsCodeAuthenticationFilter 短信登录请求
-
SmsCodeAuthenticationProvider 提供短信登录处理的实现类
-
SmsCodeAuthenticationToken 存放认证信息(包括未认证前的参数信息传递)
-
最后开发一个过滤器放在 短信登录请求之前,进行短信验证码的验证,
因为这个过滤器只关心提交的验证码是否正常就行了。所以可以应用到任意业务中,对任意业务提交进行短信的验证
SmsCodeAuthenticationToken
package cn.mrcode.imooc.springsecurity.securitycore.authentication.mobile;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* 编写思路:直接复制参考 UsernamePasswordAuthenticationToken 的写法
* 分析哪些需要哪些是不需要的。包括功能
* @author zhuqiang
* @version 1.0.1 2018/8/4 17:54
* @date 2018/8/4 17:54
*/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
// ~ Instance fields
// ================================================================================================
/** 存放用户名 : credentials 字段去掉,因为短信认证在授权认证前已经过滤了 */
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;
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 principal
* @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
// ========================================================================================================
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return null;
}
@Override
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();
}
}
SmsCodeAuthenticationFilter
package cn.mrcode.imooc.springsecurity.securitycore.authentication.mobile;
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;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 短信验证码验证: 直接仿照UsernamePasswordAuthenticationFilter
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/5 9:29
*/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private boolean postOnly = true;
// ~ Constructors
// ===================================================================================================
public SmsCodeAuthenticationFilter() {
// 拦截该路径,如果是访问该路径,则标识是需要短信登录
super(new AntPathRequestMatcher("/authentication/sms", "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();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
// Allow subclasses to set the "details" property
// 把request里面的一些信息copy近token里面
// 后面认证成功的时候还需要copy这信息到新的token
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected void setDetails(HttpServletRequest request,
SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return mobileParameter;
}
}
SmsCodeAuthenticationProvider
这个没有找到仿照的地方。没有发现和usernamePassword类型的提供provider
package cn.mrcode.imooc.springsecurity.securitycore.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 : zhuqiang
* @version : V1.0
* @date : 2018/8/5 9:20
*/
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
UserDetails user = userDetailsService.loadUserByUsername((String) token.getPrincipal());
if(user == null){
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
// 需要把未认证中的一些信息copy到已认证的token中
authenticationResult.setDetails(token);
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
SmsCodeAuthenticationSecurityConfig
需要的几个东西已经准备好了。这里要进行配置把这些加入到 security的认证流程中去;
package cn.mrcode.imooc.springsecurity.securitycore.authentication.mobile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
/**
* app和浏览器都需要使用,短信验证配置
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/5 9:59
*/
@Component
public class SmsCodeAuthenticationSecurityConfig
extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter();
// 这两个设置参数 不知道从哪里来的
// 难道是要去看一个源码?
// 把该过滤器交给管理器
// 图上流程,因为最先走的 短信认证的过滤器(不是验证码,只是认证)
// 要使用管理器来获取provider,所以把管理器注册进去
filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
filter.setAuthenticationFailureHandler(authenticationFailureHandler);
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
http
// 注册到AuthenticationManager中去
.authenticationProvider(smsCodeAuthenticationProvider)
// 添加到 UsernamePasswordAuthenticationFilter 之后
// 貌似所有的入口都是 UsernamePasswordAuthenticationFilter
// 然后UsernamePasswordAuthenticationFilter的provider不支持这个地址的请求
// 所以就会落在我们自己的认证过滤器上。完成接下来的认证
.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
}
}
BrowserSecurityConfig 应用方配置
变化的配置用注释标出来了。无变化的把注释去掉了;
package cn.mrcode.imooc.springsecurity.securitybrowser;
import cn.mrcode.imooc.springsecurity.securitycore.authentication.mobile.SmsCodeAuthenticationSecurityConfig;
import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityProperties;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.SmsCodeFilter;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCodeFilter;
import org.springframework.beans.factory.annotation.Autowired;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/3 0:05
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private DataSource dataSource;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
@Autowired
private UserDetailsService userDetailsService;
// 由下面的 .apply(smsCodeAuthenticationSecurityConfigs)方法添加这个配置
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfigs;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setFailureHandler(myAuthenticationFailureHandler);
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
// 短信的是copy图形的过滤器,这里直接copy初始化
SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
smsCodeFilter.setFailureHandler(myAuthenticationFailureHandler);
smsCodeFilter.setSecurityProperties(securityProperties);
smsCodeFilter.afterPropertiesSet();
http
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
// 在这里不能注册到我们自己的短信认证过滤器上,会报错
.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository)
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()
.antMatchers("/authentication/require",
securityProperties.getBrowser().getLoginPage(),
"/code/*",
"/error"
)
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable()
// 这里应用短信认证配置
.apply(smsCodeAuthenticationSecurityConfigs)
;
}
}
登录页面再回顾下表单的提交代码
/authentication/sms 登录地址,就是我们认证过滤器里面的支持地址;
<h3>短信验证码</h3>
<form action="/authentication/sms" method="post">
| 手机号:||
| :-----: | :-----: |
| 手机号: | |
| 短信验证码: | 发送验证码 |
| 登录 |
</form>
测试
- 登录页面
- 点击发送短信验证码
- 返回到登录页面
- 后台复制真正发送的验证码添加
- 提交短信登录
总结
自定义 认证 逻辑的配置大致步骤:
-
入口配置 应用方使用该配置
.apply(smsCodeAuthenticationSecurityConfigs)
-
提供处理过滤器 ProcessingFilter 并限制该过滤器支持拦截的url
-
提供AuthenticationProvider 进行认证的处理支持
-
把ProviderManager 赋值给 ProcessingFilter
-
把AuthenticationProvider注册到AuthenticationManager中去
(这里完成ProcessingFilter调用管理器查找Provider,完成认证这个过程)
-
把 ProcessingFilter 添加到 认证处理链中 , 之后
(也就是UsernamePasswordAuthenticationFilter)
自定义 验证码验证 逻辑的配置大致步骤
-
入口配置 应用方把验证码(验证是否有效,是否过期)的过滤器添加到认证处理链中 之前
(也就是UsernamePasswordAuthenticationFilter),就是在进入认证之前线把验证码是否有效先验证了
这里发现 认证和部分业务逻辑是不一致的。无关的直接分离;
比如这里的验证过滤器,只管提交的短信验证码在当前的session中是否有效;
而后面的短信认证登录,只是根据用户名去获取用户详细信息,并返回;
而默认的用户名密码过认证登录的逻辑是:根据用户名获取用户详细信息,然后再比对密码是否ok;
从这里可以看出来,短信认证的其实也可以写在认证逻辑里面,这样分离出去是不是就做成公用的了? -
提供验证码过的过滤器 里面的逻辑要配合 验证码发送服务中的存储方式进行获取发送的验证码
从这里可以看出来。认证中涉及到业务逻辑了:配置发送验证服务中的逻辑进行获取验证码相关信息