diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.css b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.css new file mode 100644 index 00000000..f7be05c8 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.css @@ -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; } \ No newline at end of file diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.js b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.js new file mode 100644 index 00000000..887da539 --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/login.js @@ -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 -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); + diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/oauth2-authorize.html b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/oauth2-authorize.html new file mode 100644 index 00000000..c39fe6bc --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server-h5/oauth2-authorize.html @@ -0,0 +1,81 @@ + + + + Sa-OAuth2-Server 认证中心(前后端分离版) + + + + + +
+
+ + + + +
+ +
+ + + + + +
+ +
+ + +
+ This page is provided by Sa-Token-OAuth2 +
+
+ + + + + + + + diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java index afc5bd26..a81b32c7 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/SaOAuth2ServerController.java @@ -40,7 +40,7 @@ public class SaOAuth2ServerController { oauth2Server.doLoginHandle = (name, pwd) -> { if("sa".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); - return SaResult.ok(); + return SaResult.ok().set("satoken", StpUtil.getTokenValue()); } return SaResult.error("账号名或密码错误"); }; diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/h5/SaOAuth2ServerH5Controller.java b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/h5/SaOAuth2ServerH5Controller.java new file mode 100644 index 00000000..bf63123e --- /dev/null +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/h5/SaOAuth2ServerH5Controller.java @@ -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 + * + *

情况1:客户端未登录,返回 code=401,提示用户登录

+ *

情况2:请求的 scope 需要客户端手动确认授权,返回 code=411,提示用户手动确认

+ *

情况3:已登录且请求的 scope 已确认授权,返回 code=200,redirect_uri=最终重定向 url 地址(携带code码参数)

+ * + * @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); + } + +}