前言
Smart-SSO允许应用在使用前后端分离模式下完成接入。该模式无法借助 Cookie来传递调用凭证,也就是调用凭证不能通过后端直接写入Cookie来保持会话。我们支持了一种由前端主动来获取凭证,并缓存在客户端本地,每次HTTP请求通过在Header传递凭证参数方式来保持认证状态。同时,前后端一体模式下的accessToken失效自动发起refreshToken请求刷新凭证的能力,也需要由前端主动调起。
在开始前,你需要优先完成快速开始章节的学习,并保障smart-sso- server服务端已被正常的启动提供服务,相应的测试域名都已添加至Hosts。文章中使用Nginx来启动前端服务,也需提前准备,当然你也可以用其它的web服务器代替。
启动后端服务
找到smart-sso-demo-h5应用的启动类DemoH5Application.java,直接右击run启动,如下图所示可以看到应用被监听的8082端口。
启动前端服务
在smart-sso-demo-h5模块中找到/resources/static/index.html,用它替换掉nginx默认的index.html,并启动Nginx服务。
验证
浏览器访问http://localhost,会重定向到服务端登录页。输入正确的账号密码登录后,会回跳至smart-sso-demo-h5首页,如下图所示:
此时,浏览器访问http://server.smart-sso.com:8080。此时,它不再需要经过登录页,直接进入到了smart-sso-server首页,完成单点登录功能。当然也可以验证一下“单点退出”功能。
代码说明
后端-集成接口
前后端分离模式下,后端服务需要增加必要的集成接口
/**
* 前后端分离模式下集成SSO所需的接口
*/
@RestController
@RequestMapping("/auth")
public class AuthController {
/**
* 返回SSO登录地址
*
* @param redirectUri 登录成功后的回跳地址
* @return
*/
@RequestMapping("/login_url")
public Result<String> getLoginUrl(@RequestParam String redirectUri) {
return Result.success(SSOUtils.buildLoginUrl(redirectUri));
}
/**
* 返回SSO退出地址
*
* @param redirectUri 退出成功后的回跳地址
* @return
*/
@RequestMapping("/logout_url")
public Result<String> getLogoutUrl(@RequestParam String redirectUri) {
return Result.success(SSOUtils.buildLogoutUrl(redirectUri));
}
/**
* 通过授权码获取SSO调用凭证
*
* @param code 授权码
* @return
*/
@RequestMapping(value = "/access-token", method = RequestMethod.GET)
public Result<Token> getAccessToken(@RequestParam String code) {
Result<Token> result = SSOUtils.getHttpAccessToken(code);
if (!result.isSuccess()) {
return result;
}
return Result.success(result.getData());
}
/**
* 通过刷新凭证获取新的调用凭证
*
* @param refreshToken 刷新凭证
* @return
*/
@RequestMapping(value = "/refresh-token", method = RequestMethod.GET)
public Result<Token> getRefreshToken(String refreshToken) {
Result<Token> result = SSOUtils.getHttpRefreshToken(refreshToken);
if (!result.isSuccess()) {
return result;
}
return Result.success(result.getData());
}
}
后端-跨域处理
前后端分离模式下,通常都是跨域请求,后端需要具备跨域处理能力。
/**
* 跨域过滤器
* 注:跨域过滤器@Order设置的数值,必须要优先级高于ClientContainer设置的Order,详见ClientProperties.order属性
*/
@Order(5)
@WebFilter
@Configuration
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 允许指定域访问跨域资源
response.setHeader("Access-Control-Allow-Origin", "*");
// 允许所有请求方式
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
// 有效时间
response.setHeader("Access-Control-Max-Age", "3600");
// 允许的header参数
response.setHeader("Access-Control-Allow-Headers", "*");
// 如果是预检请求,直接返回
if ("OPTIONS".equals(request.getMethod())) {
response.getWriter().print("");
return;
}
chain.doFilter(req, res);
}
}
后端-配置文件
注意:
1.smart.sso.exclude-urls在前后端分离模式下,全局过滤器需要忽略对集成所需接口的拦截;
2.smart.sso.h5-enabled必须是开启状态,默认为false;
smart:
sso:
#服务端地址
server-url: http://server.smart-sso.com:8080
#客户端密钥信息
client-id: 1001
client-secret: kpA1y7k1uyrcoGhrKvA1Ag==
#前后端分离必须忽略获取凭证相关的urls
exclude-urls: /auth/*
#前后端分离开关
h5-enabled: true
前端-凭证校验
前端页必须具备凭证获取、校验及刷新的能力,可以参考index.html自行封装。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Smart-SSO-Demo-H5</title>
</head>
<body>
<h2>Smart-SSO-Demo-H5</h2>
<br /><b>用户名</b>:<span id="_username"></span><br />
<br /><b>用户已分配的菜单</b>:<span id="_userMenus"></span>
<br /><b>用户已分配的权限</b>:<span id="_userPermissions"></span>
<br /><a href="javascript:goSSOLogoutUrl()">单点退出</a>
</body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>window.jQuery || alert('jQuery.js未加载成功,请检查网络或更换CDN')</script>
<script type="text/javascript">
// 后端接口地址
var baseUrl = "http://demo.smart-sso.com:8082";
$(function () {
if (validateLogin()) {
getUserinfo();
}
})
// 校验是否需要跳转到SSO认证中心
function validateLogin() {
var accessToken = localStorage.getItem("accessToken");
if (accessToken) {
return true;
}
var code = getParam('code');
if (code && getAccessToken(code)) {
removeCodeParamAndRedirect();
return true;
}
goSSOLoginUrl();
return false;
}
// 获取用户信息
function getUserinfo() {
smart.ajax('GET', baseUrl + "/userinfo", {}, function (res) {
var userinfo = res.data;
// 用户名
$("#_username").html(userinfo.username);
// 用户已分配的菜单
var userMenus = '';
userinfo.userMenus.forEach(function (menu) {
userMenus += '<li>' + menu + '</li>';
});
$('#_userMenus').html(userMenus);
// 用户已分配的权限
var userPermissions = '';
userinfo.userPermissions.forEach(function (permission) {
userPermissions += '<li>' + permission + '</li>';
});
$('#_userPermissions').html(userPermissions);
});
}
// 重定向至认证中心
function goSSOLoginUrl() {
$.getJSON(baseUrl + '/auth/login_url?redirectUri=' + location.href, function (res) {
window.location.href = res.data;
})
}
// 重定向至服务端退出地址
function goSSOLogoutUrl() {
$.getJSON(baseUrl + '/auth/logout_url?redirectUri=' + location.href, function (res) {
window.location.href = res.data;
})
}
// 获取accessToken
function getAccessToken(code) {
var bool = false;
$.ajax({
url: baseUrl + "/auth/access-token?code=" + code,
type: 'GET',
async: false,
dataType: 'json',
success: function (res) {
if (res.code == 1) {
localStorage.setItem("accessToken", res.data.accessToken);
localStorage.setItem("refreshToken", res.data.refreshToken);
bool = true;
}
}
});
return bool;
}
// 获取refreshToken
function getRefreshToken() {
var bool = false;
$.ajax({
url: baseUrl + "/auth/refresh-token?refreshToken=" + localStorage.getItem("refreshToken"),
type: 'GET',
async: false,
dataType: 'json',
success: function (res) {
if (res.code == 1) {
localStorage.setItem("accessToken", res.data.accessToken);
localStorage.setItem("refreshToken", res.data.refreshToken);
bool = true;
}
}
});
return bool;
}
// 从url中获取指定名称的参数值
function getParam(name) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
if (pair[0] == name) { return pair[1]; }
}
return null;
}
// 重定向方式去除地址栏中的code参数
function removeCodeParamAndRedirect() {
var url = new URL(window.location.href);
url.searchParams.delete('code');
window.location.href = url.toString();
}
</script>
<script type="text/javascript">
var smart = {};
// 为业务请求封装Ajax方法,请求头自动附带token,请求返回处理token刷新和失效跳转等逻辑
smart.ajax = function (method, url, data, successFn) {
$.ajax({
type: method,
url: url,
data: data,
dataType: 'json',
headers: {
// 传递调用凭证,名称可通过后台配置项自定义,默认为smart-sso-token
'smart-sso-token': localStorage.getItem("accessToken"),
// 跨域请求需自行设置X-Requested-With参数,Ajax仅在非跨域请求默认携带
'X-Requested-With': 'XMLHttpRequest'
},
success: function (res) {
// 响应成功
if (res.code == 1) {
successFn(res);
}
// 后端登录失效,需要清理前端缓存,并跳转至认证中心
else if (res.code == 10) {
localStorage.clear();
goSSOLoginUrl();
}
/**
* accessToken已过期,refreshToken已过期,客户端需要调用refreshToken刷新accessToken
* 1、如果refreshToken成功,之前因为accessToken过期请求失败的接口再发起一遍
* 2、否则,跳转至认证中心
*/
else if (res.code == 15) {
if (getRefreshToken()) {
smart.ajax(method, url, data, successFn);
}
else {
goSSOLoginUrl();
}
}
// 其它异常,弹出提示
else {
alert(res.message);
}
},
error: function (xhr, type, errorThrown) {
alert(JSON.stringify(xhr));
}
});
}
</script>
</html>