mirror of
https://gitee.com/dromara/sa-token.git
synced 2025-05-07 06:07:56 +08:00
feat(oauth2): 新增 oauth2 server 端前后台分离示例
This commit is contained in:
parent
59253dc6f0
commit
c91412e542
@ -0,0 +1,48 @@
|
|||||||
|
*{margin: 0; padding: 0;}
|
||||||
|
body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}
|
||||||
|
::-webkit-input-placeholder{color: #ccc;}
|
||||||
|
|
||||||
|
/* 视图盒子 */
|
||||||
|
.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}
|
||||||
|
/* 背景 EAEFF3 */
|
||||||
|
.bg-1{height: 100%; background: #E4B17F;}
|
||||||
|
|
||||||
|
/* 内容盒子 */
|
||||||
|
.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}
|
||||||
|
.content-box{display: none;}
|
||||||
|
.region-default{display: block;}
|
||||||
|
.message-box{width: 100%;; text-align: center;}
|
||||||
|
|
||||||
|
/* 登录盒子 */
|
||||||
|
/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */
|
||||||
|
.login-box{width: 450px; margin: auto; max-width: 90%; height: 100%;}
|
||||||
|
.login-box{display: flex; align-items: center; text-align: center;}
|
||||||
|
|
||||||
|
/* 表单 */
|
||||||
|
.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}
|
||||||
|
.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}
|
||||||
|
.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}
|
||||||
|
|
||||||
|
/* 输入框 */
|
||||||
|
.from-item{border: 0px #000 solid; margin-bottom: 15px;}
|
||||||
|
.s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;}
|
||||||
|
.s-input{font-size: 12px;}
|
||||||
|
.s-input:focus{border-color: #409eff}
|
||||||
|
|
||||||
|
/* 登录按钮 */
|
||||||
|
.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}
|
||||||
|
.s-btn:hover{background-color: #50aEFF;}
|
||||||
|
|
||||||
|
/* 重置按钮 */
|
||||||
|
.reset-box{text-align: left; font-size: 12px;}
|
||||||
|
.reset-box a{text-decoration: none;}
|
||||||
|
.reset-box a:hover{text-decoration: underline;}
|
||||||
|
|
||||||
|
/* 确认授权按钮 */
|
||||||
|
.confirm-btn{text-indent: 0; cursor: pointer; background-color: #409EFF; border: 1px #409EFF solid; color: #FFF; padding: 5px 15px;}
|
||||||
|
.confirm-btn:hover{background-color: #50aEFF;}
|
||||||
|
|
||||||
|
/* loading框样式 */
|
||||||
|
.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}
|
||||||
|
.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}
|
||||||
|
.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }
|
@ -0,0 +1,194 @@
|
|||||||
|
// OAuth-Server 后端 接口地址
|
||||||
|
var baseUrl = "http://sa-oauth-server.com:8000";
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------- 相关事件 -----------------------------------
|
||||||
|
|
||||||
|
// 显示默认区域
|
||||||
|
function showDefaultRegion(){
|
||||||
|
$('.content-box').hide();
|
||||||
|
$('.region-default').show();
|
||||||
|
}
|
||||||
|
// 显示登录框区域
|
||||||
|
function showLoginRegion(){
|
||||||
|
$('.content-box').hide();
|
||||||
|
$('.region-login').show();
|
||||||
|
}
|
||||||
|
// 显示确认授权框区域
|
||||||
|
function showConfirmRegion(){
|
||||||
|
$('.content-box').hide();
|
||||||
|
$('.region-confirm').show();
|
||||||
|
$('.show-clientId').text(getParam('client_id'));
|
||||||
|
$('.show-scope').text(getParam('scope'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查当前是否已经登录,如果已登录则直接开始跳转,如果未登录则等待用户输入账号密码
|
||||||
|
function tryJump(){
|
||||||
|
var data = location.search.substr(1);
|
||||||
|
sa.ajax("/oauth2/getRedirectUri", data, function(res) {
|
||||||
|
// 情况1:客户端未登录,返回 code=401,提示用户登录
|
||||||
|
if(res.code === 401) {
|
||||||
|
showLoginRegion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情况2:请求的 scope 需要客户端手动确认授权,返回 code=411,提示用户手动确认
|
||||||
|
if(res.code === 411) {
|
||||||
|
showConfirmRegion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情况3:已登录且请求的 scope 已确认授权,返回 code=200,data=最终重定向 url 地址(携带code码参数)
|
||||||
|
if(res.code == 200) {
|
||||||
|
console.log('跳转:', res.redirect_uri);
|
||||||
|
location.href = res.redirect_uri;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('未知状态码,', res.code, res);
|
||||||
|
layer.alert('错误:' + JSON.stringify(res))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录事件
|
||||||
|
function doLogin() {
|
||||||
|
// 开始登录
|
||||||
|
var data = {
|
||||||
|
name: $('[name=name]').val(),
|
||||||
|
pwd: $('[name=pwd]').val()
|
||||||
|
};
|
||||||
|
sa.ajax("/oauth2/doLogin", data, function(res) {
|
||||||
|
if(res.code == 200) {
|
||||||
|
localStorage.setItem('satoken', res.satoken);
|
||||||
|
layer.msg('登录成功', {anim: 0, icon: 6 });
|
||||||
|
setTimeout(function() {
|
||||||
|
location.reload();
|
||||||
|
}, 800);
|
||||||
|
} else {
|
||||||
|
layer.msg(res.msg, {anim: 6, icon: 2 });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认授权事件
|
||||||
|
function yes() {
|
||||||
|
var data = location.search.substr(1) + '&build_redirect_uri=true';
|
||||||
|
sa.ajax("/oauth2/doConfirm", data, function(res) {
|
||||||
|
if(res.code == 200) {
|
||||||
|
layer.msg('确认授权成功,即将跳转...', {anim: 0, icon: 6 });
|
||||||
|
setTimeout(function() {
|
||||||
|
console.log('跳转:', res.redirect_uri);
|
||||||
|
location.href = res.redirect_uri;
|
||||||
|
}, 800);
|
||||||
|
} else {
|
||||||
|
layer.msg(res.msg, {anim: 6, icon: 2 });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拒绝授权事件
|
||||||
|
function no() {
|
||||||
|
var url = joinParam(getParam('redirect_uri'), "handle=refuse&msg=用户拒绝了授权");
|
||||||
|
location.href = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完毕后触发
|
||||||
|
window.onload = function() {
|
||||||
|
tryJump();
|
||||||
|
|
||||||
|
// 绑定回车事件
|
||||||
|
$('[name=name],[name=pwd]').bind('keypress', function(event){
|
||||||
|
if(event.keyCode == "13") {
|
||||||
|
$('.login-btn').click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 输入框获取焦点
|
||||||
|
$("[name=name]").focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------- 工具函数封装 -----------------------------------
|
||||||
|
|
||||||
|
// sa
|
||||||
|
var sa = {};
|
||||||
|
|
||||||
|
// 打开loading
|
||||||
|
sa.loading = function(msg) {
|
||||||
|
layer.closeAll(); // 开始前先把所有弹窗关了
|
||||||
|
return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 隐藏loading
|
||||||
|
sa.hideLoading = function() {
|
||||||
|
layer.closeAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 封装一下Ajax
|
||||||
|
sa.ajax = function(url, data, successFn) {
|
||||||
|
sa.loading("加载中...");
|
||||||
|
$.ajax({
|
||||||
|
url: baseUrl + url,
|
||||||
|
type: "post",
|
||||||
|
data: data,
|
||||||
|
dataType: 'json',
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'satoken': localStorage.getItem('satoken')
|
||||||
|
},
|
||||||
|
success: function(res){
|
||||||
|
sa.hideLoading();
|
||||||
|
console.log('返回数据:', res);
|
||||||
|
successFn(res);
|
||||||
|
},
|
||||||
|
error: function(xhr, type, errorThrown){
|
||||||
|
sa.hideLoading();
|
||||||
|
if(xhr.status == 0){
|
||||||
|
return alert('无法连接到服务器,请检查网络');
|
||||||
|
}
|
||||||
|
return alert("异常:" + JSON.stringify(xhr));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从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] + (pair[2] ? '=' + pair[2] : '');}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印信息
|
||||||
|
var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.cc/";
|
||||||
|
console.log(str);
|
||||||
|
|
@ -0,0 +1,81 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<title>Sa-OAuth2-Server 认证中心(前后端分离版)</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<link rel="stylesheet" href="./login.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="view-box">
|
||||||
|
<div class="bg-1"></div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
将页面分为三块区域:
|
||||||
|
- 未登录时显示区域2:登录框。
|
||||||
|
- 已登录但请求的 scope 尚未手动确认授权,显示区域3:确认授权框。
|
||||||
|
- 默认显示区域1:提示文字。
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- 区域1:默认显示 -->
|
||||||
|
<div class="content-box region-default">
|
||||||
|
<div class="login-box">
|
||||||
|
<div class="message-box">
|
||||||
|
加载中...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 区域2:登录框 -->
|
||||||
|
<div class="content-box region-login">
|
||||||
|
<div class="login-box">
|
||||||
|
<div class="from-box">
|
||||||
|
<h2 class="from-title">Sa-OAuth2-Server 认证中心(前后端分离版)</h2>
|
||||||
|
<div class="from-item">
|
||||||
|
<input class="s-input" name="name" placeholder="请输入账号" />
|
||||||
|
</div>
|
||||||
|
<div class="from-item">
|
||||||
|
<input class="s-input" name="pwd" type="password" placeholder="请输入密码" />
|
||||||
|
</div>
|
||||||
|
<div class="from-item">
|
||||||
|
<button class="s-input s-btn login-btn" onclick="doLogin()">登录</button>
|
||||||
|
</div>
|
||||||
|
<div class="from-item reset-box">
|
||||||
|
<a href="javascript: location.reload();" >刷新</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 区域3:确认授权框 -->
|
||||||
|
<div class="content-box region-confirm">
|
||||||
|
<div class="login-box">
|
||||||
|
<div class="from-box">
|
||||||
|
<h2 class="from-title">Sa-OAuth2-Server 认证中心(前后端分离版)</h2><br>
|
||||||
|
<div>
|
||||||
|
<div><b>应用ID:</b><span class="show-clientId"></span></div>
|
||||||
|
<div><b>请求授权:</b><span class="show-scope"></span></div>
|
||||||
|
<br><br><div>------------- 是否同意授权 -------------</div><br>
|
||||||
|
<div>
|
||||||
|
<button class="confirm-btn" onclick="yes()">同意</button>
|
||||||
|
<button class="confirm-btn" onclick="no()">拒绝</button>
|
||||||
|
</div>
|
||||||
|
<div style="height: 10px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部 版权 -->
|
||||||
|
<div style="position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;">
|
||||||
|
This page is provided by Sa-Token-OAuth2
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- scripts -->
|
||||||
|
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||||
|
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
|
||||||
|
<script src="./login.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -40,7 +40,7 @@ public class SaOAuth2ServerController {
|
|||||||
oauth2Server.doLoginHandle = (name, pwd) -> {
|
oauth2Server.doLoginHandle = (name, pwd) -> {
|
||||||
if("sa".equals(name) && "123456".equals(pwd)) {
|
if("sa".equals(name) && "123456".equals(pwd)) {
|
||||||
StpUtil.login(10001);
|
StpUtil.login(10001);
|
||||||
return SaResult.ok();
|
return SaResult.ok().set("satoken", StpUtil.getTokenValue());
|
||||||
}
|
}
|
||||||
return SaResult.error("账号名或密码错误");
|
return SaResult.error("账号名或密码错误");
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
package com.pj.oauth2.h5;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.context.SaHolder;
|
||||||
|
import cn.dev33.satoken.context.model.SaRequest;
|
||||||
|
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
|
||||||
|
import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;
|
||||||
|
import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;
|
||||||
|
import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate;
|
||||||
|
import cn.dev33.satoken.oauth2.data.model.AccessTokenModel;
|
||||||
|
import cn.dev33.satoken.oauth2.data.model.CodeModel;
|
||||||
|
import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;
|
||||||
|
import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;
|
||||||
|
import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;
|
||||||
|
import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor;
|
||||||
|
import cn.dev33.satoken.oauth2.template.SaOAuth2Template;
|
||||||
|
import cn.dev33.satoken.util.SaResult;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sa-Token OAuth2 Server端 控制器 (前后端分离情形下所需要的接口)
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
public class SaOAuth2ServerH5Controller {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最终授权重定向地址,形如:http://xxx.com/xxx?code=xxxxx
|
||||||
|
*
|
||||||
|
* <p> 情况1:客户端未登录,返回 code=401,提示用户登录 <p/>
|
||||||
|
* <p> 情况2:请求的 scope 需要客户端手动确认授权,返回 code=411,提示用户手动确认 <p/>
|
||||||
|
* <p> 情况3:已登录且请求的 scope 已确认授权,返回 code=200,redirect_uri=最终重定向 url 地址(携带code码参数) <p/>
|
||||||
|
*
|
||||||
|
* @return /
|
||||||
|
*/
|
||||||
|
@PostMapping("/oauth2/getRedirectUri")
|
||||||
|
public Object getRedirectUri() {
|
||||||
|
|
||||||
|
// 获取变量
|
||||||
|
SaRequest req = SaHolder.getRequest();
|
||||||
|
SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig();
|
||||||
|
SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate();
|
||||||
|
SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate();
|
||||||
|
String responseType = req.getParamNotNull(SaOAuth2Consts.Param.response_type);
|
||||||
|
|
||||||
|
// 1、先判断是否开启了指定的授权模式
|
||||||
|
SaOAuth2ServerProcessor.instance.checkAuthorizeResponseType(responseType, req, cfg);
|
||||||
|
|
||||||
|
// 2、如果尚未登录, 则先去登录
|
||||||
|
long loginId = SaOAuth2Manager.getStpLogic().getLoginId(0L);
|
||||||
|
if(loginId == 0L) {
|
||||||
|
return SaResult.get(401, "need login", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3、构建请求 Model
|
||||||
|
RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId);
|
||||||
|
|
||||||
|
// 4、校验:重定向域名是否合法
|
||||||
|
oauth2Template.checkRedirectUri(ra.clientId, ra.redirectUri);
|
||||||
|
|
||||||
|
// 5、校验:此次申请的Scope,该Client是否已经签约
|
||||||
|
oauth2Template.checkContractScope(ra.clientId, ra.scopes);
|
||||||
|
|
||||||
|
// 6、判断:如果此次申请的Scope,该用户尚未授权,则转到授权页面
|
||||||
|
boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes);
|
||||||
|
if(isNeedCarefulConfirm) {
|
||||||
|
// code=411,需要用户手动确认授权
|
||||||
|
return SaResult.get(411, "need confirm", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7、判断授权类型,重定向到不同地址
|
||||||
|
// 如果是 授权码式,则:开始重定向授权,下放code
|
||||||
|
if(SaOAuth2Consts.ResponseType.code.equals(ra.responseType)) {
|
||||||
|
CodeModel codeModel = dataGenerate.generateCode(ra);
|
||||||
|
String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state);
|
||||||
|
return SaResult.ok().set("redirect_uri", redirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是 隐藏式,则:开始重定向授权,下放 token
|
||||||
|
if(SaOAuth2Consts.ResponseType.token.equals(ra.responseType)) {
|
||||||
|
AccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null);
|
||||||
|
String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state);
|
||||||
|
return SaResult.ok().set("redirect_uri", redirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认返回
|
||||||
|
throw new SaOAuth2Exception("无效 response_type: " + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user