1. Введение
предыдущий оSpring SecurityНаписал две статьи, одна вводнаяUsernamePasswordAuthenticationFilter, другой - введениеAuthenticationManager. Многие студенты заявили, что не могут понять, как использовать эти две вещи, и какие практические проблемы можно решить? Итак, сегодня мы применим эти две теории на практике, напишем с нуля СМС код подтверждения для входа и адаптируем его кSpring Securityв системе. Если у вас есть какие-либо сомнения в чтении, вы можете вернуться и просмотреть эти две статьи, которые могут развеять многие сомнения.
Конечно, вы можете изменить его, чтобы войти в систему с кодом подтверждения вашей электронной почты или других устройств связи.
2. ЖИЗНЕННЫЙ ЦИКЛ CAPTCHA
Код подтверждения имеет срок действия, обычно 5 минут. Общая логика заключается в том, что пользователь вводит номер мобильного телефона для получения кода подтверждения, а сервер кэширует код подтверждения. Пользователь может использовать проверочный код для успешной проверки только один раз в течение максимального периода действия (чтобы не тратить проверочный код впустую); срок его действия истечет по истечении максимального времени.
Жизненный цикл кеша проверочного кода:
public interface CaptchaCacheStorage {
/**
* 验证码放入缓存.
*
* @param phone the phone
* @return the string
*/
String put(String phone);
/**
* 从缓存取验证码.
*
* @param phone the phone
* @return the string
*/
String get(String phone);
/**
* 验证码手动过期.
*
* @param phone the phone
*/
void expire(String phone);
}
Обычно мы используем промежуточное программное обеспечение кэширования, такое какRedis,Ehcache,MemcachedПодождите, чтобы сделать это. Чтобы упростить использование различного промежуточного программного обеспечения учащимися, которые смотрят учебник. Здесь я объединяюSpring CacheКэш-обработка капчи намеренно абстрагируется.
private static final String SMS_CAPTCHA_CACHE = "captcha";
@Bean
CaptchaCacheStorage captchaCacheStorage() {
return new CaptchaCacheStorage() {
@CachePut(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone")
@Override
public String put(String phone) {
return RandomUtil.randomNumbers(5);
}
@Cacheable(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone")
@Override
public String get(String phone) {
return null;
}
@CacheEvict(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone")
@Override
public void expire(String phone) {
}
};
}
Важно обеспечить надежность кэша, что тесно связано с опытом пользователя.
Затем мы должны написать службу проверки кода, а код обслуживания имеет два основных функция:Отправить код подтвержденияикод верификации. Другие, такие как статистика, черные списки и исторические записи, могут быть настроены в соответствии с реальным бизнесом. Здесь реализованы только основные функции.
/**
* 验证码服务.
* 两个功能: 发送和校验.
*
* @param captchaCacheStorage the captcha cache storage
* @return the captcha service
*/
@Bean
public CaptchaService captchaService(CaptchaCacheStorage captchaCacheStorage) {
return new CaptchaService() {
@Override
public boolean sendCaptcha(String phone) {
String existed = captchaCacheStorage.get(phone);
if (StringUtils.hasText(existed)) {
// 节约成本的话如果缓存中有可用的验证码 不再发新的验证码
log.warn("captcha code 【 {} 】 is available now", existed);
return false;
}
// 生成验证码并放入缓存
String captchaCode = captchaCacheStorage.put(phone);
log.info("captcha: {}", captchaCode);
//todo 这里自行完善调用第三方短信服务发送验证码
return true;
}
@Override
public boolean verifyCaptcha(String phone, String code) {
String cacheCode = captchaCacheStorage.get(phone);
if (Objects.equals(cacheCode, code)) {
// 验证通过手动过期
captchaCacheStorage.expire(phone);
return true;
}
return false;
}
};
}
Далее, согласноCaptchaService
Написать интерфейс отправки SMS/captcha/{phone}
.
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
@Resource
CaptchaService captchaService;
/**
* 模拟手机号发送验证码.
*
* @param phone the mobile
* @return the rest
*/
@GetMapping("/{phone}")
public Rest<?> captchaByMobile(@PathVariable String phone) {
//todo 手机号 正则自行验证
if (captchaService.sendCaptcha(phone)){
return RestBody.ok("验证码发送成功");
}
return RestBody.failure(-999,"验证码发送失败");
}
}
3. Интеграция в весеннюю безопасность
Следующие уроки должны использовать знания, представленные в предыдущих двух. Если мы хотим реализовать вход с кодом подтверждения, мы должны определитьServlet Filterдля обработки. Его роль повторяется здесь:
- Перехватывать интерфейс входа по SMS.
- Получить параметры входа и инкапсулировать как
Authentication
Реквизиты для входа. - сдавать
AuthenticationManager
Сертификация.
Нам нужно сначала настроитьAuthentication
иAuthenticationManager
3.1 Учетные данные CAPTCHA
Authentication
На мой взгляд, это носитель.До аутентификации он используется для передачи ключевых параметров входа, таких как имя пользователя и пароль, и проверочный код, после успешной аутентификации он несет информацию о пользователе и набор ролей. так подражатьUsernamePasswordAuthenticationToken
для достиженияCaptchaAuthenticationToken
, Удалите ненужные функции, скопируйте палец:
package cn.felord.spring.security.captcha;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/**
* 验证码认证凭据.
* @author felord.cn
*/
public class CaptchaAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private String captcha;
/**
* 此构造函数用来初始化未授信凭据.
*
* @param principal the principal
* @param captcha the captcha
* @see CaptchaAuthenticationToken#CaptchaAuthenticationToken(Object, String, Collection)
*/
public CaptchaAuthenticationToken(Object principal, String captcha) {
super(null);
this.principal = principal;
this.captcha = captcha;
setAuthenticated(false);
}
/**
* 此构造函数用来初始化授信凭据.
*
* @param principal the principal
* @param captcha the captcha
* @param authorities the authorities
* @see CaptchaAuthenticationToken#CaptchaAuthenticationToken(Object, String)
*/
public CaptchaAuthenticationToken(Object principal, String captcha,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.captcha = captcha;
super.setAuthenticated(true); // must use super, as we override
}
public Object getCredentials() {
return this.captcha;
}
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();
captcha = null;
}
3.2 Менеджер проверки подлинности CAPTCHA
Нам также необходимо настроитьAuthenticationManager
к учетным данным, определенным вышеCaptchaAuthenticationToken
Выполните обработку аутентификации. Следующую картинку нужно снова вынуть:
определятьAuthenticationManager
Просто нужно определить его реализациюProviderManager
. иProviderManager
нужно зависеть отAuthenticationProvider
. Поэтому мы должны реализовать специальныйCaptchaAuthenticationToken
изAuthenticationProvider
.AuthenticationProvider
Процесс:
- от
CaptchaAuthenticationToken
Получите свой номер телефона и код подтверждения. - Используйте номер мобильного телефона, чтобы запросить информацию о пользователе из базы данных и определить, является ли пользователь действительным пользователем, на самом деле, чтобы достичь
UserDetailsService
интерфейс - Проверка проверочного кода.
- Если проверка прошла успешно, доверенные учетные данные инкапсулируются.
- Ошибка проверки вызывает исключение проверки подлинности.
В соответствии с этим процессом реализация выглядит следующим образом:
package cn.felord.spring.security.captcha;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.util.Assert;
import java.util.Collection;
import java.util.Objects;
/**
* 验证码认证器.
* @author felord.cn
*/
@Slf4j
public class CaptchaAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
private final GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
private final UserDetailsService userDetailsService;
private final CaptchaService captchaService;
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
/**
* Instantiates a new Captcha authentication provider.
*
* @param userDetailsService the user details service
* @param captchaService the captcha service
*/
public CaptchaAuthenticationProvider(UserDetailsService userDetailsService, CaptchaService captchaService) {
this.userDetailsService = userDetailsService;
this.captchaService = captchaService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(CaptchaAuthenticationToken.class, authentication,
() -> messages.getMessage(
"CaptchaAuthenticationProvider.onlySupports",
"Only CaptchaAuthenticationToken is supported"));
CaptchaAuthenticationToken unAuthenticationToken = (CaptchaAuthenticationToken) authentication;
String phone = unAuthenticationToken.getName();
String rawCode = (String) unAuthenticationToken.getCredentials();
UserDetails userDetails = userDetailsService.loadUserByUsername(phone);
// 此处省略对UserDetails 的可用性 是否过期 是否锁定 是否失效的检验 建议根据实际情况添加 或者在 UserDetailsService 的实现中处理
if (Objects.isNull(userDetails)) {
throw new BadCredentialsException("Bad credentials");
}
// 验证码校验
if (captchaService.verifyCaptcha(phone, rawCode)) {
return createSuccessAuthentication(authentication, userDetails);
} else {
throw new BadCredentialsException("captcha is not matched");
}
}
@Override
public boolean supports(Class<?> authentication) {
return CaptchaAuthenticationToken.class.isAssignableFrom(authentication);
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(userDetailsService, "userDetailsService must not be null");
Assert.notNull(captchaService, "captchaService must not be null");
}
@Override
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
/**
* 认证成功将非授信凭据转为授信凭据.
* 封装用户信息 角色信息。
*
* @param authentication the authentication
* @param user the user
* @return the authentication
*/
protected Authentication createSuccessAuthentication(Authentication authentication, UserDetails user) {
Collection<? extends GrantedAuthority> authorities = authoritiesMapper.mapAuthorities(user.getAuthorities());
CaptchaAuthenticationToken authenticationToken = new CaptchaAuthenticationToken(user, null, authorities);
authenticationToken.setDetails(authentication.getDetails());
return authenticationToken;
}
}
Затем вы можете собратьProviderManager
А:
ProviderManager providerManager = new ProviderManager(Collections.singletonList(captchaAuthenticationProvider));
проходить через3.1и3.2подготовка, наши приготовления завершены.
3.3 Фильтр аутентификации CAPTCHA
После настройки учетных данных капчи и менеджера аутентификации капчи мы можем определить фильтры аутентификации капчи. немного отредактироватьUsernamePasswordAuthenticationFilterможет удовлетворить потребности:
package cn.felord.spring.security.captcha;
import org.springframework.lang.Nullable;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CaptchaAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_PHONE_KEY = "phone";
public static final String SPRING_SECURITY_FORM_CAPTCHA_KEY = "captcha";
public CaptchaAuthenticationFilter() {
super(new AntPathRequestMatcher("/clogin", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String phone = obtainPhone(request);
String captcha = obtainCaptcha(request);
if (phone == null) {
phone = "";
}
if (captcha == null) {
captcha = "";
}
phone = phone.trim();
CaptchaAuthenticationToken authRequest = new CaptchaAuthenticationToken(
phone, captcha);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Nullable
protected String obtainCaptcha(HttpServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_CAPTCHA_KEY);
}
@Nullable
protected String obtainPhone(HttpServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_PHONE_KEY);
}
protected void setDetails(HttpServletRequest request,
CaptchaAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
}
Здесь мы указываем, что код проверки перехватчика запроса приземляется:
POST /clogin?phone=手机号&captcha=验证码 HTTP/1.1
Host: localhost:8082
Дальше настраивается.
3.4 Конфигурация
Я собрал всю конфигурацию, связанную с аутентификацией по капче, и добавил комментарии.
package cn.felord.spring.security.captcha;
import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.Objects;
/**
* 验证码认证配置.
*
* @author felord.cn
* @since 13 :23
*/
@Slf4j
@Configuration
public class CaptchaAuthenticationConfiguration {
private static final String SMS_CAPTCHA_CACHE = "captcha";
/**
* spring cache 管理验证码的生命周期.
*
* @return the captcha cache storage
*/
@Bean
CaptchaCacheStorage captchaCacheStorage() {
return new CaptchaCacheStorage() {
@CachePut(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone")
@Override
public String put(String phone) {
return RandomUtil.randomNumbers(5);
}
@Cacheable(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone")
@Override
public String get(String phone) {
return null;
}
@CacheEvict(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone")
@Override
public void expire(String phone) {
}
};
}
/**
* 验证码服务.
* 两个功能: 发送和校验.
*
* @param captchaCacheStorage the captcha cache storage
* @return the captcha service
*/
@Bean
public CaptchaService captchaService(CaptchaCacheStorage captchaCacheStorage) {
return new CaptchaService() {
@Override
public boolean sendCaptcha(String phone) {
String existed = captchaCacheStorage.get(phone);
if (StringUtils.hasText(existed)) {
// 节约成本的话如果缓存存在可用的验证码 不再发新的验证码
log.warn("captcha code 【 {} 】 is available now", existed);
return false;
}
// 生成验证码并放入缓存
String captchaCode = captchaCacheStorage.put(phone);
log.info("captcha: {}", captchaCode);
//todo 这里自行完善调用第三方短信服务
return true;
}
@Override
public boolean verifyCaptcha(String phone, String code) {
String cacheCode = captchaCacheStorage.get(phone);
if (Objects.equals(cacheCode, code)) {
// 验证通过手动过期
captchaCacheStorage.expire(phone);
return true;
}
return false;
}
};
}
/**
* 自行实现根据手机号查询可用的用户,这里简单举例.
* 注意该接口可能出现多态。所以最好加上注解@Qualifier
*
* @return the user details service
*/
@Bean
@Qualifier("captchaUserDetailsService")
public UserDetailsService captchaUserDetailsService() {
// 验证码登陆后密码无意义了但是需要填充一下
return username -> User.withUsername(username).password("TEMP")
//todo 这里权限 你需要自己注入
.authorities(AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_APP")).build();
}
/**
* 验证码认证器.
*
* @param captchaService the captcha service
* @param userDetailsService the user details service
* @return the captcha authentication provider
*/
@Bean
public CaptchaAuthenticationProvider captchaAuthenticationProvider(CaptchaService captchaService,
@Qualifier("captchaUserDetailsService")
UserDetailsService userDetailsService) {
return new CaptchaAuthenticationProvider(userDetailsService, captchaService);
}
/**
* 验证码认证过滤器.
*
* @param authenticationSuccessHandler the authentication success handler
* @param authenticationFailureHandler the authentication failure handler
* @param captchaAuthenticationProvider the captcha authentication provider
* @return the captcha authentication filter
*/
@Bean
public CaptchaAuthenticationFilter captchaAuthenticationFilter(AuthenticationSuccessHandler authenticationSuccessHandler,
AuthenticationFailureHandler authenticationFailureHandler,
CaptchaAuthenticationProvider captchaAuthenticationProvider) {
CaptchaAuthenticationFilter captchaAuthenticationFilter = new CaptchaAuthenticationFilter();
// 配置 authenticationManager
ProviderManager providerManager = new ProviderManager(Collections.singletonList(captchaAuthenticationProvider));
captchaAuthenticationFilter.setAuthenticationManager(providerManager);
// 成功处理器
captchaAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
// 失败处理器
captchaAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
return captchaAuthenticationFilter;
}
}
Однако это еще не конец, нужноCaptchaAuthenticationFilter
настроить весьSpring SecurityВ цепочке фильтров такой тип студентов, которые смотрели учебник Fat Brother, должен быть хорошо знаком с ним.
Пожалуйста, обратите особое внимание: убедитесь, что к интерфейсу входа и интерфейсу кода подтверждения можно получить анонимный доступ. Если это динамическое разрешение, вы можете добавить его в интерфейс.
ROLE_ANONYMOUS
Роль.
Готово, тест выглядит следующим образом:
И оригинальный метод входа не затрагивается.
4. Резюме
сквозьUsernamePasswordAuthenticationFilterиAuthenticationManagerсистемное обучение, мы узналиSpring SecurityВесь процесс сертификации, эта статья является практическим применением этих двух статей. Думаю, после прочтения этой статьи вас не смутят иллюстрации в предыдущих статьях, это тоже попытка от теории к практике. DEMO можно получить через соответствующие статьи в личном блоге felord.cn.
关注公众号:Felordcn获取更多资讯