Unified Multi-Channel Login with Spring Security: Password, SMS, and Mini‑App
This article demonstrates how to implement a unified authentication system in Spring Security that supports traditional username/password login, SMS-based captcha login, and WeChat Mini‑Program login, detailing the required components, custom filters, provider configurations, and code examples for a production‑ready solution.
Product managers often require multiple login methods—username/password, SMS verification, and WeChat Mini‑Program—in a single application. This guide shows how to achieve all three using Spring Security.
Overall Principle
The core of Spring Security authentication involves three parts: AbstractAuthenticationProcessingFilter, AuthenticationProvider, and Authentication. The diagram below illustrates the basic flow.
ChannelUserDetailsService
Only one UserDetailsService can be injected into Spring IoC, so we extend it to support both phone‑based and OpenID‑based lookups:
public interface ChannelUserDetailsService extends UserDetailsService {
/**
* Load user by phone for captcha login.
*/
UserDetails loadByPhone(String phone);
/**
* Load user by OpenID for Mini‑App login.
*/
UserDetails loadByOpenId(String openId);
}Captcha Login
The captcha login endpoint is /login/captcha. It requires a CaptchaService to send and verify codes:
public interface CaptchaService {
/** Send a captcha code to the given phone. */
boolean sendCaptchaCode(String phone);
/** Verify the received captcha against the cached value. */
boolean verifyCaptchaCode(String phone, String captcha);
}Typical request:
POST /login/captcha?phone=182****0032&captcha=596001 HTTP/1.1
Host: localhost:8085WeChat Mini‑Program Login
The Mini‑Program sends clientId (identifying the app) and jsCode. A functional interface supplies the corresponding app configuration:
@FunctionalInterface
public interface MiniAppClientService {
MiniAppClient get(String clientId);
}The filter exchanges jsCode for openid and session_key via WeChat's code2session API, then loads the user by OpenID and caches the session key:
public class MiniAppAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {
private static final String ENDPOINT = "https://api.weixin.qq.com/sns/jscode2session";
// ... constructor and fields omitted for brevity ...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MiniAppAuthenticationToken token = (MiniAppAuthenticationToken) authentication;
String clientId = token.getName();
String jsCode = (String) token.getCredentials();
ObjectNode response = getResponse(miniAppClientService.get(clientId), jsCode);
String openId = response.get("openid").asText();
String sessionKey = response.get("session_key").asText();
UserDetails user = channelUserDetailsService.loadByOpenId(openId);
miniAppSessionKeyCache.put(user.getUsername(), sessionKey);
return createSuccessAuthentication(authentication, user);
}
// ... other methods omitted ...
}Typical request:
POST /login/miniapp?clientId=wx12342&code=asdfasdfasdfasdfsd HTTP/1.1
Host: localhost:8085Abstract Channel Filter
To avoid configuring separate filters for each channel, an abstract filter extracts the channel identifier from the request URL:
public abstract class AbstractChannelAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
protected AbstractChannelAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
super(requiresAuthenticationRequestMatcher);
}
/** Return the channel name (e.g., "captcha" or "miniapp"). */
protected abstract String channel();
}Mini‑App Filter Implementation
public class MiniAppAuthenticationFilter extends AbstractChannelAuthenticationProcessingFilter {
private static final String CHANNEL_ID = "miniapp";
private static final String SPRING_SECURITY_FORM_MINI_CLIENT_KEY = "clientId";
private static final String SPRING_SECURITY_FORM_JS_CODE_KEY = "jsCode";
public MiniAppAuthenticationFilter() {
super(new AntPathRequestMatcher("/login/" + CHANNEL_ID, "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals(HttpMethod.POST.name())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String clientId = obtainClientId(request);
String jsCode = obtainJsCode(request);
MiniAppAuthenticationToken authRequest = new MiniAppAuthenticationToken(clientId, jsCode);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Override
public String channel() { return CHANNEL_ID; }
protected String obtainClientId(HttpServletRequest request) {
String clientId = request.getParameter(SPRING_SECURITY_FORM_MINI_CLIENT_KEY);
if (!StringUtils.hasText(clientId)) {
throw new IllegalArgumentException("clientId is required");
}
return clientId.trim();
}
protected String obtainJsCode(HttpServletRequest request) {
String jsCode = request.getParameter(SPRING_SECURITY_FORM_JS_CODE_KEY);
if (!StringUtils.hasText(jsCode)) {
throw new IllegalArgumentException("js_code is required");
}
return jsCode.trim();
}
protected void setDetails(HttpServletRequest request, MiniAppAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
}Channel Aggregation Filter
public class ChannelAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String CHANNEL_URI_VARIABLE_NAME = "channel";
private static final RequestMatcher LOGIN_REQUEST_MATCHER =
new AntPathRequestMatcher("/login/{" + CHANNEL_URI_VARIABLE_NAME + "}", "POST");
private final List<? extends AbstractChannelAuthenticationProcessingFilter> channelFilters;
public ChannelAuthenticationFilter(List<? extends AbstractChannelAuthenticationProcessingFilter> channelFilters) {
super(LOGIN_REQUEST_MATCHER);
this.channelFilters = CollectionUtils.isEmpty(channelFilters) ? Collections.emptyList() : channelFilters;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
String channel = LOGIN_REQUEST_MATCHER.matcher(request).getVariables().get(CHANNEL_URI_VARIABLE_NAME);
for (AbstractChannelAuthenticationProcessingFilter filter : channelFilters) {
if (Objects.equals(channel, filter.channel())) {
return filter.attemptAuthentication(request, response);
}
}
throw new ProviderNotFoundException("No Suitable Provider");
}
}Spring Configuration
@Configuration(proxyBeanMethods = false)
public class ChannelAuthenticationConfiguration {
@Bean
@ConditionalOnBean({ChannelUserDetailsService.class, CaptchaService.class, JwtTokenGenerator.class})
public CaptchaAuthenticationFilter captchaAuthenticationFilter(ChannelUserDetailsService uds,
CaptchaService cs,
JwtTokenGenerator jwt) {
CaptchaAuthenticationProvider provider = new CaptchaAuthenticationProvider(uds, cs);
CaptchaAuthenticationFilter filter = new CaptchaAuthenticationFilter();
filter.setAuthenticationManager(new ProviderManager(Collections.singletonList(provider)));
filter.setAuthenticationSuccessHandler(new LoginAuthenticationSuccessHandler(jwt));
filter.setAuthenticationFailureHandler(new AuthenticationEntryPointFailureHandler(new SimpleAuthenticationEntryPoint()));
return filter;
}
@Bean
@ConditionalOnBean({ChannelUserDetailsService.class, MiniAppClientService.class, MiniAppSessionKeyCache.class, JwtTokenGenerator.class})
public MiniAppAuthenticationFilter miniAppAuthenticationFilter(MiniAppClientService clientService,
ChannelUserDetailsService uds,
MiniAppSessionKeyCache cache,
JwtTokenGenerator jwt) {
MiniAppAuthenticationFilter filter = new MiniAppAuthenticationFilter();
MiniAppAuthenticationProvider provider = new MiniAppAuthenticationProvider(clientService, uds, cache);
filter.setAuthenticationManager(new ProviderManager(Collections.singletonList(provider)));
filter.setAuthenticationSuccessHandler(new LoginAuthenticationSuccessHandler(jwt));
filter.setAuthenticationFailureHandler(new AuthenticationEntryPointFailureHandler(new SimpleAuthenticationEntryPoint()));
return filter;
}
@Bean
public ChannelAuthenticationFilter channelAuthenticationFilter(List<? extends AbstractChannelAuthenticationProcessingFilter> filters) {
return new ChannelAuthenticationFilter(filters);
}
}Conclusion
By wiring the ChannelAuthenticationFilter into HttpSecurity, the application now supports three distinct login channels—standard username/password, SMS captcha, and WeChat Mini‑Program—using a single, extensible authentication pipeline. The complete source code is open‑source; follow the author "码农小胖哥" and reply with channellogin to obtain production‑grade examples.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
