Backend Development 18 min read

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.

macrozheng
macrozheng
macrozheng
Master Single Sign-On (SSO) with SpringBoot, Vue & Uni‑App: A Hands‑On Guide

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.

SSO concept diagram
SSO concept diagram

Traditional login requires separate credentials for each system, which is inconvenient and less secure.

Traditional login diagram
Traditional login diagram

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

Example 1 architecture diagram
Example 1 architecture diagram

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_id

and

platform_secret

are generated with

RandomStringUtils.randomAlphanumeric()

. The

sso_url

points 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

Example 2 architecture diagram
Example 2 architecture diagram

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.

JavamicroservicesAuthenticationSpringBootSSO
macrozheng
Written by

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.

0 followers
Reader feedback

How this landed with the community

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