How to Build an Extensible Multi‑Method Login System with Strategy & Factory Patterns in Spring Boot
This article walks through designing a flexible login module that supports password, WeChat, and SMS authentication by applying the Strategy and Factory patterns in a Spring Boot project, showing code examples, project structure, and best‑practice tips for clean, maintainable backend development.
Requirement Analysis
The login module must support multiple authentication methods (username/password, WeChat QR‑code, SMS verification) and allow new methods (e.g., Alipay) to be added without modifying existing code.
Username/Password : validate encrypted password, check if the account is locked.
WeChat QR‑code : exchange an auth code for an OpenID via the WeChat Open Platform API and verify the OpenID is bound to a system user.
SMS verification : generate a one‑time code, store it (e.g., in Redis), and validate its correctness and expiration.
Using a plain if‑else chain leads to poor extensibility, mixed responsibilities and duplicated logic.
Design Pattern Selection
Combine the Strategy pattern (encapsulate each login algorithm behind a common interface) with the Factory pattern (create the appropriate strategy instance based on the login type). This satisfies the Open‑Closed Principle.
Spring Boot Project Structure
src/main/java/com/example/login
├── config
│ └── StrategyConfig.java // optional bean name configuration
├── controller
│ └── LoginController.java // REST endpoint
├── factory
│ └── LoginStrategyFactory.java // strategy lookup
├── model
│ └── LoginRequest.java // request DTO
├── service
│ ├── impl
│ │ ├── PasswordLoginStrategy.java
│ │ ├── WechatLoginStrategy.java
│ │ └── SmsLoginStrategy.java
│ └── LoginStrategy.java // strategy interface
└── Application.javaStrategy Interface
public interface LoginStrategy {
/** Unique identifier of the login type, e.g. "password", "wechat" */
String getLoginType();
/** Execute the login logic. Parameters are passed in a map to keep the interface generic. */
String execute(Map<String, Object> params);
}Strategy Implementations
Password Login Strategy
@Service
public class PasswordLoginStrategy implements LoginStrategy {
@Override
public String getLoginType() { return "password"; }
@Override
public String execute(Map<String, Object> params) {
String username = (String) params.get("username");
String password = (String) params.get("password");
// TODO: replace with real DB query and password hash verification
if (!"123456".equals(password)) {
throw new IllegalArgumentException("Invalid password");
}
checkUserLocked(username);
return "Login successful (username/password)";
}
private void checkUserLocked(String username) {
// Placeholder for account‑lock check (e.g., call user service)
System.out.println("Check if user " + username + " is locked");
}
}WeChat QR‑code Strategy
@Service
public class WechatLoginStrategy implements LoginStrategy {
@Override
public String getLoginType() { return "wechat"; }
@Override
public String execute(Map<String, Object> params) {
String authCode = (String) params.get("authCode");
String openId = callWechatApi(authCode);
String userId = getUserIdByOpenId(openId);
if (userId == null) {
throw new IllegalArgumentException("WeChat account not bound to a system user");
}
return "Login successful (WeChat QR‑code)";
}
private String callWechatApi(String authCode) {
// Real implementation should invoke WeChat Open Platform API
System.out.println("Calling WeChat API with authCode=" + authCode);
return "wechat_open_id_123"; // mock OpenID
}
private String getUserIdByOpenId(String openId) {
// Mock DB lookup – replace with actual persistence query
return "user123";
}
}SMS Verification Strategy
@Service
public class SmsLoginStrategy implements LoginStrategy {
@Override
public String getLoginType() { return "sms"; }
@Override
public String execute(Map<String, Object> params) {
String phone = (String) params.get("phone");
String code = (String) params.get("code");
// TODO: retrieve the real code from Redis or another store
if (!"666888".equals(code)) {
throw new IllegalArgumentException("Invalid verification code");
}
checkPhoneRegistered(phone);
return "Login successful (SMS verification)";
}
private void checkPhoneRegistered(String phone) {
// Placeholder for phone‑registration check
System.out.println("Check if phone " + phone + " is registered");
}
}Factory Implementation
@Component
public class LoginStrategyFactory {
private final Map<String, LoginStrategy> strategyMap;
public LoginStrategyFactory(Map<String, LoginStrategy> beans) {
// Re‑map beans by the value returned from getLoginType()
this.strategyMap = new HashMap<>();
beans.forEach((beanName, strategy) ->
this.strategyMap.put(strategy.getLoginType(), strategy));
}
public LoginStrategy getStrategy(String loginType) {
LoginStrategy strategy = strategyMap.get(loginType);
if (strategy == null) {
throw new IllegalArgumentException("Unsupported login type: " + loginType);
}
return strategy;
}
}DTO for Login Requests
public class LoginRequest {
/** e.g. "password", "wechat", "sms" */
private String loginType;
/** Method‑specific parameters */
private Map<String, Object> params;
// getters and setters omitted for brevity
}Controller Integration
@RestController
@RequestMapping("/login")
public class LoginController {
private final LoginStrategyFactory factory;
@Autowired
public LoginController(LoginStrategyFactory factory) {
this.factory = factory;
}
@PostMapping
public String login(@RequestBody LoginRequest request) {
// Basic validation of request fields can be added here
LoginStrategy strategy = factory.getStrategy(request.getLoginType());
return strategy.execute(request.getParams());
}
}Testing the Three Login Types
Example JSON payloads sent to /login:
{
"loginType": "password",
"params": {"username": "user123", "password": "123456"}
} {
"loginType": "wechat",
"params": {"authCode": "wechat_auth_code_456"}
} {
"loginType": "sms",
"params": {"phone": "13800138000", "code": "666888"}
}Extending the Module
To add a new authentication method (e.g., Alipay), create a class AlipayLoginStrategy that implements LoginStrategy and annotate it with @Service (or define a bean in StrategyConfig). No changes are required in the factory or controller.
@Service
public class AlipayLoginStrategy implements LoginStrategy {
@Override
public String getLoginType() { return "alipay"; }
@Override
public String execute(Map<String, Object> params) {
// Implement Alipay specific login logic here
return "Login successful (Alipay)";
}
}Core Advantages
Strategy pattern : each login algorithm lives in its own class, improving readability and testability.
Open‑Closed Principle : new login types are added without touching existing code.
Factory pattern : hides creation details; callers only need the login type identifier.
Spring integration : automatic injection of all LoginStrategy beans via a Map<String, LoginStrategy> simplifies lifecycle management.
Best Practices
Parameter validation : perform required‑field checks in the controller to avoid duplicated validation in each strategy.
Extract common logic : if multiple strategies share steps (e.g., token generation), create an abstract base class that implements LoginStrategy and let concrete strategies extend it.
Logging and exception handling : wrap strategy execution with try‑catch, log failures, and translate technical exceptions into business‑level error messages.
Configuration‑driven login types : optionally list supported loginType values in application.yml so the front‑end can query the list instead of hard‑coding.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.
