Implementation of Single Sign-On (SSO) with Service A and Service B Using Ticket and Token Mechanisms
This article explains the concept, advantages, and three implementation methods of Single Sign-On (SSO), then provides two complete practical examples—including architecture diagrams, step‑by‑step flows, and full Java code for ticket‑based and encrypted data‑based SSO—followed by supplementary RSA key generation notes and a brief promotional note.
Concept
Single Sign-On (SSO) is an authentication service that allows a user to log in once with a single set of credentials and then access multiple applications or systems (e.g., System A, System B, System C) without re‑entering username/password.
In traditional login, users must provide separate credentials for each system, which is inconvenient and vulnerable. SSO solves this by authenticating once and sharing the authentication token across systems, improving user experience and security.
Advantages of Single Sign-On
User experience improvement: one login grants access to all systems, greatly increasing efficiency.
Enhanced security: a single authentication point reduces the attack surface and prevents credential reuse.
Simplified management: administrators can manage users and permissions centrally.
Implementation Methods
Shared authentication: multiple systems rely on a common authentication server.
Proxy authentication: one system authenticates on behalf of others.
Token‑based authentication: after login, a token is issued and used by all systems.
Practical Example 1 – Ticket Based SSO (Service A → Service B)
Architecture Diagram
Process Flow
User logs into Service A with username/password.
User clicks a button in Service A to jump to Service B; Service A attaches a ticket.
Service B uses the ticket to request user information from Service A.
Service A validates the ticket and returns user data.
Service B generates a token, attaches a redirect URL, and returns it to Service A.
Service A uses the token to access Service B resources.
Code Implementation – Database
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 AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='单点登陆信息表';Insert Test Data
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);Service A – Jump to Service B (Controller)
/**
* com.itaq.cheetah.serviceA.controller.PortalController#jump
* title:跳转 ServiceB
*
* 1. 前端点击Jump链接触发此接口调用
* 2. 此接口生成ticket并携带着请求 ServiceB
* 2.1 ServiceB拿着ticket请求我方服务获取用户信息
* 2.2 ServiceB获取到我方用户信息并进行数据同步
* 2.3 ServiceB返回一个链接,连接中带 token
* 3. 重定向到返回的链接实现登录
*
*/
@PostMapping("/jumpB")
public WrapperResult
jump(@RequestBody @Validated SsoJumpReq req) {
log.debug("单点登录:{}", req.getPlatformName());
// 1. 判断平台是否存在
SsoClientDetail one = iSsoClientDetailService.getOne(new LambdaQueryWrapper
().eq(SsoClientDetail::getPlatformName, req.getPlatformName()));
if (Objects.isNull(one)) {
return WrapperResult.faild("不存在的app");
}
// 2. 校验本系统的 token(示例代码略)
// 3. 生成 ticket 并存入 Redis
String ticket = UUID.randomUUID().toString().replaceAll("-", "");
UserInfo userInfo = new UserInfo();
userInfo.setId(1L);
userInfo.setUsername("阿Q");
redisTemplate.opsForValue().set(RedisConstants.TICKET_PREFIX + ticket, userInfo, 5, TimeUnit.MINUTES);
// 4. 调用 ServiceB 的 SSO 接口
String ssoUrl = one.getSsoUrl();
Map
data = new HashMap<>(1);
data.put("ticket", ticket);
WrapperResult
ssoRespDto = HttpRequest.get(ssoUrl)
.queryMap(data)
.connectTimeout(Duration.ofSeconds(120))
.readTimeout(Duration.ofSeconds(120))
.execute()
.asValue(new TypeReference
>() {});
log.info("请求ServiceB 结果:{}", JsonUtils.toPrettyString(ssoRespDto));
return WrapperResult.success(ssoRespDto.getData().getRedirectUrl());
}Service B – Receive Ticket and Request User Info
/**
* com.itaq.cheetah.serviceB.controller.SsoController#sso
* 获取票据,并请求 ServiceA 获取用户信息
*/
@GetMapping("/url")
public WrapperResult
sso(@RequestParam("ticket") String ticket) throws JsonProcessingException {
log.info("收到票据:{}", ticket);
// 1. 根据 ticket 换取 ServiceA 用户信息
Map
param = new HashMap<>(1);
param.put("ticket", ticket);
String ssoUrl = "http://localhost:8081/getUser";
String s = HttpRequest.get(ssoUrl).queryMap(param).connectTimeout(Duration.ofSeconds(120)).readTimeout(Duration.ofSeconds(120)).execute().asString();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
WrapperResult
ssoUserInfoWrapperResult = objectMapper.readValue(s, new TypeReference
>() {});
log.info("ticket登录结果:{}", new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(ssoUserInfoWrapperResult));
// 2. 同步用户信息到本地(略)
// 3. 生成 token 并返回重定向 URL
SsoRespDto respDto = new SsoRespDto();
respDto.setRedirectUrl("http://localhost:8082/index?token=123456");
WrapperResult
success = WrapperResult.success(respDto);
log.info(new ObjectMapper().writeValueAsString(success));
return success;
}Service A – Provide User Info by Ticket
@ApiOperation("根据ticket获取用户信息")
@GetMapping("/getUser")
public WrapperResult
loginByTicket(@RequestParam("ticket") String ticket) {
log.info("收到票据:{}", ticket);
UserInfo userInfo = (UserInfo) redisTemplate.opsForValue().get(RedisConstants.TICKET_PREFIX + ticket);
if (Objects.isNull(userInfo)) {
return WrapperResult.faild("无法识别的票据信息");
}
SsoUserInfo ssoUserInfo = new SsoUserInfo();
BeanUtil.copyProperties(userInfo, ssoUserInfo);
return WrapperResult.success(ssoUserInfo);
}Practical Example 2 – Encrypted Data SSO (Service B → Service A)
Architecture Diagram
Process Flow
User logs into Service B with username/password.
User clicks a button in Service B to jump to Service A; Service B encrypts user data and sends it.
Service A verifies the signature, decrypts the data, and saves the user.
Service A generates a token and returns it to Service B.
Service B uses the token to access Service A resources.
Configuration – Service B (application.yml excerpt)
# 本服务的 appId 与 appSecret,由 Service A 提供
appId: A9mQUjun
appSecret: Y6at4LexY5tguevJcKuaIioZ1vS3SDaULwOtXW63buBK4w2e1UEgrKmscjEq
encrypt:
# 加密方式 RSA | AES
type: RSA
rsa:
publicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0KLYE2Tv4qx/duxu8Qvq5ZN58yEjj/uwsxfs96pj+9iOOAUKLur8IIKjR/bi54GICUy0BHO6dzpWc0xqGK170F9NTv0bHe0qbh7jHgzq9MJrfcVD+XZAH17ho5tCGIo+z7CiC+rMWGTqmRopd/EQuzfx4Op4/85hoPlpKxdcxAfys0jpZ9tBMtROPsYKhCz01iDnHV2K95s4UwaQLbbx0VALVaXv1/4Yjw/PW4xK0syW/nqUtVqpfwPuX+fHf+bJ2s4kLnFBNwYAKFSU6znGmtJuq6aoxCunu2PbzI8xc7SYxHEfDqG8Zp29wtZcTJecWSDMBmywlaXjkXLzapvE7QIDAQAB
privateKey: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQotgTZO/irH927G7xC+rlk3nzISOP+7CzF+z3qmP72I44BQou6vwggqNH9uLngYgJTLQEc7p3OlZzTGoYrXvQX01O/Rsd7SpuHuMeDOr0wmt9xUP5dkAfXuGjm0IYij7PsKIL6sxYZOqZGil38RC7N/Hg6nj/zmGg+WkrF1zEB/KzSOln20Ey1E4+xgqELPTWIOcdXYr3mzhTBpAttvHRUAtVpe/X/hiPD89bjErSzJb+epS1Wql/A+5f58d/5snaziQucUE3BgAoVJTrOcaa0m6rpqjEK6e7Y9vMjzFztJjEcR8Oobxmnb3C1lxMl5xZIMwGbLCVpeORcvNqm8TtAgMBAAECggEBAKMhoQfRFYxMSkIHbluFcP5eyKylDbRoHOp726pvDUx/L/x3XFYBIHCfFOKRFSvk6SQ0WFFe176f27a9Wfu/sh7kVYNcflZw+YsvFXCKsy/70KZ/lr24izy8KHuPSyf6+E/WkW32Ah9fkNtzTFdfIzDv9m1hiIijq0x9l5C87KjNELnbvC0I6vwFOx0ak+JBbpaJ7IRjZxKZup7UIPvt9nbLzcbKelI83An2JUe8HNhrfWxH9UIyMOBoAY+bKCuAbUtHqSlImPiWyiCwE2/Fh7dmPSOAYYp9aZelnhd25jlR+eh4yaUoIID9ubmYVYbjcPW5SSNdfSZMfQ3oa79QeRUCgYEA6K4L+VLRiX8Dg7NCO1fM2+FTv2csTkPX6n7z/uu7kh0+wQDws+/C6Q906OtizvJBIJqFm2jPACNQCvnRixY1srgMJJlH/Rpeb4LtZGwdM1k0jAZIYQcBlGfaq3RaRI/+6+T0xdsh+7VF5A/smp/VXdK2xI3+JbLQ2wm9uN+3yZcCgYEA5Yvly7veDJYf2+8HIQkRhjWrWm1y5lCSe+HG+1ktfqnhN8YEOiPa71u0TXealL0T8EoKsqhWEjomxZ7n0jLigogz7OxxsGAE6HXAiKX0REINNYrq+1qNaqmkfLrhAJyg3JNgTSlb0xd56w7FSqOBttVL9INawGb1P98kYc5OzhsCgYBEfIY1urTGPcZxC2BhSzSXO7mEyv91ge6ZrQhwbj5lgYopEPfIXrgGFXCZ5j7NHu0ghZrx5WWYasxyjpmo0L65fgbE9wEDdLF7LRRmzJPDu2wGEwtW09MZNYBdmv++0ot8L4YEfr1/8xlBSZag5I7O8Oiu7gRyYDGtZy6are7QvQKBgQCaUZnUhOF7/rU+a4yUZf9VBeHD8k7LjaFdDWVzdvmB7P1PPJ185Lv8LN+jMORIWHD+GxjkEQ2ERXnpY7If+zuSW7Tk8/Reib7i9L7SXxc/iFRPCax9/NuTuKavgAdiHOp8P8v/M+3alS7OmuiCDDhZTT46DNDHBrCcFwzjgAo0vwKBgECBs6hEUVsYU74Uc64he8Zgkvj7wZ/yxnFlWmRRERprfBsuiY/y+DAf5ehezSRFpHXUrAkpeVXq2ydnr9BKTs6TV3AxlDMBNSndXsUYHENncR7tEHCSGRFTTu5jxdYA+k47R865Jh+2vQvPaPaXsEKSkDegvcFeUVR/yi5AsDubService B – Redirect to Service A with Encrypted User Info
/**
* com.itaq.cheetah.serviceB.controller.ToServiceAController#redirectToServiceA
* 跳转 ServiceA 服务
*/
@GetMapping
public WrapperResult
redirectToServiceA() {
// 1. Build user info
SsoUserInfo data = buildSsoUserInfo();
Long timestamp = System.currentTimeMillis();
String flowId = UUID.randomUUID().toString();
String businessId = "sso";
String dataEncrypt;
String encryptType = configProperties.getEncryptType();
// 2. Encrypt according to configuration
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("未配置加密方式");
}
// 3. Sign the request
SsoSignSource build = SsoSignSource.builder()
.platformId(configProperties.getAppId())
.platformSecret(configProperties.getAppSecret())
.businessId(businessId)
.data(dataEncrypt)
.flowId(flowId)
.timestamp(timestamp)
.build();
String sign = build.sign();
// 4. Build request body
ToServiceAReq req = ToServiceAReq.builder()
.platformId(configProperties.getAppId())
.businessId("sso")
.flowId(flowId)
.timestamp(timestamp)
.sign(sign)
.data(dataEncrypt)
.build();
// 5. Call Service A
String s = HttpRequest.post("http://localhost:8081/serviceA")
.bodyString(JsonUtils.toString(req))
.execute()
.asString();
log.info("结果:{}", s);
return WrapperResult.success(s);
}Service A – Receive Encrypted Data, Verify Signature, and Respond
@PostMapping
public WrapperResult
sso(@VerifySign ToServiceAReq req) {
log.info("收到单点登录ServiceA的请求:{}", JsonUtils.toPrettyString(req));
// 1. Verify signature and decrypt data (handled by @VerifySign annotation)
// 2. Synchronize user information (omitted)
// 3. Simulate login and generate token
String url = "127.0.0.1:8081/index?token=xxx";
SsoRespDto ssoRespDto = new SsoRespDto();
ssoRespDto.setRedirectUrl(url);
return WrapperResult.success(ssoRespDto);
}Supplementary Knowledge
The RSA keys used in this article were generated with the online tool https://www.bchrt.com/tools/rsa/ ; you can also generate them using Hutool's RSA class or Java's built‑in security APIs.
Source code: Follow the public account "码猿技术专栏" and reply with the keyword "sso" to obtain the full source.
Final Note (Please Follow)
If this article helped you, please like, view, share, and bookmark – your support keeps the author motivated!
For more premium content, join the author's Knowledge Planet for 199 CNY, which includes extensive practical projects, Spring full‑stack series, massive data sharding, DDD micro‑service tutorials, and more. Contact via WeChat: special_coder .
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.