mirror of
https://gitee.com/dromara/sa-token.git
synced 2025-09-20 02:29:27 +08:00
OAuth2.0 beta ..
This commit is contained in:
@@ -3,8 +3,9 @@ package com.pj;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
|
||||
/**
|
||||
* 启动
|
||||
* 启动:OAuth2-Client端
|
||||
* @author kong
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@@ -12,7 +13,13 @@ public class SaOAuth2ClientApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SaOAuth2ClientApplication.class, args);
|
||||
System.out.println("\n客户端启动成功,访问: http://localhost:8002/login.html");
|
||||
System.out.println("\nSa-Token-OAuth Client端启动成功\n\n" + str);
|
||||
}
|
||||
|
||||
static String str = "首先在host文件 (C:\\WINDOWS\\system32\\drivers\\etc\\hosts) 添加以下内容: \r\n" +
|
||||
" 127.0.0.1 sa-oauth-server.com \r\n" +
|
||||
" 127.0.0.1 sa-oauth-client.com \r\n" +
|
||||
"再从浏览器访问:\r\n" +
|
||||
" http://sa-oauth-client.com:8002";
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,188 @@
|
||||
package com.pj.oauth2;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
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 com.pj.utils.SoMap;
|
||||
|
||||
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 = "aaaa-bbbb-cccc-dddd-eeee"; // 应用秘钥
|
||||
private String serverUrl = "http://sa-oauth-server.com: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("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;
|
||||
}
|
||||
|
||||
}
|
@@ -33,12 +33,10 @@ public class SoMap extends LinkedHashMap<String, Object> {
|
||||
public SoMap() {
|
||||
}
|
||||
|
||||
|
||||
/** 以下元素会在isNull函数中被判定为Null, */
|
||||
public static final Object[] NULL_ELEMENT_ARRAY = {null, ""};
|
||||
public static final List<Object> NULL_ELEMENT_LIST;
|
||||
|
||||
|
||||
static {
|
||||
NULL_ELEMENT_LIST = Arrays.asList(NULL_ELEMENT_ARRAY);
|
||||
}
|
||||
@@ -144,6 +142,22 @@ public class SoMap extends LinkedHashMap<String, Object> {
|
||||
return getDateByFormat(key, "yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/** 转为Map并返回 */
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public SoMap getMap(String key) {
|
||||
Object value = get(key);
|
||||
if(value == null) {
|
||||
return SoMap.getSoMap();
|
||||
}
|
||||
if(value instanceof Map) {
|
||||
return SoMap.getSoMap((Map)value);
|
||||
}
|
||||
if(value instanceof String) {
|
||||
return SoMap.getSoMap().setJsonString((String)value);
|
||||
}
|
||||
throw new RuntimeException("值无法转化为SoMap: " + value);
|
||||
}
|
||||
|
||||
/** 获取集合(必须原先就是个集合,否则会创建个新集合并返回) */
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Object> getList(String key) {
|
||||
|
@@ -5,9 +5,3 @@ server:
|
||||
sa-token:
|
||||
# token名称 (同时也是cookie名称)
|
||||
token-name: satoken-client
|
||||
|
||||
spring:
|
||||
# 静态文件路径映射
|
||||
resources:
|
||||
static-locations: classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/
|
||||
# static-locations: file:E:\work\project-yun\sa-token\sa-token-demo-oauth2\sa-token-demo-oauth2-client\src\main\resources\static\
|
||||
|
@@ -0,0 +1,253 @@
|
||||
<!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>当前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://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/">
|
||||
<button>点我开始授权登录(静默授权)</button>
|
||||
</a>
|
||||
<span class="ps">当请求链接不包含scope权限时,将无需用户手动确认,做到静默授权,当然此时我们也只能获取openid</span>
|
||||
<code>http://sa-oauth-server.com:8001/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/</code>
|
||||
|
||||
<a href="http://sa-oauth-server.com: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://sa-oauth-server.com: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://sa-oauth-server.com: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 置换资源: 获取账号昵称、头像、性别等信息 </span>
|
||||
<code>http://sa-oauth-server.com:8001/oauth2/userinfo?access_token={value}</code>
|
||||
|
||||
<br>
|
||||
<h3>模式二:隐藏式(Implicit)</h3>
|
||||
<a href="http://sa-oauth-server.com: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 )</span>
|
||||
<code>http://sa-oauth-server.com: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://sa-oauth-server.com:8001/oauth2/token?grant_type=password&client_id={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://sa-oauth-server.com:8001/oauth2/client_token?grant_type=client_credentials&client_id={value}&client_secret={value}</code>
|
||||
|
||||
<br><br>
|
||||
<span>更多资料请参考 Sa-Token 官方文档地址:</span>
|
||||
<a href="http://sa-token.dev33.cn/">http://sa-token.dev33.cn/</a>
|
||||
|
||||
<div style="height: 200px;"></div>
|
||||
</div>
|
||||
<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>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) {
|
||||
doLogin(code);
|
||||
}
|
||||
|
||||
// 根据 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) {
|
||||
layer.alert(JSON.stringify(res.data));
|
||||
},
|
||||
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>
|
Reference in New Issue
Block a user