看本文前,请熟悉OAuth2的的基本概念及原理。本文主要讲解授权码模式的实战。同时请仔细阅读官方文档。
官方文档地址:OAuth2.0简述 - Sa-Token (dev33.cn)
Sa-Token OAuth2 可以帮你把oauth2的事情都搞定,你只需要实现自己的用户认证(doLogin方法)即可!不得不为作者是设计大大的点个赞!
注意:官方文档已经讲的非常明白。请按照官方文档流程搭建服务。本文会把一些需要额外注意的操作列举出来。
注意:此处相比官方文档,额外引入了 sa-token-dao-redis-jackson 和 common-pool2 相关依赖。实际开发及生产环境,也请加入该依赖。实际版本号,建议查阅官方最新版本号。(确保授权码和用户token的重启服务后丢失,以及解决无法在分布式环境中共享数据的问题)。
cn.dev33
sa-token-spring-boot-starter
1.31.0
cn.dev33
sa-token-oauth2
1.31.0
cn.dev33
sa-token-dao-redis-jackson
1.31.0
org.apache.commons
commons-pool2
2.11.1
新建 SaOAuth2TemplateImpl
/**
* Sa-Token OAuth2.0 整合实现
*/
@Component
public class SaOAuth2TemplateImpl extends SaOAuth2Template {
// 根据 id 获取 Client 信息
@Override
public SaClientModel getClientModel(String clientId) {
// 此为模拟数据,真实环境需要从数据库查询
if("1001".equals(clientId)) {
return new SaClientModel()
.setClientId("10001")
.setClientSecret("aaaa-bbbb-cccc-dddd-eeee")
.setAllowUrl("*")
.setContractScope("userinfo")
.setIsAutoMode(true);
}
return null;
}
// 根据ClientId 和 LoginId 获取openid
@Override
public String getOpenid(String clientId, Object loginId) {
// 此为模拟数据,真实环境需要从数据库查询
return "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__";
}
}
你可以在 框架配置 了解有关 SaClientModel
对象所有属性的详细定义
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
import cn.dev33.satoken.oauth2.config.SaOAuth2Config;
import cn.dev33.satoken.oauth2.logic.SaOAuth2Handle;
import cn.dev33.satoken.oauth2.logic.SaOAuth2Util;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.cloud.base.springcloudbaseserveroauth.pojo.request.LoginRequest;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* oAuth2 Server端 controller
*
* @author ZT
*/
@Slf4j
@RestController
@Api(value = "oAuth2 相关接口",tags = "oAuth2 相关接口")
public class Oauth2ServerController {
/**
* 统一认证地址
*/
@RequestMapping("/oauth2/authorize")
@ApiOperation(value = "统一认证地址",notes = "统一认证地址", produces = "application/json")
public Object authorize() {
log.info("------- 进入【统一认证地址】请求: " + SaHolder.getRequest().getUrl());
return SaOAuth2Handle.authorize(SaHolder.getRequest(), SaHolder.getResponse(), SaOAuth2Manager.getConfig());
}
/**
* 获取 Access-Token
*/
@RequestMapping("/oauth2/token")
@ApiOperation(value = "获取 Access-Token",notes = "获取 Access-Token", produces = "application/json")
public Object token() {
log.info("------- 进入【获取 Access-Token】请求: " + SaHolder.getRequest().getUrl());
return SaOAuth2Handle.token(SaHolder.getRequest(), SaHolder.getResponse(), SaOAuth2Manager.getConfig());
}
/**
* 刷新 Access-Token
*/
@RequestMapping("/oauth2/refresh")
@ApiOperation(value = "刷新 Access-Token",notes = "刷新 Access-Token", produces = "application/json")
public Object refresh() {
log.info("------- 进入【刷新 Access-Token】请求: " + SaHolder.getRequest().getUrl());
return SaOAuth2Handle.refreshToken(SaHolder.getRequest());
}
/**
* 回收 Access-Token
*/
@RequestMapping("/oauth2/revoke")
@ApiOperation(value = "回收 Access-Token",notes = "回收 Access-Token", produces = "application/json")
public Object revoke() {
log.info("------- 进入【回收 Access-Token】请求: " + SaHolder.getRequest().getUrl());
return SaOAuth2Handle.revokeToken(SaHolder.getRequest());
}
/**
* 登录接口
*/
@RequestMapping("/oauth2/doLogin")
@ApiOperation(value = "登录接口",notes = "登录接口", produces = "application/json")
public Object doLogin(LoginRequest loginRequest) {
log.info("------- 进入请求: " + SaHolder.getRequest().getUrl());
// 注意,此处是一个Demo写法,判断登陆类型为1通过,其它都为false
// 实际生产中。我们的登陆是支持多种登陆方式的。例如:账号+密码,手机—+验证码,扫码登陆,SDK唤醒
if ("1".equals(loginRequest.getLoginType())) {
StpUtil.login("10001");
return null;
} else if ("2".equals(loginRequest.getLoginType())) {
throw new RuntimeException("登陆失败");
}
return new RuntimeException("无效的请求,请查收");
}
/**
* 确认授权接口
*/
@RequestMapping("/oauth2/doConfirm")
@ApiOperation(value = "确认授权接口",notes = "确认授权接口", produces = "application/json")
public Object doConfirm() {
log.info("------- 进入【确认授权接口】请求: " + SaHolder.getRequest().getUrl());
return SaOAuth2Handle.doConfirm(SaHolder.getRequest());
}
/**
* Sa-OAuth2 定制化配置
*/
@Autowired
public void setSaOAuth2Config(SaOAuth2Config cfg) {
cfg.
// 配置:未登录时返回的View
setNotLoginView(() -> {
String msg = "当前会话在SSO-Server端尚未登录,请先访问"
+ " doLogin登录 "
+ "进行登录l之后,刷新页面开始授权";
return msg;
}).
// // 此处是官方demo配置:登录处理函数。我们实际是自定义登陆方法
// setDoLoginHandle((name, pwd) -> {
// if ("sa".equals(name) && "123456".equals(pwd)) {
// StpUtil.login(10001);
// return SaResult.ok();
// }
// return SaResult.error("账号名或密码错误");
// }).
// 配置:确认授权时返回的View
setConfirmView((clientId, scope) -> {
String msg = "应用 " + clientId + " 请求授权:" + scope + "
"
+ "请确认: 确认授权
"
+ "确认之后刷新页面
";
return msg;
})
;
}
/**
* 获取Userinfo信息:昵称、头像、性别等等
*/
@RequestMapping("/oauth2/userinfo")
public SaResult userinfo() {
// 获取 Access-Token 对应的账号id
String accessToken = SaHolder.getRequest().getParamNotNull("access_token");
Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken);
log.info("-------- 此Access-Token对应的账号id: " + loginId);
// 校验 Access-Token 是否具有权限: userinfo
SaOAuth2Util.checkScope(accessToken, "userinfo");
// 模拟账号信息 (真实环境需要查询数据库获取信息)
Map map = new LinkedHashMap();
map.put("nickname", "shengzhang_");
map.put("avatar", "http://xxx.com/1.jpg");
map.put("age", "18");
map.put("sex", "男");
map.put("address", "山东省 青岛市 城阳区");
return SaResult.data(map);
}
}
# Sa-Token 配置
sa-token:
token-name: satoken-server
# OAuth2.0 配置
oauth2:
# 是否打开模式:授权码(Authorization Code)
is-code: true
# 是否打开模式:隐藏式(Implicit)
is-implicit: false
# 是否打开模式:密码式(Password)
is-password: false
# 是否打开模式:凭证式(Client Credentials)
is-client: false
# 是否在每次 Refresh-Token 刷新 Access-Token 时,产生一个新的 Refresh-Token
isNewRefresh: false
# Code授权码 保存的时间(单位:秒) 默认五分钟
codeTimeout: 300
# Access-Token 保存的时间(单位:秒)默认两个小时
accessTokenTimeout: 7200
# Refresh-Token 保存的时间(单位:秒) 默认30 天
refreshTokenTimeout: 2592000
# Client-Token 保存的时间(单位:秒) 默认两个小时
clientTokenTimeout: 7200
# Past-Client-Token 保存的时间(单位:秒) ,默认为-1,代表延续 Client-Token 的有效时间
pastClientTokenTimeout: 7200
6.1 由于暂未搭建Client端,我们可以使用百度官网作为重定向URL进行测试:
ip+端口+你baseUrl+/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=https://www.baidu.com&scope=userinfo
6.2 由于首次访问,我们在OAuth-Server端暂未登录,会被转发到登录视图
6.3 访问doLogin接口进行登录之后
ip+端口+你的baseUrl+/oauth2/doLogin?loginType=1
6.4刷新<6.2>步骤的页面
6.5 点击确认授权
6.6 刷新<6.4>步骤的页面,我们会被重定向至 redirect_uri 页面,并携带了code参数
6.7 我们拿着code参数,访问以下地址:
ip+端口+你baseUrl+/oauth2/token?grant_type=authorization_code&client_id=1001&client_secret=aaaa-bbbb-cccc-dddd-eeee&code={code}
将得到 Access-Token
、Refresh-Token
、openid
等授权信息
测试完毕
问:我想静默授权,不希望用户点击授权,怎么办?
答:scope参数为空,或者请求的权限用户近期已确认过,则无需用户再次确认,达到静默授权的效果,否则需要用户手动确认,服务器才可以下放code授权码。
问:哪些功能是封装好的,哪些是核心需要实现的?
答:/oauth2/doLogin() 是需要我们核心实现的方法,其它的/oauth2/xxx方法,交给框架处理。
问:我看官方Demo,一个 @RequestMapping("/oauth2/*") 把请求全部处理了,你这里拆开怎么说?
答:在 cn.dev33.satoken.oauth2.logic 包下的 SaOAuth2Consts 类内,定义了所有的Api接口,所有的参数名称,所有的返回类型,所有的授权类型。官方demo用/oauth2/*处理了所有请求,我们参考源码,其实每个api接口都有自己对应的方法调用。所以,我们可以自定义接口,自定接口Url,只需要调用对应的Api方法即可(参考本文调用的各方法)。
问:授权码签发成功后,签发jwt可以么?
答:不合适。jwt是用户在 oauth-server端的敏感数据,不应该透露给client端。
问:授权码签发成功后,怎么让用户持续交互呢?不能一直拿access_token + open_id 一直交互吧?
答:OAuth-Client 的后端调用 OAuth-Server 的后端,用 Access-Token。OAuth-Client 的前端调用 OAuth-Client 的后端,根据openid找到userId,然后根据userId生成的token(StpUtil.login(userId))。简单来说,oauth2本身就是授权签发用户非敏感信息,不是做长交互的。
问: OAuth-Client 端在自己的数据库中创建的用户,绑定了open_id后,怎么交给 OAuth-Server 的后端去识别验证用户呢?
答:client的用户为啥要交给OAuth-Server端来验证呢?我们用QQ快速登陆了CSDN,难道CSDN判断是否登陆,要从QQ服务器验证用户合法性么?