
作者:小傅哥 博客:https://bugstack.cn
❝沉淀、分享、成长,让自己和他人都能有所收获!😜 ❞
大家好,我是技术UP主小傅哥。
当你进入一个较大一些的中大厂互联网公司以后,你会发现自己参与的业务系统开发,好像从来没有关心过关于用户的身份鉴权,而是直接拿到用户的ID就做业务了。那这里的鉴权跑到哪里去了呢?🤔
其实在公司里是一套统一的授权服务和组件的,并且维护用户的ID、用户的关联绑定也都是这套系统来处理的。之后这套系统会和 API 网关进行对接,等网关下发到你的后端服务系统时,在内部微服间流转就是真实的用户ID啦。
那么为了让伙伴们更好的理解关于 OAuth2 SSO 统一单点登录的前后端分离服务,小傅哥这里做了一个结合 Spring Security OAuth2很容易理解案例工程。学习后就可以扩展使用 SSO 到你自己的系统了,比如可以做一个统一的用户鉴权中心。
单点登录(Single Sign-On,SSO)是一种认证技术,用户只需进行一次身份验证,就可以访问多个相互信任的应用系统,而无需再次输入凭证。SSO的主要目的是简化用户的登录过程,提高用户体验和安全性,同时减少管理多个用户名和密码的复杂性。

SSO的工作原理通常涉及以下几个步骤:
SSO的优点包括:
192.168.1.107 sso.xfg.com
192.168.1.107 client1.xfg.com
192.168.1.107 client2.xfg.com

server:
port: 8091
application:
name: xfg-dev-tech-sso
servlet:
context-path: /auth
session:
cookie:
name: OAuth2SSOToken
AuthorizationServerConfig
@Bean
public ClientDetailsService inMemoryClientDetailsService() throws Exception {
return new InMemoryClientDetailsServiceBuilder()
// client1 mall
.withClient("client1")
.secret(passwordEncoder.encode("client1_secret"))
.scopes("all")
.authorizedGrantTypes("authorization_code", "refresh_token")
.redirectUris("http://client1.xfg.com/client1/login")
.accessTokenValiditySeconds(7200)
.autoApprove(true)
.and()
// client2 lottery
.withClient("client2")
.secret(passwordEncoder.encode("client2_secret"))
.scopes("all")
.authorizedGrantTypes("authorization_code", "refresh_token")
.redirectUris("http://client2.xfg.com/client2/login")
.accessTokenValiditySeconds(7200)
.autoApprove(true)
.and()
.build();
}
@Component("unauthorizedEntryPoint")
public class AppUnauthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
Map<String, String[]> paramMap = request.getParameterMap();
StringBuilder param = new StringBuilder();
paramMap.forEach((k, v) -> {
param.append("&").append(k).append("=").append(v[0]);
});
param.deleteCharAt(0);
String isRedirectValue = request.getParameter("isRedirect");
if (!StringUtils.isEmpty(isRedirectValue) && Boolean.parseBoolean(isRedirectValue)) {
response.sendRedirect("http://sso.xfg.com/authPage/#/login?" + param);
return;
}
String authUrl = "http://sso.xfg.com/auth/oauth/authorize?" + param + "&isRedirect=true";
Map<String, Object> result = new HashMap<>();
result.put("code", 800);
result.put("msg", "授权地址");
result.put("data", authUrl);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
PrintWriter writer = response.getWriter();
ObjectMapper mapper = new ObjectMapper();
writer.print(mapper.writeValueAsString(result));
writer.flush();
writer.close();
}
}
server:
port: 8001
servlet:
context-path: /client1
security:
oauth2:
client:
client-id: client1
preEstablishedRedirectUri:
client-secret: client1_secret
access-token-uri: http://sso.xfg.com/auth/oauth/token
user-authorization-uri: http://sso.xfg.com/auth/oauth/authorize
resource:
user-info-uri: http://sso.xfg.com/auth/user
token-info-uri: http://sso.xfg.com/auth/oauth/check_token
@RestController
public class Client01Controller {
@GetMapping("/create_pay_order")
public Result createPayOrder() {
Result result = new Result();
result.setCode(0);
result.setData("下单完成");
return result;
}
@GetMapping("/")
public void callback(HttpServletResponse response) throws IOException {
response.sendRedirect("http://client1.xfg.com/client1Page/#/home");
}
}
server:
port: 8002
servlet:
context-path: /client2
security:
oauth2:
client:
client-id: client2
client-secret: client2_secret
preEstablishedRedirectUri:
access-token-uri: http://sso.xfg.com/auth/oauth/token
user-authorization-uri: http://sso.xfg.com/auth/oauth/authorize
resource:
user-info-uri: http://sso.xfg.com/auth/user
token-info-uri: http://sso.xfg.com/auth/oauth/check_token
@RestController
public class Client02Controller {
@GetMapping("/lottery")
public Result lottery() {
Result result = new Result();
result.setCode(0);
result.setData("下单红包,金额:" + RandomStringUtils.randomNumeric(10) + "元");
return result;
}
@GetMapping("/")
public void callback(HttpServletResponse response) throws IOException {
response.sendRedirect("http://client2.xfg.com/client2Page/#/home");
}
}
<div class="login-container">
<h2>登录</h2>
<input type="text" id="username" placeholder="用户名" required>
<input type="password" id="password" placeholder="密码" required>
<button id="login-btn">登录</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const base = 'http://sso.xfg.com'; // 设置你的基础URL
document.getElementById('login-btn').addEventListener('click', function() {
const loginForm = {
username: document.getElementById('username').value,
password: document.getElementById('password').value
};
postRequest('/auth/login', loginForm).then(resp => {
if (resp.data.code === 0) {
const pageUrl = window.location.href;
const param = pageUrl.split('?')[1];
window.location.href = '/auth/oauth/authorize?' + param;
} else {
console.log('登录失败:' + resp.data.msg);
}
});
});
function postRequest(url, params) {
return axios({
method: 'post',
url: `${base}${url}`,
data: params,
transformRequest: [function (data) {
let ret = '';
for (let it in data) {
ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&';
}
return ret;
}],
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
}
</script>
<div>
<button id="testButton">开始下单</button>
<p id="result">下单结果:</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const base = 'http://client1.xfg.com';
function getRequest(url) {
return axios.get(`${base}${url}`);
}
document.getElementById('testButton').addEventListener('click', function() {
getRequest('/client1/create_pay_order').then(resp => {
const resultElement = document.getElementById('result');
if (resp.data.code === 0) {
const linkHtml = " <a href='http://client2.xfg.com/client2Page/#/home'>领红包</a>";
resultElement.innerHTML = resp.data.data + linkHtml;
} else if (resp.data.code === 800) {
window.location.href = resp.data.data;
} else {
console.log('失败:' + resp.data);
}
}).catch(error => {
console.log('请求失败:', error);
});
});
</script>
<div>
<button id="testButton">随机红包</button>
<p id="result">红包结果:</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const base = 'http://client2.xfg.com';
function getRequest(url) {
return axios.get(`${base}${url}`);
}
document.getElementById('testButton').addEventListener('click', function() {
getRequest('/client2/lottery').then(resp => {
const resultElement = document.getElementById('result');
if (resp.data.code === 0) {
resultElement.textContent = resp.data.data;
} else if (resp.data.code === 800) {
window.location.href = resp.data.data;
} else {
console.log('失败:' + resp.data);
}
}).catch(error => {
console.log('请求失败:', error);
});
});
</script>

server {
listen 80;
server_name sso.xfg.com;
location /auth/ {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.1.107:8091/auth/;
}
location /authPage/ {
alias /usr/share/nginx/html/;
index auth.html;
}
location ~ .*\.(js|css)$ {
alias /usr/share/nginx/html/;
index auth.html;
}
}
server {
listen 80;
server_name client1.xfg.com;
location /client1/ {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.1.107:8001/client1/;
}
location /client1Page/ {
alias /usr/share/nginx/html/;
index client1.html;
}
location ~ .*\.(js|css)$ {
alias /usr/share/nginx/html/;
index client1.html;
}
}
server {
listen 80;
server_name client2.xfg.com;
location /client2/ {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.1.107:8002/client2/;
}
location /client2Page/ {
alias /usr/share/nginx/html/;
index client2.html;
}
location ~ .*\.(js|css)$ {
alias /usr/share/nginx/html/;
index client2.html;
}
}
更多的代码从工程中阅读即可,复杂度不高。



你可以访问地址1进行验证,登录之后也可以进入地址2进行验证;
测试过程:
- END -
加入小傅哥的星球「码农会锁」,💐斩获大厂Offer!阅读500+份简历和评审,学习6个业务项目;MVC+DDD,双架构开发小型电商、大营销(超级大课)、OpenAI 大模型应用、Lottery、IM、AI 问答助手。7个组件项目;OpenAI 代码评审、BCP 透视业务监控、动态线程池、支付SDK设计和开发、API网关、SpringBoot Starter、IDEA Plugin 插件开发。1套源码课程、1套基础教程、1到云服务器教程以及各类场景解决方案。
小傅哥有那么多课程内容,我加入后都可以学习吗?可以!
好啦,这就是小傅哥的技术列车🚌,嘎嘎实惠!🤔 几乎没有哪个大厂架构师,用这么一个普惠的价格手把手的教大家学习了。
星球「码农会锁」- 加入后从课程入口进入项目学习
星球全程手把手指导教学,遇到技术问题帮忙排查代码。已经有很多伙伴开始学起来了,还有大家交的作业笔记。有了的项目驱动学习,清晰的目标感,大家冲起来也有了更明确的方向!干干干!!!