今天春天太苦了!!先后经历了失恋,然后住院,春招也就找了半个月,幸亏秋招的时候有公司捞我了,不然今年要失业了!!!
话不多说,步入正题,单点登录这个问题我很久之前有研究过,但是使用SpringSecurity写的,只感觉很离谱,要的配置实在是太多了,这个月公司提前配置,要整这个东西,我用SpringSecurity这个Oauth2实现单点登录又做了一次,但这次死活不行,前后弄了一个多星期,真离谱!!
后来发现了Sa-token的这款工具,发现挺好用的,查了查,好像说比Shiro和SpringSecurity还要好,于是使用这个将单点登录,Oauth2分开做了一下,最后还整合了一下(这两个整合有点鸡肋,其实没必要,但有些业务是这样需求的,就比如我公司配置就要求进行整合…)
很多前置知识不懂可以去看sa-token的官网
@RestController
public class SsoServerController {
//处理所有的sso请求
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoProcessor.instance.serverDister();
}
@Autowired
private void configSso(SaSsoConfig sso) {
// 配置:未登录时返回的View
sso.setNotLoginView(() -> {
return new ModelAndView("login.html");
});
// 配置:登录处理函数
sso.setDoLoginHandle((name, pwd) -> {
// 此处仅做模拟登录,真实环境应该查询数据进行登录
if("admin".equals(name) && "123456".equals(pwd)) {
StpUtil.login(10001);
return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
}
return SaResult.error("登录失败!");
});
// 配置 Http 请求处理器 (在模式三的单点注销功能下用到,如不需要可以注释掉)
sso.setSendHttp(url -> {
try {
// 发起 http 请求
System.out.println("------ 发起请求:" + url);
return Forest.get(url).executeAsString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
});
}
}
登录的login.html如下
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sa-OAuth2-认证中心-登录页title>
<style type="text/css">
body{background-color: #F5F5D5;}
*{margin: 0px; padding: 0px;}
.login-box{width: 400px; margin: 20vh auto;}
.login-box input{line-height: 25px; margin-bottom: 10px;}
.login-box button{padding: 5px 15px; cursor: pointer; }
style>
head>
<body>
<div class="login-box">
<h2>Sa-OAuth2-认证中心-登录页h2> <br>
账号:<input name="name" /> <br>
密码:<input name="pwd" type="password" /> <br>
<button onclick="doLogin()">登录button>
<span style="color: #666;">(测试账号: sa 123456)span>
div>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js">script>
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js">script>
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')script>
<script type="text/javascript">
// 登录方法
function doLogin() {
console.log('-----------');
$.ajax({
url: '/sso/doLogin',
data: {
name: $('[name=name]').val(),
pwd: $('[name=pwd]').val()
},
dataType: 'json',
success: function(res) {
if(res.code == 200) {
layer.msg('登录成功!');
setTimeout(function() {
// location.reload(true);
location.reload();
}, 800);
} else {
layer.alert(res.msg);
}
},
error: function(e) {
console.log('error');
}
});
}
script>
body>
html>
yml文件配置如下:
# ??
server:
port: 9000
# Sa-Token ??
sa-token:
sso:
# Ticket??? (??: ?)??????
ticket-timeout: 300
# ???????????
allow-url: "*"
# ??????????
is-slo: true
# ------- SSO-??????? ???????SSO????? is-slo=true ????
# ???????
isHttp: true
# ?????????SSO???????????
secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
# ---- ????????????? Sa-Token ??http??????????????
spring:
# Redis?? ?SSO?????????Redis??????
redis:
# Redis?????????0?
database: 1
# Redis?????
host: 182.92.5.218
# Redis???????
port: 6379
# Redis?????????????
password: 123456
forest:
# ?? forest ??????
log-enabled: false
认证中心已经配置完毕,现在到客户端中心
# ??
server:
port: 9002
# sa-token??
sa-token:
# SSO-????
sso:
# SSO-Server? ??????
auth-url: http://localhost:9000/sso/auth
# ??????????
is-slo: true
# ??Sa-Token?????Redis?? ??????SSO-Server??????Redis?
spring:
# Redis?? ?SSO?????????Redis??????
redis:
# Redis?????????0?
database: 1
# Redis?????
host: 182.92.5.218
# Redis???????
port: 6379
# Redis?????????????
password: 123456
@RestController
public class SsoClientController {
// 首页
@RequestMapping("/")
public String index() {
return StpUtil.isLogin()?"登录成功":"未登录";
}
@RequestMapping("/test")
public String test() {
return "test....";
}
/*
* SSO-Client端:处理所有SSO相关请求
* http://{host}:{port}/sso/login -- Client端登录地址,接受参数:back=登录后的跳转地址
* http://{host}:{port}/sso/logout -- Client端单点注销地址(isSlo=true时打开),接受参数:back=注销后的跳转地址
* http://{host}:{port}/sso/logoutCall -- Client端单点注销回调地址(isSlo=true时打开),此接口为框架回调,开发者无需关心
*/
//这里是必须配置的
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoProcessor.instance.clientDister();
}
}
我们测试的是/test路径,但是这里还需要一个拦截器:
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
/** 注册 [Sa-Token全局过滤器] */
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
.addInclude("/**")
.addExclude("/sso/*","/oauth2/*" ,"/favicon.ico")
.setAuth(obj -> {
//没登录的时候就去登录
if(!StpUtil.isLogin()) {
String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString());
SaHolder.getResponse().redirect("/sso/login?back=" + SaFoxUtil.encodeUrl(back));
SaRouter.back();
}
})
;
}
}
测试的时候,首先会跳转到本客户端下的login,然后重定向到认证中心的login,之后就重定向回来,再次进入该拦截器(这里是重点,会再次进入拦截器),然后判断已经登录了,所以跳过,进入/test路径成功,返回数据,整个单点登录就完成了,当你新建一个客户端,就无需再次登录了.
sa-token下的Oauth2.0做起来也比较简单:
server:
port: 8001
# sa-token??
sa-token:
# token?? (????cookie??)
token-name: satoken-server
# OAuth2.0 ??
oauth2:
is-code: true
is-implicit: true
is-password: true
is-client: true
spring:
# redis??
redis:
# Redis?????????0?
database: 1
# Redis?????
host: 182.92.5.218
# Redis???????
port: 6379
# Redis?????????????
password: 123456
# ??????????
timeout: 1000ms
lettuce:
pool:
# ????????
max-active: 200
# ???????????????????????
max-wait: -1ms
# ???????????
max-idle: 10
# ???????????
min-idle: 0
@RestController
public class SaOAuth2ServerController {
// 处理所有OAuth相关请求
@RequestMapping("/oauth2/*")
public Object request() {
System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl());
return SaOAuth2Handle.serverRequest();
}
@Autowired
public void setSaOAuth2Config(SaOAuth2Config config){
config.
// 未登录的视图
setNotLoginView(()->{
return new ModelAndView("login.html");
}).
// 登录处理函数
setDoLoginHandle((name, pwd) -> {
if("admin".equals(name) && "123456".equals(pwd)) {
StpUtil.login(1001);
return SaResult.ok();
}
return SaResult.error("账号名或密码错误");
}).
// 授权确认视图
setConfirmView((clientId, scope)->{
Map<String, Object> map = new HashMap<>();
map.put("clientId", clientId);
map.put("scope", scope);
return new ModelAndView("confirm.html", map);
})
;
}
}
注意,这里的Oauth所有请求必须是/oauth2开头的,其他的不行,而sso单点登录的可以修改,只需要保持与客户端的一致就可以了,但这里不行
@Component
public class SaOAuth2TemplateImpl extends SaOAuth2Template {
@Override
public SaClientModel getClientModel(String clientId) {
return new SaClientModel()
.setClientId("1001")
.setClientSecret("123456")
.setAllowUrl("*")
.setIsAutoMode(true).setContractScope("userinfo");
}
@Override
public String getOpenid(String clientId, Object loginId) {
return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__";
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sa-OAuth2-认证中心-登录页</title>
<style type="text/css">
body{background-color: #F5F5D5;}
*{margin: 0px; padding: 0px;}
.login-box{width: 400px; margin: 20vh auto;}
.login-box input{line-height: 25px; margin-bottom: 10px;}
.login-box button{padding: 5px 15px; cursor: pointer; }
</style>
</head>
<body>
<div class="login-box">
<h2>Sa-OAuth2-认证中心-登录页</h2> <br>
账号:<input name="name" /> <br>
密码:<input name="pwd" type="password" /> <br>
<button onclick="doLogin()">登录</button>
<span style="color: #666;">(测试账号: sa 123456)</span>
</div>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
<script type="text/javascript">
// 登录方法
function doLogin() {
console.log('-----------');
$.ajax({
url: '/oauth2/doLogin',
data: {
name: $('[name=name]').val(),
pwd: $('[name=pwd]').val()
},
dataType: 'json',
success: function(res) {
if(res.code == 200) {
layer.msg('登录成功!');
setTimeout(function() {
// location.reload(true);
location.reload();
}, 800);
} else {
layer.alert(res.msg);
}
},
error: function(e) {
console.log('error');
}
});
}
</script>
</body>
</html>
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sa-OAuth2-认证中心-确认授权页title>
<style type="text/css">
body{background-color: #F5F5D5;}
*{margin: 0px; padding: 0px;}
.login-box{width: 400px; margin: 20vh auto; padding: 70px; border: 1px #000 solid;}
.login-box button{padding: 5px 15px; cursor: pointer; }
style>
head>
<body>
<div class="login-box">
<h2>Sa-OAuth2-认证中心-确认授权页h2> <br>
<div>
<div><b>应用ID:b><span th:utext="${clientId}">span>div>
<div><b>请求授权:b><span th:utext="${scope}">span>div>
<br><div>------------- 是否同意授权 -------------div><br>
<div>
<button onclick="yes()">同意button>
<button onclick="no()">拒绝button>
div>
div>
div>
<script src="https://unpkg.zhimg.com/[email protected]/dist/jquery.min.js">script>
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js">script>
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')script>
<script type="text/javascript">
// 同意授权
function yes() {
console.log('-----------');
$.ajax({
url: '/oauth2/doConfirm',
data: {
client_id: getParam('client_id'),
scope: getParam('scope')
},
dataType: 'json',
success: function(res) {
if(res.code == 200) {
layer.msg('授权成功!');
setTimeout(function() {
//页面自动刷新,重定向回初始页面
location.reload(true);
}, 800);
} else {
// 重定向至授权失败URL
layer.alert('授权失败!');
}
},
error: function(e) {
console.log('error');
}
});
}
// 拒绝授权
function no() {
var url = joinParam(getParam('redirect_uri'), "handle=refuse&msg=用户拒绝了授权");
location.href = url;
}
// 从url中查询到指定名称的参数值
function getParam(name, defaultValue){
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(defaultValue == undefined ? null : defaultValue);
}
// 在url上拼接上kv参数并返回
function joinParam(url, parameStr) {
if(parameStr == null || parameStr.length == 0) {
return url;
}
var index = url.indexOf('?');
// ? 不存在
if(index == -1) {
return url + '?' + parameStr;
}
// ? 是最后一位
if(index == url.length - 1) {
return url + parameStr;
}
// ? 是其中一位
if(index > -1 && index < url.length - 1) {
// 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 &
if(url.lastIndexOf('&') != url.length - 1 && parameStrindexOf('&') != 0) {
return url + '&' + parameStr;
} else {
return url + parameStr;
}
}
}
script>
body>
html>
我们先写yml配置:
server:
port: 8002
# sa-token??
sa-token:
# token?? (????cookie??)
token-name: satoken-client
spring:
# redis??
redis:
# Redis?????????0?
database: 1
# Redis?????
host: 182.92.5.218
# Redis???????
port: 6379
# Redis?????????????
password: 123456
# ??????????
timeout: 1000ms
lettuce:
pool:
# ????????
max-active: 200
# ???????????????????????
max-wait: -1ms
# ???????????
max-idle: 10
# ???????????
min-idle: 0
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sa-OAuth2-Client端-测试页</title>
<style type="text/css">
body{background-color: #D0D9E0;}
*{margin: 0px; padding: 0px;}
.login-box{max-width: 1000px; margin: 30px auto; padding: 1em;}
.info{line-height: 30px;}
.btn-box{margin-top: 10px; margin-bottom: 15px;}
.btn-box a{margin-right: 10px;}
.btn-box a:hover{text-decoration:underline !important;}
.login-box input{line-height: 25px; margin-bottom: 10px; padding-left: 5px;}
.login-box button{padding: 5px 15px; margin-top: 20px; cursor: pointer; }
.login-box a{text-decoration: none;}
.pst{color: #666; margin-top: 15px;}
.ps{color: #666; margin-left: 10px;}
.login-box code{display: block; background-color: #F5F2F0; border: 1px #ccc solid; color: #600; padding: 15px; margin-top: 5px; border-radius: 2px; }
.info b,.info span{color: green;}
</style>
</head>
<body>
<div class="login-box">
<h2>Sa-OAuth2-Client端-测试页</h2> <br>
<div class="info">
<div>当前账号id:
<b class="uid" th:utext="${uid}"></b>
</div>
<div>当前Openid: <span class="openid"></span></div>
<div>当前Access-Token: <span class="access_token"></span></div>
<div>当前Refresh-Token: <span class="refresh_token"></span></div>
<div>当前令牌包含Scope: <span class="scope"></span></div>
<div>当前Client-Token: <span class="client_token"></span></div>
</div>
<div class="btn-box">
<a href="javascript:logout();">注销</a>
<a href="/">回到首页</a>
</div>
<hr><br>
<h3>模式一:授权码(Authorization Code)</h3>
<p class="pst">授权码:OAuth2.0标准授权流程,先 (重定向) 获取Code授权码,再 (Rest API) 获取 Access-Token 和 Openid </p>
<a href="http://localhost:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://localhost:8002/">
<button>点我开始授权登录(静默授权)</button>
</a>
<span class="ps">当请求链接不包含scope权限时,将无需用户手动确认,做到静默授权,当然此时我们也只能获取openid</span>
<code>http://localhost:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://localhost:8002/</code>
<a href="http://localhost:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo">
<button>授权登录(显式授权)</button>
</a>
<span class="ps">当请求链接包含具体的scope权限时,将需要用户手动确认,此时我们除了openid以外还可以获取更多的资源</span>
<code>http://localhost:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo</code>
<button onclick="refreshToken()">刷新令牌</button>
<span class="ps">我们可以拿着 Refresh-Token 去刷新我们的 Access-Token,每次刷新后旧Token将作废</span>
<code>http://localhost:8001/oauth2/refresh?grant_type=refresh_token&client_id={value}&client_secret={value}&refresh_token={value}</code>
<button onclick="getUserinfo()">获取账号信息</button>
<span class="ps">使用 Access-Token 置换资源: 获取账号昵称、头像、性别等信息 (Access-Token具备userinfo权限时才可以获取成功) </span>
<code>http://localhost:8001/oauth2/userinfo?access_token={value}</code>
<br>
<h3>模式二:隐藏式(Implicit)</h3>
<a href="http://localhost:8001/oauth2/authorize?response_type=token&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo">
<button>隐藏式</button>
</a>
<span class="ps">越过授权码的步骤,直接返回token到前端页面( 格式:http//:domain.com#token=xxxx-xxxx )
<code>http://localhost:8001/oauth2/authorize?response_type=token&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=userinfo</code>
<br>
<h3>模式三:密码式(Password)</h3>
<p class="pst">在下面输入Server端的用户名和密码,使用密码式进行 OAuth2 授权登录</p>
账号:<input name="username">
密码:<input name="password">
<button onclick="passwordLogin()">登录</button>
<code>http://localhost:8001/oauth2/token?grant_type=password&client_id={value}&client_secret={value}&username={value}&password={value}</code>
<br>
<h3>模式四:凭证式(Client Credentials)</h3>
<p class="pst">以上三种模式获取的都是用户的 Access-Token,代表用户对第三方应用的授权,在OAuth2.0中还有一种针对 Client级别的授权,
即:Client-Token,代表应用自身的资源授权</p>
<p class="pst">Client-Token具有延迟作废特性,即:在每次获取最新Client-Token的时候,旧Client-Token不会立即过期,而是作为Past-Token再次
储存起来,资源请求方只要携带其中之一便可通过Token校验,这种特性保证了在大量并发请求时不会出现“新旧Token交替造成的授权失效”,
保证了服务的高可用</p>
<button onclick="getClientToken()">获取应用Client-Token</button>
<code>http://localhost:8001/oauth2/client_token?grant_type=client_credentials&client_id={value}&client_secret={value}</code>
<br><br>
<span>更多资料请参考 Sa-Token 官方文档地址:</span>
<a href="https://sa-token.cc/">https://sa-token.cc/</a>
<div style="height: 200px;"></div>
</div>
<script src="https://unpkg.zhimg.com/[email protected]/dist/jquery.min.js"></script>
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
<script type="text/javascript">
// 根据code授权码进行登录
function doLogin(code) {
$.ajax({
url: '/codeLogin?code=' + code,
dataType: 'json',
success: function(res) {
console.log('返回:', res);
if(res.code == 200) {
setInfo(res.data);
layer.msg('登录成功!');
} else {
layer.msg(res.msg);
}
},
error: function(xhr, type, errorThrown){
return layer.alert("异常:" + JSON.stringify(xhr));
}
});
}
var code = getParam('code');
if(code) {
alert("进入了...code")
doLogin(code);
}else{
alert("2222222")
}
// 根据 Refresh-Token 去刷新 Access-Token
function refreshToken() {
var refreshToken = $('.refresh_token').text();
if(refreshToken == '') {
return layer.alert('您还没有获取 Refresh-Token ,请先授权登录');
}
$.ajax({
url: '/refresh?refreshToken=' + refreshToken,
dataType: 'json',
success: function(res) {
console.log('返回:', res);
if(res.code == 200) {
setInfo(res.data);
layer.msg('登录成功!');
} else {
layer.msg(res.msg);
}
},
error: function(xhr, type, errorThrown){
return layer.alert("异常:" + JSON.stringify(xhr));
}
});
}
// 模式三:密码式-授权登录
function passwordLogin() {
$.ajax({
url: '/passwordLogin',
data: {
username: $('[name=username]').val(),
password: $('[name=password]').val()
},
dataType: 'json',
success: function(res) {
console.log('返回:', res);
if(res.code == 200) {
setInfo(res.data);
layer.msg('登录成功!');
} else {
layer.msg(res.msg);
}
},
error: function(xhr, type, errorThrown){
return layer.alert("异常:" + JSON.stringify(xhr));
}
});
}
// 模式四:获取应用的 Client-Token
function getClientToken () {
$.ajax({
url: '/clientToken',
dataType: 'json',
success: function(res) {
console.log('返回:', res);
if(res.code == 200) {
setInfo(res.data);
layer.msg('获取成功!');
} else {
layer.msg(res.msg);
}
},
error: function(xhr, type, errorThrown){
return layer.alert("异常:" + JSON.stringify(xhr));
}
});
}
// 使用 Access-Token 置换资源: 获取账号昵称、头像、性别等信息
function getUserinfo() {
var accessToken = $('.access_token').text();
if(accessToken == '') {
return layer.alert('您还没有获取 Access-Token ,请先授权登录');
}
$.ajax({
url: '/getUserinfo',
data: {accessToken: accessToken},
dataType: 'json',
success: function(res) {
if(res.code == 200) {
layer.alert(JSON.stringify(res.data));
} else {
layer.alert(res.msg);
}
},
error: function(xhr, type, errorThrown){
return layer.alert("异常:" + JSON.stringify(xhr));
}
});
}
// 注销
function logout() {
$.ajax({
url: '/logout',
dataType: 'json',
success: function(res) {
location.href = '/';
},
error: function(xhr, type, errorThrown){
return layer.alert("异常:" + JSON.stringify(xhr));
}
});
}
// 写入数据
function setInfo(info) {
console.log('info', info);
for (var key in info) {
$('.' + key).text(info[key]);
}
if($('.uid').text() == '') {
$('.uid').html('<b style="color: #E00;">未登录</b>')
}
}
setInfo({});
// 从url中查询到指定名称的参数值
function getParam(name, defaultValue){
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(defaultValue == undefined ? null : defaultValue);
}
</script>
</body>
</html>
我们以授权码模式来说,关键在于对http://localhost:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=…这个路径的请求,请求完这个路径后,会返回一个code码,这个code不是token,再接续写代码:
package com.hyb.client;
import javax.servlet.http.HttpServletRequest;
import cn.dev33.satoken.sso.SaSsoProcessor;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import com.ejlchina.okhttps.OkHttps;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
/**
* Sa-OAuth2 Client端 控制器
* @author kong
*/
@RestController
public class SaOAuthClientController {
// 相关参数配置
private String clientId = "1001"; // 应用id
private String clientSecret = "123456"; // 应用秘钥
private String serverUrl = "http://localhost:8001"; // 服务端接口
// 进入首页
@RequestMapping("/")
public Object index(HttpServletRequest request) {
request.setAttribute("uid", StpUtil.getLoginIdDefaultNull());
return new ModelAndView("index.html");
} // 进入首页
// 根据Code码进行登录,获取 Access-Token 和 openid
@RequestMapping("/codeLogin")
public SaResult codeLogin(String code) {
// 调用Server端接口,获取 Access-Token 以及其他信息
String str = OkHttps.sync(serverUrl + "/oauth2/token")
.addBodyPara("grant_type", "authorization_code")
.addBodyPara("code", code)
.addBodyPara("client_id", clientId)
.addBodyPara("client_secret", clientSecret)
.post()
.getBody()
.toString();
SoMap so = SoMap.getSoMap().setJsonString(str);
System.out.println("返回结果: " + so);
// code不等于200 代表请求失败
if(so.getInt("code") != 200) {
return SaResult.error(so.getString("msg"));
}
// 根据openid获取其对应的userId
SoMap data = so.getMap("data");
long uid = getUserIdByOpenid(data.getString("openid"));
data.set("uid", uid);
// 返回相关参数
StpUtil.login(uid);
return SaResult.data(data);
}
// 根据 Refresh-Token 去刷新 Access-Token
@RequestMapping("/refresh")
public SaResult refresh(String refreshToken) {
// 调用Server端接口,通过 Refresh-Token 刷新出一个新的 Access-Token
String str = OkHttps.sync(serverUrl + "/oauth2/refresh")
.addBodyPara("grant_type", "refresh_token")
.addBodyPara("client_id", clientId)
.addBodyPara("client_secret", clientSecret)
.addBodyPara("refresh_token", refreshToken)
.post()
.getBody()
.toString();
SoMap so = SoMap.getSoMap().setJsonString(str);
System.out.println("返回结果: " + so);
// code不等于200 代表请求失败
if(so.getInt("code") != 200) {
return SaResult.error(so.getString("msg"));
}
// 返回相关参数 (data=新的Access-Token )
SoMap data = so.getMap("data");
return SaResult.data(data);
}
// 模式三:密码式-授权登录
@RequestMapping("/passwordLogin")
public SaResult passwordLogin(String username, String password) {
// 模式三:密码式-授权登录
String str = OkHttps.sync(serverUrl + "/oauth2/token")
.addBodyPara("grant_type", "password")
.addBodyPara("client_id", clientId)
.addBodyPara("client_secret", clientSecret)
.addBodyPara("username", username)
.addBodyPara("password", password)
.post()
.getBody()
.toString();
SoMap so = SoMap.getSoMap().setJsonString(str);
System.out.println("返回结果: " + so);
// code不等于200 代表请求失败
if(so.getInt("code") != 200) {
return SaResult.error(so.getString("msg"));
}
// 根据openid获取其对应的userId
SoMap data = so.getMap("data");
long uid = getUserIdByOpenid(data.getString("openid"));
data.set("uid", uid);
// 返回相关参数
StpUtil.login(uid);
return SaResult.data(data);
}
// 模式四:获取应用的 Client-Token
@RequestMapping("/clientToken")
public SaResult clientToken() {
// 调用Server端接口
String str = OkHttps.sync(serverUrl + "/oauth2/client_token")
.addBodyPara("grant_type", "client_credentials")
.addBodyPara("client_id", clientId)
.addBodyPara("client_secret", clientSecret)
.post()
.getBody()
.toString();
SoMap so = SoMap.getSoMap().setJsonString(str);
System.out.println("返回结果: " + so);
// code不等于200 代表请求失败
if(so.getInt("code") != 200) {
return SaResult.error(so.getString("msg"));
}
// 返回相关参数 (data=新的Client-Token )
SoMap data = so.getMap("data");
return SaResult.data(data);
}
// 注销登录
@RequestMapping("/logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}
// 根据 Access-Token 置换相关的资源: 获取账号昵称、头像、性别等信息
@RequestMapping("/getUserinfo")
public SaResult getUserinfo(String accessToken) {
// 调用Server端接口,查询开放的资源
String str = OkHttps.sync(serverUrl + "/oauth2/userinfo")
.addBodyPara("access_token", accessToken)
.post()
.getBody()
.toString();
SoMap so = SoMap.getSoMap().setJsonString(str);
System.out.println("返回结果: " + so);
// code不等于200 代表请求失败
if(so.getInt("code") != 200) {
return SaResult.error(so.getString("msg"));
}
// 返回相关参数 (data=获取到的资源 )
SoMap data = so.getMap("data");
return SaResult.data(data);
}
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace();
return SaResult.error(e.getMessage());
}
// ------------ 模拟方法 ------------------
// 模拟方法:根据openid获取userId
private long getUserIdByOpenid(String openid) {
// 此方法仅做模拟,实际开发要根据具体业务逻辑来获取userId
return 10001;
}
}
从上面可以看到,会看到一个/codeLogin请求,这个请求就是根据code这个码,去申请token,这个token可以解析出用户的账号和密码,到这里Oauth2的内容结束了.
总结: 先请求得到code,根据code得到信息,授权码模式的路径大概就是先/oauth2/authorize,然后到/codeLogin?code=…
在sa-token里,将它们实现的sso和oauth做了一个比较:
https://sa-token.cc/doc.html#/fun/sso-vs-oauth2
并且对于Oauth做了一个解释:简单来讲,OAuth2.0的应用场景可以理解为单点登录的升级版,单点登录解决了多个系统间会话的共享,OAuth2.0在此基础上增加了应用之间的权限控制 (SO:有些系统采用OAuth2.0模式实现了单点登录,但这总给人一种“杀鸡焉用宰牛刀”的感觉)
所以说,在sa-token中,并没有使用Oauth2整合单点登录的例子,认为没有必要,而如果真的要使用,请看下面的步骤:
# ??
server:
port: 9000
# Sa-Token ??
sa-token:
oauth2:
is-code: true
is-implicit: true
is-password: true
is-client: true
sso:
ticket-timeout: 300
allow-url: "*"
is-slo: true
isHttp: true
secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
spring:
# Redis?? ?SSO?????????Redis??????
redis:
# Redis?????????0?
database: 1
# Redis?????
host: 182.92.5.218
# Redis???????
port: 6379
# Redis?????????????
password: 123456
forest:
# ?? forest ??????
log-enabled: false
@RestController
public class SsoServerController {
//处理所有的sso请求,这里是处理单点登录的
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoProcessor.instance.serverDister();
}
//这里是处理授权的,授权的这个路径不能改变样子
@RequestMapping("/oauth2/*")
public Object request() {
System.out.println("------- 进入请求: " + SaHolder.getRequest().getUrl());
return SaOAuth2Handle.serverRequest();
}
@Autowired
public void setSaOAuth2Config(SaOAuth2Config config){
config.
// 授权确认视图
//
setConfirmView((clientId, scope)->{
Map<String, Object> map = new HashMap<>();
map.put("clientId", clientId);
map.put("scope", scope);
return new ModelAndView("confirm.html", map);
})
;
}
@Autowired
private void configSso(SaSsoConfig sso) {
// 配置:未登录时返回的View
sso.setNotLoginView(() -> {
return new ModelAndView("login.html");
});
// 配置:登录处理函数
sso.setDoLoginHandle((name, pwd) -> {
// 此处仅做模拟登录,真实环境应该查询数据进行登录
if("admin".equals(name) && "123456".equals(pwd)) {
StpUtil.login(10001);
return SaResult.ok("登录成功!").setData(StpUtil.getTokenValue());
}
return SaResult.error("登录失败!");
});
// 配置 Http 请求处理器 (在模式三的单点注销功能下用到,如不需要可以注释掉)
sso.setSendHttp(url -> {
try {
// 发起 http 请求
System.out.println("------ 发起请求:" + url);
return Forest.get(url).executeAsString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
});
}
}
以上便是Oauth2实现单点登录的全部认证中心的配置,这里的大概逻辑就是,sso实现登录,oauth实现授权
客户端便有点讲究,我们必须要认清楚Oauth2.0实现单点登录的流程:
我们可以看到前面一部分是sso的内容,也就是如果访问应用路径不存在,就重定向到统一认证服务,这里直接使用和sso单点登录的拦截器就可以了(上面有配置),但上面有说明,认证成功重定向回来后,还会进入拦截器,所以,这个时候我们得在拦截器里做文章:
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
/** 注册 [Sa-Token全局过滤器] */
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
.addInclude("/**")
.addExclude("/sso/*","/oauth2/*" ,"/favicon.ico")
.setAuth(obj -> {
//没登录的时候就去登录
if(!StpUtil.isLogin()) {
String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString());
SaHolder.getResponse().redirect("/sso/login?back=" + SaFoxUtil.encodeUrl(back));
SaRouter.back();
//登录后但还未授权就去授权
}else if (SaFoxUtil.isEmpty(SaHolder.getRequest().getParam("code"))){
String back = SaFoxUtil.joinParam(SaHolder.getRequest().getUrl(), SpringMVCUtil.getRequest().getQueryString());
SaHolder.getResponse().redirect("http://localhost:9000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=" + SaFoxUtil.encodeUrl(back)+"&scope=userinfo");
SaRouter.back();
}
//既登录又授权之后,就可以拿到code去请求token了,就是像上面说的请求codeLogin路径就可以了,可以不在这里写
})
;
}
}
流程如下: 第一次请求/test路径,没登录,去认证中心进行sso登录,登录后回调,回到拦截器,判断已经登录,来到第二个if判断,发现没授权,去请求授权,然后携带者code重定向回来,这个时候,已经登录,且code不为空,说明已经登录并授权,可以拿着这个code去申请token了,这个token里就包含着用户信息,这样就相当于登录且授权成功.
其他controller的配置如下:
@RestController
public class SsoClientController {
// 首页
@RequestMapping("/")
public String index() {
return StpUtil.isLogin()?"登录成功":"未登录";
}
@RequestMapping("/test")
public String test() {
return "test....";
}
/*
* SSO-Client端:处理所有SSO相关请求
* http://{host}:{port}/sso/login -- Client端登录地址,接受参数:back=登录后的跳转地址
* http://{host}:{port}/sso/logout -- Client端单点注销地址(isSlo=true时打开),接受参数:back=注销后的跳转地址
* http://{host}:{port}/sso/logoutCall -- Client端单点注销回调地址(isSlo=true时打开),此接口为框架回调,开发者无需关心
*/
@RequestMapping("/sso/*")
public Object ssoRequest() {
return SaSsoProcessor.instance.clientDister();
}
}
# ??
server:
port: 9002
# sa-token??
sa-token:
# SSO-????
sso:
# SSO-Server? ??????
auth-url: http://localhost:9000/sso/auth
# ??????????
is-slo: true
spring:
# Redis?? ?SSO?????????Redis??????
redis:
# Redis?????????0?
database: 1
# Redis?????
host: 182.92.5.218
# Redis???????
port: 6379
# Redis?????????????
password: 123456
这个客户端的配置其实就是sso单点登录的配置是一样的,只不过加了点拦截器的配置而已
这款插件可以使得token是jwt风格的
@Configuration
public class SaTokenConfigure {
// Sa-Token 整合 jwt (Simple 简单模式)
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple();
}
}
sa-token:
jwt-secret-key: ddhvsuidvsjkdgqwuidgwf
这个配置即可,还需要一个maven
cn.dev33
sa-token-jwt
1.34.0
更多插件配置可以看官网