Master Single Sign-On (SSO) with SpringBoot, Vue & Uni‑App: A Hands‑On Guide
This article explains the concept, advantages, and implementation methods of Single Sign‑On (SSO) and provides two complete hands‑on examples—including architecture diagrams, database schema, configuration, and Java code for token‑based, ticket‑based, and RSA/AES encrypted SSO flows—using SpringBoot, Vue and Uni‑App.
Concept
Single Sign‑On (SSO) is an authentication service that lets a user log in once with a username/password and then access multiple systems (System A, B, C) without re‑entering credentials.
Traditional login requires separate credentials for each system, which is inconvenient and less secure.
SSO improves user experience and security by sharing authentication information across systems.
Advantages of SSO
User experience improvement: one login for multiple systems.
Enhanced security: centralized authentication reduces attack surface.
Simplified management: administrators manage users and permissions in a single place.
Implementation approaches
Shared authentication service.
Proxy authentication.
Token‑based authentication.
This tutorial references the open‑source mall project (SpringBoot + Vue + uni‑app) with 60K GitHub stars, containerized with Docker and supporting a full e‑commerce workflow.
Practical Example 1
Architecture
User logs into ServiceA with username/password.
User clicks a button to jump to ServiceB, sending the ticket issued by ServiceA.
ServiceB uses the ticket to request user info from ServiceA.
ServiceA validates the ticket and returns user info.
ServiceB generates a token and returns a redirect URL to ServiceA.
ServiceA uses the token to access ServiceB resources.
Database schema
<code>CREATE TABLE `sso_client_detail` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`platform_name` varchar(64) DEFAULT NULL COMMENT '应用名称',
`platform_id` varchar(64) NOT NULL COMMENT '应用标识',
`platform_secret` varchar(64) NOT NULL COMMENT '应用秘钥',
`encrypt_type` varchar(32) NOT NULL DEFAULT 'RSA' COMMENT '加密方式:AES或者RSA',
`public_key` varchar(1024) DEFAULT NULL COMMENT 'RSA加密的应用公钥',
`sso_url` varchar(128) DEFAULT NULL COMMENT '单点登录地址',
`remark` varchar(1024) DEFAULT NULL COMMENT '备注',
`create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
`update_date` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人',
`del_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '删除标志,0:正常;1:已删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='单点登陆信息表';
</code>Insert test data:
<code>INSERT INTO cheetah.sso_client_detail
(id, platform_name, platform_id, platform_secret, encrypt_type, public_key, sso_url, remark, create_date, create_by, update_date, update_by, del_flag)
VALUES (1, 'serviceA', 'A9mQUjun', 'Y6at4LexY5tguevJcKuaIioZ1vS3SDaULwOtXW63buBK4w2e1UEgrKmscjEq', 'RSA', NULL, 'http://127.0.0.1:8081/sso/url', NULL, '2023-05-23 16:55:26', 'system', '2023-05-30 13:16:16', NULL, 0);
</code>Fields
platform_idand
platform_secretare generated with
RandomStringUtils.randomAlphanumeric(). The
sso_urlpoints to the target service.
Code implementation
Service A – jump to Service B
<code>@PostMapping("/jumpB")
public WrapperResult<String> jump(@RequestBody @Validated SsoJumpReq req) {
log.debug("Single sign‑on: {}", req.getPlatformName());
SsoClientDetail one = iSsoClientDetailService.getOne(
new LambdaQueryWrapper<SsoClientDetail>()
.eq(SsoClientDetail::getPlatformName, req.getPlatformName()));
if (one == null) {
return WrapperResult.faild("App does not exist");
}
// generate ticket and store user info in Redis
String ticket = UUID.randomUUID().toString().replaceAll("-", "");
UserInfo userInfo = new UserInfo();
userInfo.setId(1L);
userInfo.setUsername("A_Q");
redisTemplate.opsForValue().set(RedisConstants.TICKET_PREFIX + ticket,
userInfo, 5, TimeUnit.MINUTES);
// call Service B with ticket
String ssoUrl = one.getSsoUrl();
Map<String, Object> data = new HashMap<>(1);
data.put("ticket", ticket);
WrapperResult<SsoRespDto> resp = HttpRequest.get(ssoUrl)
.queryMap(data)
.connectTimeout(Duration.ofSeconds(120))
.readTimeout(Duration.ofSeconds(120))
.execute()
.asValue(new TypeReference<WrapperResult<SsoRespDto>>() {});
return WrapperResult.success(resp.getData().getRedirectUrl());
}
</code>Service B – receive ticket and request user info from Service A
<code>@GetMapping("/url")
public WrapperResult<SsoRespDto> sso(@RequestParam("ticket") String ticket) throws JsonProcessingException {
log.info("Received ticket: {}", ticket);
Map<String, Object> param = new HashMap<>(1);
param.put("ticket", ticket);
String ssoUrl = "http://localhost:8081/getUser";
String response = HttpRequest.get(ssoUrl)
.queryMap(param)
.connectTimeout(Duration.ofSeconds(120))
.readTimeout(Duration.ofSeconds(120))
.execute()
.asString();
WrapperResult<SsoUserInfo> userInfo = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.readValue(response, new TypeReference<WrapperResult<SsoUserInfo>>() {});
// generate token and redirect URL
SsoRespDto dto = new SsoRespDto();
dto.setRedirectUrl("http://localhost:8082/index?token=123456");
return WrapperResult.success(dto);
}
</code>Service A – provide user info by ticket
<code>@GetMapping("/getUser")
public WrapperResult<SsoUserInfo> loginByTicket(@RequestParam("ticket") String ticket) {
UserInfo userInfo = (UserInfo) redisTemplate.opsForValue()
.get(RedisConstants.TICKET_PREFIX + ticket);
if (userInfo == null) {
return WrapperResult.faild("Invalid ticket");
}
SsoUserInfo ssoUserInfo = new SsoUserInfo();
BeanUtil.copyProperties(userInfo, ssoUserInfo);
return WrapperResult.success(ssoUserInfo);
}
</code>Practical Example 2
Architecture
In this flow Service B logs in first, encrypts user data, and Service A decrypts and validates the signature.
User logs into Service B.
User clicks a button to jump to Service A, sending encrypted user info.
Service A verifies the signature, decrypts the data, stores the user, and returns a token.
Service B uses the token to access Service A resources.
Configuration
Service B (application.yml) includes
appId,
appSecret, encryption type (RSA or AES), and RSA keys. Service A configuration contains the matching RSA private key.
Code snippets
Service B – redirect to Service A with encrypted data
<code>@GetMapping
public WrapperResult<String> redirectToServiceA() {
SsoUserInfo data = buildSsoUserInfo();
long timestamp = System.currentTimeMillis();
String flowId = UUID.randomUUID().toString();
String encryptType = configProperties.getEncryptType();
String dataEncrypt;
switch (encryptType) {
case "AES":
AES aes = new AES(configProperties.getAppSecret().getBytes(StandardCharsets.UTF_8));
dataEncrypt = aes.encryptBase64(JsonUtils.toString(data), StandardCharsets.UTF_8);
break;
case "RSA":
RSA rsa = new RSA(AsymmetricAlgorithm.RSA_ECB_PKCS1.getValue(),
null, configProperties.getServiceAPublicKey());
dataEncrypt = rsa.encryptBase64(JsonUtils.toString(data), StandardCharsets.UTF_8, KeyType.PublicKey);
break;
default:
return WrapperResult.faild("Encryption type not configured");
}
SsoSignSource signSource = SsoSignSource.builder()
.platformId(configProperties.getAppId())
.platformSecret(configProperties.getAppSecret())
.businessId("sso")
.data(dataEncrypt)
.flowId(flowId)
.timestamp(timestamp)
.build();
String sign = signSource.sign();
ToServiceAReq req = ToServiceAReq.builder()
.platformId(configProperties.getAppId())
.businessId("sso")
.flowId(flowId)
.timestamp(timestamp)
.sign(sign)
.data(dataEncrypt)
.build();
String result = HttpRequest.post("http://localhost:8081/serviceA")
.bodyString(JsonUtils.toString(req))
.execute()
.asString();
return WrapperResult.success(result);
}
</code>Service A – receive encrypted data and respond
<code>@PostMapping
public WrapperResult<SsoRespDto> sso(@VerifySign ToServiceAReq req) {
// Decrypt, sync user, generate token
String url = "127.0.0.1:8081/index?token=xxx";
SsoRespDto resp = new SsoRespDto();
resp.setRedirectUrl(url);
return WrapperResult.success(resp);
}
</code>Supplementary Knowledge
The RSA keys used in the examples were generated with the online tool https://www.bchrt.com/tools/rsa/ or can be created with Hutool’s RSA class or Java’s built‑in security APIs.
Project Source
Git repository: https://gitee.com/zhangxiaoQ/cheetah-sso-doublec
Further Learning
For a complete e‑commerce project (mall) with 60K GitHub stars, see the video tutorials (≈40 hours) covering the full Java stack, Docker deployment, micro‑service architecture, and more.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.