之前文章都是基于用户名密码登录,第六章图形验证码登录其实还是用户名密码登录,只不过多了一层图形验证码校验而已;Spring Security默认提供的认证流程就是用户名密码登录,整个流程都已经固定了,虽然提供了一些接口扩展,但是有些时候我们就需要有自己特殊的身份认证逻辑,比如用短信验证码登录,它和用户名密码登录的逻辑是不一样的,这时候就需要重新写一套身份认证逻辑。
@GetMapping("/code/sms")
public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 创建验证码
ValidateCode smsCode = createCodeSmsCode(request);
// 将验证码放到session中
sessionStrategy.setAttribute(new ServletWebRequest(request), SMS_CODE_SESSION_KEY, smsCode);
String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
// 发送验证码
smsCodeSender.send(mobile, smsCode.getCode());
}
<tr>
<td>手机号:td>
<td><input type="text" name="mobile" value="13012345678">td>
tr>
<tr>
<td>短信验证码:td>
<td>
<input type="text" name="smsCode">
<a href="/code/sms?mobile=13012345678">发送验证码a>
td>
tr>
SmsAuthenticationFilter
过滤器处理,这个过滤器拿到请求以后会在登录请求中拿到手机号,然后封装成自定义的一个SmsAuthenticationToken(未认证)。AuthenticationManager
整个系统只有一个,它会检索系统中所有的AuthenticationProvider,这时候我们要提供自己的SmsAuthenticationProvider
,用它来校验自己写的SmsAuthenticationToken的手机号信息。UserDetailsService
,把手机号传给它让它去读用户信息,去判断是否能登录,登录成功的话再把SmsAuthenticationToken标记为已认证。在SmsAuthenticationFilter过滤器之前加一个过滤器来验证短信验证码
。public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
/**
* 进入SmsAuthenticationFilter时,构建一个未认证的Token
*
* @param mobile
*/
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
/**
* 认证成功以后构建为已认证的Token
*
* @param principal
* @param authorities
*/
public SmsCodeAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@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();
}
}
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String mobileParameter = "mobile";
private boolean postOnly = true;
/**
* 表示要处理的请求路径
*/
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}
@Override
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);
// 把请求信息设到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 usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.mobileParameter = usernameParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return mobileParameter;
}
}
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
/**
* 进行身份认证的逻辑
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
if (user == null) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
/**
* 表示支持校验的Token,这里是SmsCodeAuthenticationToken
*
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
@Component("validateCodeFilter")
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
/**
* 验证码校验失败处理器
*/
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
/**
* 系统配置信息
*/
@Autowired
private SecurityProperties securityProperties;
/**
* 系统中的校验码处理器
*/
@Autowired
private ValidateCodeProcessorHolder validateCodeProcessorHolder;
/**
* 存放所有需要校验验证码的url
*/
private Map<String, ValidateCodeType> urlMap = new HashMap<>();
/**
* 验证请求url与配置的url是否匹配的工具类
*/
private AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* 初始化要拦截的url配置信息
*/
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
urlMap.put("/authentication/mobile", ValidateCodeType.SMS);
addUrlToMap(securityProperties.getCode().getSms().getUrl(), ValidateCodeType.SMS);
}
/**
* 讲系统中配置的需要校验验证码的URL根据校验的类型放入map
*
* @param urlString
* @param type
*/
protected void addUrlToMap(String urlString, ValidateCodeType type) {
if (StringUtils.isNotBlank(urlString)) {
String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString, ",");
for (String url : urls) {
urlMap.put(url, type);
}
}
}
/**
* 验证短信验证码
*
* @param request
* @param response
* @param chain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
ValidateCodeType type = getValidateCodeType(request);
if (type != null) {
logger.info("校验请求(" + request.getRequestURI() + ")中的验证码,验证码类型" + type);
try {
// 进行验证码的校验
validateCodeProcessorHolder.findValidateCodeProcessor(type)
.validate(new ServletWebRequest(request, response));
logger.info("验证码校验通过");
} catch (ValidateCodeException exception) {
// 如果校验抛出异常,则交给我们之前文章定义的异常处理器进行处理
authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
return;
}
}
// 继续调用后边的过滤器
chain.doFilter(request, response);
}
/**
* 获取校验码的类型,如果当前请求不需要校验,则返回null
*
* @param request
* @return
*/
private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
ValidateCodeType result = null;
if (!StringUtils.equalsIgnoreCase(request.getMethod(), "GET")) {
Set<String> urls = urlMap.keySet();
for (String url : urls) {
if (pathMatcher.match(url, request.getRequestURI())) {
result = urlMap.get(url);
}
}
}
return result;
}
}
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler meicloudAuthenticationFailureHandler;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
// 设置AuthenticationManager
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 设置登录成功处理器
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(meicloudAuthenticationSuccessHandler);
// 设置登录失败处理器
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(meicloudAuthenticationFailureHandler);
String key = UUID.randomUUID().toString();
smsCodeAuthenticationFilter.setRememberMeServices(new PersistentTokenBasedRememberMeServices(key, userDetailsService, persistentTokenRepository));
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
// 将自己写的Provider加到Provider集合里去
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private SecurityProperties securityProperties;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler meicloudAuthenticationFailureHandler;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 验证码校验过滤器
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
// 将验证码校验过滤器加到 UsernamePasswordAuthenticationFilter 过滤器之前
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
// 当用户登录认证时默认跳转的页面
.loginPage("/authentication/require")
// 以下这行 UsernamePasswordAuthenticationFilter 会知道要处理表单的 /authentication/form 请求,而不是默认的 /login
.loginProcessingUrl("/authentication/form")
.successHandler(meicloudAuthenticationSuccessHandler)
.failureHandler(meicloudAuthenticationFailureHandler)
// 配置记住我功能
.and()
.rememberMe()
// 配置TokenRepository
.tokenRepository(persistentTokenRepository())
// 配置Token过期时间
.tokenValiditySeconds(3600)
// 最终拿到用户名之后,使用UserDetailsService去做登录
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()
// 排除对 "/authentication/require" 和 "/meicloud-signIn.html" 的身份验证
.antMatchers("/authentication/require", securityProperties.getBrowser().getSignInPage(), "/code/*").permitAll()
// 表示所有请求都需要身份验证
.anyRequest()
.authenticated()
.and()
.csrf().disable()// 暂时把跨站请求伪造的功能关闭掉
// 相当于把smsCodeAuthenticationSecurityConfig里的配置加到上面这些配置的后面
.apply(smsCodeAuthenticationSecurityConfig);
}
/**
* 记住我功能的Token存取器配置
*
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 启动的时候自动创建表,建表语句 JdbcTokenRepositoryImpl 已经都写好了
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
}