比如我们有的业务场景需要走outh2 或者短信验证码登录 下面以短信验证码为例
首先梳理默认的登录流程
1.http.formLogin() 会在WebSecurityConfigurerAdapter创建一个FormLoginConfigurer
2.WebSecurityConfigurerAdapter是WebSecurity的confugures 详见源码:
spring-security源码-初始化(九)
搜索"5."
3.WebSecurity的build方法会触发WebSecurityConfigurerAdapter 的int和configure
spring-security源码-初始化(九)
搜索"6."
4,WebSecurityConfigurerAdapter的build 就会遍历内部的init confgures 则对应FormLoginConfigurer 会添加一个
UsernamePasswordAuthenticationFilter
执行登录处理 参考
Spring-Security源码-Filter之UsernamePasswordAuthenticationFilter(十四)
5.
UsernamePasswordAuthenticationFilter
继承AbstractAuthenticationProcessingFilter
UsernamePasswordAuthenticationFilter
会负责解析入参封装成token交给AuthenticationManager处理
AuthenticationManager内部会匹配当前类型的AuthenticationProvider Token的处理器
Filter处理详见源码
<2>
AuthenticationManager初始化处参考
<8>
AuthenticationManager处理参考
<跳转>
因为
UsernamePasswordAuthenticationFilter
和FormLoginConfigurer只适合用户名和密码这种形式,所以验证码打算从Configure 到Fitler 到AuthenticationManager 的AuthenticationProvider整套自定义
1.定义一个验证码的Token 参考UsernamePasswordAuthenticationToken实现
ublic class MessageAuthenticationToken extends AbstractAuthenticationToken {
private String messageCode;
private String phoneNumber;
private Object principal;
private Object credentials;
public MessageAuthenticationToken(String messageCode,String phoneNumber) {
super(null);
this.messageCode=messageCode;
this.phoneNumber=phoneNumber;
super.setAuthenticated(true); // must use super, as we override
* The credentials that prove the principal is correct. This is usually a password,
* but could be anything relevant to the <code>AuthenticationManager</code>. Callers
* are expected to populate the credentials.
* @return the credentials that prove the identity of the <code>Principal</code>
@Override
public Object getCredentials() {
return credentials;
* The identity of the principal being authenticated. In the case of an authentication
* request with username and password, this would be the username. Callers are
* expected to populate the principal for an authentication request.
* The <tt>AuthenticationManager</tt> implementation will often return an
* <tt>Authentication</tt> containing richer information as the principal for use by
* the application. Many of the authentication providers will create a
* {@code UserDetails} object as the principal.
* @return the <code>Principal</code> being authenticated or the authenticated
* principal after authentication.
@Override
public Object getPrincipal() {
return principal;
public void setCredentials(Object credentials) {
this.credentials = credentials;
public void setPrincipal(Object principal) {
this.principal = principal;
public String getMessageCode() {
return messageCode;
public void setMessageCode(String messageCode) {
this.messageCode = messageCode;
public String getPhoneNumber() {
return phoneNumber;
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
2.自定义验证码登录请求Filter
@Order(1600)
public class MessageAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//默认的登录认证请求 登录commit
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");
public MessageAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
* 允许调用方改变
* @param loginProcessingUrl
public MessageAuthenticationFilter(String loginProcessingUrl ) {
super(new AntPathRequestMatcher(loginProcessingUrl,
"POST"));
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//获取验证码
String messageCode = obtainMessageCode(request);
messageCode = (messageCode != null) ? messageCode : "";
messageCode = messageCode.trim();
//获取手机号
String phoneNumber = obtainPhoneNumber(request);
phoneNumber = (phoneNumber != null) ? phoneNumber : "";
//验证码登录的token
MessageAuthenticationToken authRequest = new MessageAuthenticationToken(messageCode, phoneNumber);
return this.getAuthenticationManager().authenticate(authRequest);
protected String obtainMessageCode(HttpServletRequest request) {
return request.getParameter("messageCode");
protected String obtainPhoneNumber(HttpServletRequest request) {
return request.getParameter("phoneNumber");
3.自定义一个UserDetail主要根据手机号获取用户信息
public class MessageCodeUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//模拟查库
MyUserDetail userDetail= new MyUserDetail();
userDetail.setUsername("liqiang");
userDetail.setPassword("liqiang");
return userDetail;
3.自定义AuthenticationManager的AuthenticationProvider 处理器
public class MessageAuthenticationProvider implements AuthenticationProvider {
public static Map<String,String> redisCache=new HashMap<>();
private UserDetailsService userDetailsService;
public MessageAuthenticationProvider(UserDetailsService userDetailsService){
this.userDetailsService=userDetailsService;
static {
redisCache.put("13128273410","1111");
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String code= getMessageCode(authentication);
if(!StringUtils.hasLength(code)){
throw new BadCredentialsException("验证码已失效");
MessageAuthenticationToken messageAuthenticationToken=(MessageAuthenticationToken)authentication;
if(!messageAuthenticationToken.getMessageCode().equals(code)){
throw new BadCredentialsException("验证码错误");
UserDetails userDetails= userDetailsService.loadUserByUsername(((MessageAuthenticationToken) authentication).getPhoneNumber());
messageAuthenticationToken.setCredentials(userDetails.getAuthorities());
messageAuthenticationToken.setPrincipal(userDetails.getUsername());
return messageAuthenticationToken;
private String getMessageCode(Authentication authentication) {
MessageAuthenticationToken messageAuthenticationToken=(MessageAuthenticationToken)authentication;
return redisCache.get(messageAuthenticationToken.getPhoneNumber());
* 匹配token处理器
* @param authentication
* @return
@Override
public boolean supports(Class<?> authentication) {
return (MessageAuthenticationToken.class.isAssignableFrom(authentication));
4.自定义一个Configure替换默认的
public class MessageLoginConfig<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<MessageLoginConfig<H>, H>{
//登录页面 用于登录失败 跳回的登录页面
private String loginPage;
//登录请求处理页面
private String loginProcessingUrl;
//登录成功跳转页面
private String defaultSuccessUrl;
public MessageLoginConfig<H> loginPage(String loginPage) {
this.loginPage = loginPage;
return this;
public MessageLoginConfig<H> loginProcessingUrl(String loginProcessUrl) {
this.loginProcessingUrl=loginProcessUrl;
return this;
public MessageLoginConfig<H> defaultSuccessUrl(String defaultSuccessUrl) {
this.defaultSuccessUrl = defaultSuccessUrl;
return this;
private MessageAuthenticationFilter authFilter;
@Override
public void init(H builder) throws Exception {
//初始化一个Filter
if(loginProcessingUrl==null) {
authFilter = new MessageAuthenticationFilter();
}else{
authFilter = new MessageAuthenticationFilter(loginProcessingUrl);
//登录验证成功的处理器
authFilter.setAuthenticationSuccessHandler(new MessageCodeAuthenticationSuccessHandler(defaultSuccessUrl));
//登录验证失败的处理器
authFilter.setAuthenticationFailureHandler(new MessageCodeAuthenticationFailureHandler(loginPage));
//登录认证失败的处理器 未登录访问需要登录的页面 的异常处理器参考<>
registerAuthenticationEntryPoint(builder, new AuthenticationEntryPoint() {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private PortResolver portResolver = new PortResolverImpl();
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
this.redirectStrategy.sendRedirect(request, response, buildRedirectUrlToLoginPage(request,response,loginPage));
protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response,
String url) {
if (UrlUtils.isAbsoluteUrl(url)) {
return url;
int serverPort = this.portResolver.getServerPort(request);
String scheme = request.getScheme();
RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
urlBuilder.setScheme(scheme);
urlBuilder.setServerName(request.getServerName());
urlBuilder.setPort(serverPort);
urlBuilder.setContextPath(request.getContextPath());
urlBuilder.setPathInfo(url);
return urlBuilder.getUrl();
@Override
public void configure(H builder) throws Exception {
//替换默认的UsernamePasswordAuthenticationFilter
builder.addFilterAfter(authFilter, UsernamePasswordAuthenticationFilter.class);
//设置AuthenticationManager 用于登录认证
this.authFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
@SuppressWarnings("unchecked")
protected final void registerAuthenticationEntryPoint(H http, AuthenticationEntryPoint authenticationEntryPoint) {
ExceptionHandlingConfigurer<H> exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);
if (exceptionHandling == null) {
return;
exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint),
getAuthenticationEntryPointMatcher(http));
protected final RequestMatcher getAuthenticationEntryPointMatcher(H http) {
ContentNegotiationStrategy contentNegotiationStrategy = http.getSharedObject(ContentNegotiationStrategy.class);
if (contentNegotiationStrategy == null) {
contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
MediaTypeRequestMatcher mediaMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy,
MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML,
MediaType.TEXT_PLAIN);
mediaMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
RequestMatcher notXRequestedWith = new NegatedRequestMatcher(
new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
return new AndRequestMatcher(Arrays.asList(notXRequestedWith, mediaMatcher));
class MessageCodeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private PortResolver portResolver = new PortResolverImpl();
public MessageCodeAuthenticationSuccessHandler(String url){
this.url=url;
private String url;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
this.redirectStrategy.sendRedirect(request, response,buildRedirectUrlToLoginPage(request,response,url));
protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response,
String url) {
if (UrlUtils.isAbsoluteUrl(url)) {
return url;
int serverPort = this.portResolver.getServerPort(request);
String scheme = request.getScheme();
RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
urlBuilder.setScheme(scheme);
urlBuilder.setServerName(request.getServerName());
urlBuilder.setPort(serverPort);
urlBuilder.setContextPath(request.getContextPath());
urlBuilder.setPathInfo(url);
return urlBuilder.getUrl();
class MessageCodeAuthenticationFailureHandler implements AuthenticationFailureHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private PortResolver portResolver = new PortResolverImpl();
public MessageCodeAuthenticationFailureHandler(String url){
this.url=url;
private String url;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
this.redirectStrategy.sendRedirect(request, response, buildRedirectUrlToLoginPage(request,response,url));
protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response,
String url) {
if (UrlUtils.isAbsoluteUrl(url)) {
return url;
int serverPort = this.portResolver.getServerPort(request);
String scheme = request.getScheme();
RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
urlBuilder.setScheme(scheme);
urlBuilder.setServerName(request.getServerName());
urlBuilder.setPort(serverPort);
urlBuilder.setContextPath(request.getContextPath());
urlBuilder.setPathInfo(url);
return urlBuilder.getUrl();
WebAdapter配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
* 对于不需要授权的静态文件放行
* @param web
* @throws Exception
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
* 对密码进行加密的实例
* @return
@Bean
PasswordEncoder passwordEncoder() {
* 不加密所以使用NoOpPasswordEncoder
* 更多可以参考PasswordEncoder 的默认实现官方推荐使用: BCryptPasswordEncoder,BCryptPasswordEncoder
return NoOpPasswordEncoder.getInstance();
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
MessageCodeUserDetailService messageCodeUserDetailService=new MessageCodeUserDetailService();
* 多个用户通过and隔开
* 添加自定义的messageCodeUserDetailService
auth.userDetailsService(messageCodeUserDetailService)
.and()
//添加自定义的MessageAuthenticationProvider
.authenticationProvider(new MessageAuthenticationProvider(messageCodeUserDetailService));
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.authorizeRequests()
// .anyRequest()
// .authenticated()
// .and().rememberMe()//记住登录
// .tokenRepository(new InMemoryTokenRepositoryImpl())
// .and()
// .formLogin()// rm表单的方式
// .loginPage("/login")//登录页面路径
// .loginProcessingUrl("/doLogin")
// //自定义登录请求地址
// .defaultSuccessUrl("/hello")
// .usernameParameter("loginName")
// .passwordParameter("loginPassword")
// .permitAll(true)//不拦截
// .and()
// .csrf()//记得关闭
// .disable()
// .sessionManagement().
// maximumSessions(1)
// .maxSessionsPreventsLogin(true);
// ("/login","doLogin")主要是在最后一个过滤器校验是否登录和授权处增加一个Permiall的Attribute 实现登录页面和doLogin的放心 参考<>
http.authorizeRequests().antMatchers("/login","/doLogin").permitAll()
.anyRequest()
.authenticated()
.and().rememberMe()//记住登录
.tokenRepository(new InMemoryTokenRepositoryImpl())
.and()
//使用自定义的Configure
.apply(new MessageLoginConfig<HttpSecurity>())
.loginPage("/login")//登录页面路径
.loginProcessingUrl("/doLogin")
//自定义登录请求地址
.defaultSuccessUrl("/hello")
.and()
.csrf()//记得关闭
.disable()
.sessionManagement().
maximumSessions(1)
.maxSessionsPreventsLogin(true);