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.

Programmer DD
Programmer DD
Programmer DD
Unified Multi-Channel Login with Spring Security: Password, SMS, and Mini‑App

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:8085

WeChat 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:8085

Abstract 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaAuthenticationCaptchaspring-securitymulti-channel loginWeChat MiniApp
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.