目录
1.开发短信验证的步骤
2.开发短信验证码的接口,用来发送短信验证码
3.重构代码
4.校验短信验证码并且登录
5.重构逻辑
1.开发生成短信验证码的接口,这个接口主要做三件事情,第一,生成随机的验证码,第二,将验证码存到Session中,第三,
调用短信服务商,发送短信
2.编写校验验证码合法性的过滤器
3.编写认证流程,自定义过滤器链,加入Security的过滤器链
短信验证码的界面:
短信验证码相比于图形验证码少了图片的字段,
包装短信验证码的配置类,图形验证码继承此类即可。
短信验证码的包装类
开发生成短信验证码的接口。
开发发送短信验证码的接口,这个接口既要在系统存存在一个默认的发送配置,又要让用户可以自定义实现发送的逻辑
在短信验证码的这两个这里用@Autowired注入了接口名字一样的两个Bean,Spring怎么知道需要注入的实例Bean是哪一个呢?
所以我们需要指定Bean的名字,这样Spring才能正确的注入。
重构的点:
1.图像验证码和手机验码生成接口主干逻辑板相同,主干逻辑相同而其中步骤有些不同的这种代码,会用模板方法模式把他抽出来。相同的代码会放在复用,不同的逻辑会到子类里面实现。
通常的情况下,接口都是单实现,现在这个验证码的生成器这个接口有两个实现类,并且其中验证码的生成逻辑可以交给自定义服务去实现,所以这个生成器的bean需要条件装配,要条件装配的bean通常都不会打上@Component注解,而是通过@Bean的方式注入一个默认的实现类。条件装配注解@ConditionalOnMissingBean,可以以有种重配置方式,比如以bean名字的方式,或者以接口类型的方式。如果在系统中某个接口已经有多个实现类了,那么就要以bean的名字来条件装配。
Spring看到@Autowird Map 就会在启动的时候查找容器里面所有的这个接口的实现,将bean的名字作为Key,实现类作为value
2.分层的去封装
如果整个验证码的发送流程变了,不在是生成验证码,存入session,发送这样一个主逻辑,那么就可以重新实现封装主干逻辑的接口,如果仅仅只是验证码的生成逻辑变了,那么只需要实现Generator接口即可。
两个接口,验证码处理的接口,和验证码生成的接口,在请求验证码的接口上按照请求的类型来调用不同的处理逻辑,
仿照表单登录的流程,写短信验证码登录的流程,manager不能覆盖,manager只有一个。
可以看到的是这里面暂时没有包含验短信验证码的流程逻辑,只是拿着一个手机号去查用户信息,如果查到用户信息,则标记登录成功。那么校验短信验证码是否合法的的逻辑应该和图形验证码的校验逻辑是一样的,重新在AuthenticationFilter的前面新增一个过滤器去验证,这样把这个验证的逻辑提出来是方便可以给其他的接口重用这个验证短信验证码的逻辑。
那就仿照着用户名密码登录验证的这个流程来写,首先写token类,token在认证前放用户名,认证后放用户的信息。
在表单登录之前,UsernamePasswordAuthenticationToken中的属性和参数如下图所示,principal字段存的是用户名,credentials存的是密码,被标记为未认证过。
然后拿到Token的类型,挑出能够处理这种token类型的provider.
然后就根据用户名查询用户是否存在
一系列认证过后会重新生成已认证的UsernamePasswordAuthenticationToken,返回
认证过后的Token的principal,字段存放的就是用户的信息,标记为已认证。
所以我们就首先来写这个token,
package org.lilly.core.authentication.mobile;
import java.util.Collection;
import javax.security.auth.Subject;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
/**
* 短信认证的token 类
* 模仿的是表单登录的Token: UsernamePasswordAuthenticationToken
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
/**
* 存放手机号
*/
private final Object mobile;
// ~ Constructors
// ===================================================================================================
/**
* This constructor can be safely used by any code that wishes to create a
* UsernamePasswordAuthenticationToken
, as the {@link #isAuthenticated()}
* will return false
.
*
*/
public SmsAuthenticationToken(Object mobile) {
super(null);
this.mobile = mobile;
setAuthenticated(false);
}
/**
* This constructor should only be used by AuthenticationManager
or
* AuthenticationProvider
implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = true
)
* authentication token.
*
* @param mobile
* @param authorities
*/
public SmsAuthenticationToken(Object mobile,
Collection extends GrantedAuthority> authorities) {
super(authorities);
this.mobile = mobile;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
// ========================================================================================================
@Override
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.mobile;
}
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();
}
@Override
public boolean implies(Subject subject) {
return false;
}
}
其次写provider组件和Filter组件,这两个组件也是仿照着来写
package org.lilly.core.authentication.mobile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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;
/**
* 短信登录的filter
* 模仿的是表单登录的Filter :UsernamePasswordAuthenticationFilter
*/
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "mobile";
private String mobileParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private boolean postOnly = true;
// ~ Constructors
// ===================================================================================================
public SmsAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/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 = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsAuthenticationToken authRequest = new SmsAuthenticationToken(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 Authentication
* request token to the AuthenticationManager
*/
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,
SmsAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
/**
* Sets the parameter name which will be used to obtain the username from the login
* request.
*
* @param mobileParameter the parameter name. Defaults to "username".
*/
public void setUsernameParameter(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
* unsuccessfulAuthentication() method will be called as if handling a failed
* authentication.
*
* Defaults to true but may be overridden by subclasses.
*/
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return mobileParameter;
}
}
package org.lilly.core.authentication.mobile;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.lilly.core.service.MyUserDetailsService;
import org.springframework.context.support.MessageSourceAccessor;
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.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.UsernameNotFoundException;
import org.springframework.util.Assert;
/**
* 短信登录的校验类 Provider
* 模仿的是表单登录的Provider: DaoAuthenticationProvider
*/
public class SmsAuthenticationProvider implements AuthenticationProvider {
protected final Log logger = LogFactory.getLog(getClass());
// ~ Instance fields
// ================================================================================================
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
private MyUserDetailsService myUserDetailsService;
// ~ Methods
// ========================================================================================================
/**
* Performs authentication with the same contract as
* {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}
* .
*
* @param authentication the authentication request object.
* @return a fully authenticated object including credentials. May return
* null
if the AuthenticationProvider
is unable to support
* authentication of the passed Authentication
object. In such a case,
* the next AuthenticationProvider
that supports the presented
* Authentication
class will be tried.
* @throws AuthenticationException if authentication fails.
*/
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//SmsAuthenticationProvider只支持SmsAuthenticationToken类型的Token
Assert.isInstanceOf(SmsAuthenticationToken.class, authentication,
messages.getMessage(
"SmsAuthenticationProvider.onlySupports",
"Only SmsAuthenticationToken is supported"));
String mobile = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
//根据mobile查询用户的信息。
UserDetails user = null;
try {
user = retrieveUser(mobile,
(SmsAuthenticationToken) authentication);
} catch (UsernameNotFoundException notFound) {
logger.debug("User '" + mobile + "' not found");
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
//封装认证后的token
SmsAuthenticationToken result = new SmsAuthenticationToken(
mobile, authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
/**
* Returns true
if this AuthenticationProvider
supports the
* indicated Authentication
object.
*
* Returning true
does not guarantee an
* AuthenticationProvider
will be able to authenticate the presented
* instance of the Authentication
class. It simply indicates it can
* support closer evaluation of it. An AuthenticationProvider
can still
* return null
from the {@link #authenticate(Authentication)} method to
* indicate another AuthenticationProvider
should be tried.
*
*
* Selection of an AuthenticationProvider
capable of performing
* authentication is conducted at runtime the ProviderManager
.
*
*
* @param authentication
* @return true
if the implementation can more closely evaluate the
* Authentication
class presented
*/
public boolean supports(Class> authentication) {
//isAssignableFrom SmsAuthenticationToken 对象所表示的类或接口与指定的authentication
// 参数所表示的类或接口是否相同,或是否是其超类或超接口
return (SmsAuthenticationToken.class
.isAssignableFrom(authentication));
}
protected final UserDetails retrieveUser(String mobile,
SmsAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getMyUserDetailsService().loadUserByMobile(mobile);
} catch (AuthenticationException notFound) {
throw notFound;
} catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
public GrantedAuthoritiesMapper getAuthoritiesMapper() {
return authoritiesMapper;
}
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
this.authoritiesMapper = authoritiesMapper;
}
public MyUserDetailsService getMyUserDetailsService() {
return myUserDetailsService;
}
public void setMyUserDetailsService(MyUserDetailsService myUserDetailsService) {
this.myUserDetailsService = myUserDetailsService;
}
}
然后需要仿照校验图片验证码的校验逻辑写短信验证码的校验逻辑
package org.lilly.core.validate.filter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.lilly.core.properties.SecurityProperties;
import org.lilly.core.validate.code.ValidateException;
import org.lilly.core.validate.code.image.ImageCode;
import org.lilly.core.validate.code.sms.ValidateCode;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* 校验手机验证码的过滤器 OncePerRequestFilter 只会执行一次的过滤器
*/
public class SmsValidateCodeFilter extends OncePerRequestFilter {
private static final String SESSION_KEY = "SESSION_CODE_";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private AuthenticationFailureHandler authenticationFailureHandler;
private SecurityProperties securityProperties;
public SmsValidateCodeFilter(AuthenticationFailureHandler authenticationFailureHandler, SecurityProperties securityProperties) {
this.authenticationFailureHandler = authenticationFailureHandler;
this.securityProperties = securityProperties;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
if (StringUtils.equalsIgnoreCase("/authentication/mobile", httpServletRequest.getRequestURI())
&& StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) {
try {
validate(httpServletRequest);
} catch (ValidateException e) {
//如果有异常,用登录失败异常处理器,这里必须处理AuthenticationException
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
//验证码校验有异常必须要返回,不然还会去验用户名和密码。
return;
}
}
//没有异常 登录成功放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private void validate(HttpServletRequest httpServletRequest) throws ServletRequestBindingException {
String key = SESSION_KEY + "SMS".toUpperCase();
ValidateCode validateCodeInSession = (ValidateCode) sessionStrategy.getAttribute(new ServletWebRequest(httpServletRequest), key);
//拿到post请求中的参数
String code = ServletRequestUtils.getStringParameter(httpServletRequest, "smsCode");
if (StringUtils.isBlank(code)) {
throw new ValidateException("验证码不能为空");
}
if (validateCodeInSession == null) {
throw new ValidateException("验证码不存在");
}
if (validateCodeInSession.isExpire()) {
sessionStrategy.removeAttribute(new ServletWebRequest(httpServletRequest), key);
throw new ValidateException("验证码已过期");
}
if (!StringUtils.equals(validateCodeInSession.getCode(), code)) {
throw new ValidateException("验证码不正确");
}
sessionStrategy.removeAttribute(new ServletWebRequest(httpServletRequest), key);
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
最后需要重写过滤器链的配置来把短信验证码的所有自定义的组件串联起来。
package org.lilly.core.authentication.config;
import org.lilly.core.authentication.mobile.SmsAuthenticationFilter;
import org.lilly.core.authentication.mobile.SmsAuthenticationProvider;
import org.lilly.core.service.MyUserDetailsService;
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.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.*;
import org.springframework.stereotype.Component;
/**
* 自定义的短信验证码登录security配置,
* SecurityConfigurerAdapter用于将自定义的校验组织组件串起来
*/
@Component
public class SmsSecurityConfig extends SecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
super.configure(httpSecurity);
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
//为sms filter设置Manager组件 Manager不是我们自己写的而且在整个系统中就只有一个,所以这里需要拿到系统中的Manager
//sharedObject 存放的就是顶级对象,用于获取所有的运行时信息。
smsAuthenticationFilter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class));
//为sms filter设置 校验成功的处理
smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
//为sms filter 设置校验失败的处理
smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
//设置Provider,并且将sms过滤器加到过滤器链中
SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
smsAuthenticationProvider.setMyUserDetailsService(myUserDetailsService);
//authenticationProvider的provider我们可以自定义 即便是表单登录
httpSecurity.authenticationProvider(smsAuthenticationProvider)
.addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
package org.lilly.browser.config;
import org.lilly.browser.authentication.LillyAuthenticationFailureHandler;
import org.lilly.browser.authentication.LillyAuthenticationSuccessHandler;
import org.lilly.core.authentication.config.SmsSecurityConfig;
import org.lilly.core.authentication.mobile.SmsAuthenticationFilter;
import org.lilly.core.properties.SecurityProperties;
import org.lilly.core.validate.filter.SmsValidateCodeFilter;
import org.lilly.core.validate.filter.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.EnableWebSecurity;
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;
/**
* User: Mr.Wang
* Date: 2020/5/31
*/
@Configuration
@EnableWebSecurity
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private LillyAuthenticationSuccessHandler lillyAuthenticationSuccessHandler;
@Autowired
private LillyAuthenticationFailureHandler lillyAuthenticationFailureHandler;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SmsSecurityConfig smsSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(lillyAuthenticationFailureHandler, securityProperties);
SmsValidateCodeFilter smsValidateCodeFilter = new SmsValidateCodeFilter(lillyAuthenticationFailureHandler, securityProperties);
// validateCodeFilter.afterPropertiesSet();
http
.authorizeRequests().antMatchers("/index",
"/authentication/require",
"/authentication/form",
"/authentication/mobile",
"/code/*",
securityProperties.getBrowser().getLoginPage()) //允许不登陆就可以访问的方法,多个用逗号分隔
.permitAll()
.anyRequest().authenticated() //其他的需要授权后访问
.and()
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(smsValidateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin() //使用form表单post方式进行登录
.loginPage("/authentication/require") //自定义登录页面的跳转
.loginProcessingUrl("/authentication/form") //表单登陆提交的登陆请求地址
.successHandler(lillyAuthenticationSuccessHandler) //登录成功事件处理
// .successForwardUrl("/hello") 设置登录成功跳转页面
// .failureUrl("/login?error=true") //error=true控制页面错误信息的展示
.failureHandler(lillyAuthenticationFailureHandler)
.permitAll()
.and()
.rememberMe()
.tokenRepository(tokenRepository())
.userDetailsService(userDetailsService)
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
.and()
.apply(smsSecurityConfig);
//关闭打开的csrf保护
// http.cors().and().csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//第二次启动时需要注释掉这行代码
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
当你写代码的时候,在复制粘贴,那么功能写完了以后就要有意识的回过头来重构,将这些重复的代码消除掉。大到整块功能,小到一行字符串都需要去消除重复,不然修改功能的时候你需要记住自己复制过几次,把复制的地方都得改掉。
所以这次重构主要做消除重复,把配置重新抽象一下
1.两种校验码的过滤器合并成一个。
这个过滤器每个请求都会来访问,但是只针对短信验证码和图形验证码这两个特定的url才进行校验,并且这个过滤器应该放在
SmsAuthenticationFilter,UsernamePasswordAuthenticationFilter这两个过滤器之前。AbstractPreAuthenticatedProcessingFilter就在两者之前。
package org.lilly.core.validate.filter;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.lilly.core.enums.ValidateCodeType;
import org.lilly.core.properties.SecurityProperties;
import org.lilly.core.utils.SecurityConstant;
import org.lilly.core.validate.ValidateCodeProcessorHolder;
import org.lilly.core.validate.code.ValidateException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* 图形验证码,短信验证码的校验Filter
*/
@Component("imageAndSmsValidateCodeFilter")
public class ImageAndSmsValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
private final Logger LOGGER = LoggerFactory.getLogger(getClass());
/**
* 根据对应的URL类型获取对应校验处理器
*/
private Map urlsMap = new HashMap<>();
@Autowired
private SecurityProperties securityProperties;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private ValidateCodeProcessorHolder validateCodeProcessorHolder;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//1.根据请求的url获取需要检验码校验的类型
ValidateCodeType validateCodeType = urlsMap.get(httpServletRequest.getRequestURI());
if (validateCodeType != null) {
//2.根据校验类型获取校验码的校验逻辑
try {
LOGGER.info("校验请求(" + httpServletRequest.getRequestURI() + ")中的验证码,验证码类型为" + validateCodeType);
validateCodeProcessorHolder
.findValidateCodeProcessorByType(validateCodeType)
.validate(new ServletWebRequest(httpServletRequest, httpServletResponse));
} catch (ValidateException e) {
//如果有异常,用登录失败异常处理器,这里必须处理AuthenticationException
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
//验证码校验有异常必须要返回,不然还会去验用户名和密码。
return;
}
}
//doFilter >>>>>>>
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
/**
* 初始化要拦截的URL配置信息
*
* @throws ServletException
*/
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
//添加表单登录的请求
urlsMap.put(SecurityConstant.AUTHENTICATION_FORM_LOGIN, ValidateCodeType.IMAGE);
//添加用户配置的拦截url
addUrlsToMap(securityProperties.getValidateCode().getImageCode().getUrl(), ValidateCodeType.IMAGE);
//添加短信登录请求
urlsMap.put(SecurityConstant.AUTHENTICATION_MOBILE_LOGIN, ValidateCodeType.SMS);
addUrlsToMap(securityProperties.getValidateCode().getSmsCode().getUrl(), ValidateCodeType.SMS);
}
/**
* 添加额外用户配置的额外的拦截url
*
* @param urls
* @param type
*/
private void addUrlsToMap(String urls, ValidateCodeType type) {
if (StringUtils.isBlank(urls)) {
return;
} else {
String[] split = StringUtils.splitByWholeSeparatorPreserveAllTokens(urls, ",");
for (String url : split) {
urlsMap.put(url, type);
}
}
}
}
2.验证码配置抽象出来
第一个配置是密码登录相关的配置,只配置密码登录的东西
第二个是校验码相关的配置代码
@Component("validateCodeSecurityConfig")
public class ValidateCodeSecurityConfig extends SecurityConfigurerAdapter {
/**
* ImageAndSmsValidateCodeFilter
*/
@Autowired
private Filter imageAndSmsValidateCodeFilter;
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
//http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
//AbstractPreAuthenticatedProcessingFilter 这个类将检查安全上下文的当前内容,如果为空,它将尝试从HTTP请求中提取用户信息并将其提交给AuthenticationManager
// SmsAuthenticationFilter,UsernamePasswordAuthenticationFilter都继承此类
http.addFilterBefore(imageAndSmsValidateCodeFilter, AbstractPreAuthenticatedProcessingFilter.class);
}
}
第三个短信登录的配置
三个基本的配置有了 然后就是配置浏览器的配置:
package org.lilly.browser.config;
import org.lilly.browser.authentication.LillyAuthenticationFailureHandler;
import org.lilly.browser.authentication.LillyAuthenticationSuccessHandler;
import org.lilly.core.config.AbstractChannelSecurityConfig;
import org.lilly.core.config.SmsCodeAuthenticationSecurityConfig;
import org.lilly.core.config.ValidateCodeSecurityConfig;
import org.lilly.core.properties.SecurityProperties;
import org.lilly.core.utils.SecurityConstant;
import org.lilly.core.validate.filter.ImageAndSmsValidateCodeFilter;
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.EnableWebSecurity;
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;
/**
* User: Mr.Wang
* Date: 2020/5/31
* AbstractChannelSecurityConfig 直接继承表单登录配置的公共代码
*/
@Configuration
public class BrowserSecurityConfig extends AbstractChannelSecurityConfig {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Autowired
private ValidateCodeSecurityConfig validateCodeSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
// ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(lillyAuthenticationFailureHandler, securityProperties);
// SmsValidateCodeFilter smsValidateCodeFilter = new SmsValidateCodeFilter(lillyAuthenticationFailureHandler, securityProperties);
// validateCodeFilter.afterPropertiesSet();
//表单登录基本配置
applyPasswordAuthenticationConfig(http);
//浏览器需要的配置
http
.apply(validateCodeSecurityConfig)
.and()
.apply(smsCodeAuthenticationSecurityConfig).
and()
.rememberMe()
.tokenRepository(tokenRepository())
.userDetailsService(userDetailsService)
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()).and()
.authorizeRequests()
.antMatchers("/index",
"/authentication/require",
"/authentication/form",
"/authentication/mobile",
"/code/*",
"/favicon.ico",
securityProperties.getBrowser().getLoginPage()) //允许不登陆就可以访问的方法,多个用逗号分隔
.permitAll()
.anyRequest().authenticated(); //其他的需要授权后访问;
// http
// .authorizeRequests().antMatchers("/index",
// "/authentication/require",
// "/authentication/form",
// "/authentication/mobile",
// "/code/*",
// securityProperties.getBrowser().getLoginPage()) //允许不登陆就可以访问的方法,多个用逗号分隔
// .permitAll()
// .anyRequest().authenticated() //其他的需要授权后访问
// .and()
// .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(smsValidateCodeFilter, UsernamePasswordAuthenticationFilter.class)
// .formLogin() //使用form表单post方式进行登录
// .loginPage("/authentication/require") //自定义登录页面的跳转
// .loginProcessingUrl(SecurityConstant.AUTHENTICATION_FORM_LOGIN) //表单登陆提交的登陆请求地址
// .successHandler(lillyAuthenticationSuccessHandler) //登录成功事件处理
.successForwardUrl("/hello") 设置登录成功跳转页面
.failureUrl("/login?error=true") //error=true控制页面错误信息的展示
// .failureHandler(lillyAuthenticationFailureHandler)
// .permitAll()
// .and()
// .rememberMe()
// .tokenRepository(tokenRepository())
// .userDetailsService(userDetailsService)
// .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
// .and()
// .apply(smsCodeAuthenticationSecurityConfig);
//关闭打开的csrf保护
// http.cors().and().csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//第二次启动时需要注释掉这行代码
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
3.常量提取出来。