Jeecgboot对接单点登录流程
13
min3.0k
字2025-06-27
2025-10-24
-
OAuth: OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
OAuth2.0:对于用户相关的OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。
采用OAuth2.0标准协议来进行用户身份验证和获取用户授权,相对于之前的OAuth1.0协议,其认证流程更简单和安全。
应用场景
第三方应用授权登录:在APP或者网页接入一些第三方应用时,时长会需要用户登录另一个合作平台,比如QQ,微博,微信的授权登录。

原生app授权:app登录请求后台接口,为了安全认证,所有请求都带token信息,如果登录验证、请求后台数据。
前后端分离单页面应用(spa):前后端分离框架,前端请求后台数据,需要进行oauth2安全认证,比如使用vue、react后者h5开发的app。
名词定义
(1) Third-party application:第三方应用程序,本文中又称"客户端"(client),比如打开知乎,使用第三方登录,选择qq登录,这时候知乎就是客户端。
(2)HTTP service:HTTP服务提供商,本文中简称"服务提供商",即上例的qq。
(3)Resource Owner:资源所有者,本文中又称"用户"(user),即登录用户。
(4)User Agent:用户代理,本文中就是指浏览器。
(5)Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器。
(6)Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。
授权码模式
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。
(1)用户访问客户端,后者将前者导向认证服务器,假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(2)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌:GET /oauth/token?response_type=code&client_id=test&redirect_uri=重定向页面链接。请求成功返回code授权码,一般有效时间是10分钟。
(3)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。POST /oauth/token?response_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=重定向页面链接。请求成功返回access Token和refresh Token。
jeecgboot oauth2 接入流程
以下是jeecgboot框架接入maxkey的oauth2协议改造流程,流程改造后可以支撑maxkey的支持的任意单点登录协议。核心是通过extend AuthDefaultRequest 和 implements AuthSource 来实现自定义第三方平台的单点登录。
前言
jeecgboot集成了JustAuth框架来进行第三方登录,利用这个框架来进行Maxkey单点登录的接入。
注意:一个系统对应sso的一个应用,接入一个新的系统需要向SSO系统申请一个应用的id和secret!
1.先改造前端
前端需要新增一个maxkey的单点登录按钮,图标自行修改
前端文件路径:
src/layout/components/SocialCallback/index.vue
<div class="aui-flex-box">
<div class="aui-third-login">
<a title="oauth2" @click="onThirdLogin('MAXKEY')"><WechatFilled /></a>
</div>
</div>2.Justauth新增MAXKEY的配置项
maxkey的配置根据实际环境填写,回调url根据业务系统的实际环境修改。
一般需要修改开发环境和生产环境两个配置文件
- application-dev.yml
- application-prod.yml
justauth:
enabled: true
type:
MAXKEY:
server-url: http://127.0.0.1
client-id: 1153308756386250752
client-secret: gDjBMTQwODIwMjUwOTU4MjA1OTQHER
redirect-uri: http://127.0.0.1:8080/jeecg-boot/sys/thirdLogin/MAXKEY/callback3. 新增hutool-json依赖
在jeecg-system-biz的pom文件中添加依赖,路径如下:
jeecg-boot/jeecg-module-system/jeecg-system-biz/pom.xml
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>5.8.20</version>
</dependency>4. SpringContextUtils新增getProperty方法
在类中添加getProperty方法用来获取JustAuth的环境变量,路径如下:
src/main/java/org/jeecg/common/util/SpringContextUtils.java
public static String getProperty(String key) {
return null == getApplicationContext() ? null :
getApplicationContext().getEnvironment().getProperty(key);
}5. ISysThirdAccountService新增createUserBySysThirdAccount方法
/**
* 创建第三方用户
* @param phone 手机号
* @param sysThirdAccount 第三方账号
* @return SysUser
*/
SysUser createUserBySysThirdAccount(String phone, String username, SysThirdAccount sysThirdAccount);6. SysThirdAccountServiceImpl 重写createUserBySysThirdAccount方法
主要作用就是提供创建第三方账户信息和用户信息并将第三方账户信息表通过ThirdUserUuid和用户信息表中的id建立绑定关系。
@Override
public SysUser createUserBySysThirdAccount(String phone, String username, SysThirdAccount sysThirdAccount) {
//先查询第三方,获取登录方式
String thirdUserUuid = sysThirdAccount.getThirdUserUuid();
Integer tenantId = sysThirdAccount.getTenantId();
LambdaQueryWrapper<SysThirdAccount> query = new LambdaQueryWrapper<>();
query.eq(SysThirdAccount::getThirdUserUuid, thirdUserUuid);
query.eq(SysThirdAccount::getTenantId, sysThirdAccount.getTenantId());
SysThirdAccount account = sysThirdAccountMapper.selectOne(query);
//通过用户名查询数据库是否已存在
SysUser userByName = sysUserMapper.getUserByName(sysThirdAccount.getRealname());
if(null != userByName){
//如果账号存在的话,则自动加上一个时间戳
String format = DateUtils.yyyymmddhhmmss.get().format(new Date());
thirdUserUuid = sysThirdAccount.getThirdUserUuid() + format;
}
//添加用户
SysUser user = new SysUser();
user.setActivitiSync(CommonConstant.ACT_SYNC_1);
user.setDelFlag(CommonConstant.DEL_FLAG_0);
user.setStatus(1);
user.setUsername(sysThirdAccount.getRealname());
user.setPhone(phone);
//设置初始密码
String salt = oConvertUtils.randomGen(8);
user.setSalt(salt);
String passwordEncode = PasswordUtil.encrypt(user.getUsername(), "123456", salt);
user.setPassword(passwordEncode);
user.setRealname(username);
user.setAvatar(account.getAvatar());
String userid = UUIDGenerator.generate();
user.setId(userid);
sysUserMapper.insert(user);
//更新用户第三方账户表的userId
SysThirdAccount sysThirdAccountUpdate = new SysThirdAccount();
sysThirdAccountUpdate.setSysUserId(userid);
sysThirdAccountUpdate.setTenantId(tenantId);
sysThirdAccountMapper.update(sysThirdAccountUpdate,query);
return user;
}4. 新增Maxkey的OAuth配置类以及工具类
在这个路径下新增maxkey文件夹:
src/main/java/org/jeecg/modules/system

创建下面三个类
AuthMaxKeyRequest.java
package org.jeecg.modules.system.maxkey;
import cn.hutool.core.lang.Dict;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthDefaultRequest;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.util.SpringContextUtils;
import java.io.IOException;
/**
* maxkey
*/
public class AuthMaxKeyRequest extends AuthDefaultRequest {
private static final ObjectMapper OBJECT_MAPPER = SpringContextUtils.getBean(ObjectMapper.class);
public static final String SERVER_URL = SpringContextUtils.getProperty("justauth.type.maxkey.server-url");
/**
* 设定归属域
*/
public AuthMaxKeyRequest(AuthConfig config) {
super(config, AuthMaxKeySource.MAXKEY);
}
public AuthMaxKeyRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthMaxKeySource.MAXKEY, authStateCache);
}
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode());
Dict object = this.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
return AuthToken.builder()
.accessToken(object.getStr("access_token"))
.refreshToken(object.getStr("refresh_token"))
.idToken(object.getStr("id_token"))
.tokenType(object.getStr("token_type"))
.scope(object.getStr("scope"))
.build();
}
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken);
Dict object = this.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
// {
// "birthday" : null,
// "gender" : 0,
// "displayName" : "小明",
// "departmentId" : "1152634643468517376",
// "mobile" : null,
// "createdate" : 1754975989000,
// "title" : "应用开发工程师",
// "userId" : "1152634683125661696",
// "online_ticket" : "1153414562293219328",
// "employeeNumber" : "6000000",
// "realname" : "小明",
// "institution" : "1",
// "randomId" : "a053bdcb-38df-474b-a15c-9604f781e93b",
// "state" : null,
// "department" : "AI数智化中心",
// "user" : "6000000",
// "email" : null,
// "username" : "6000000"
//}
return AuthUser.builder()
.uuid(object.getStr("userId"))
.username(object.getStr("username"))
.nickname(object.getStr("displayName"))
.avatar(object.getStr("avatar_url"))
.blog(object.getStr("web_url"))
.company(object.getStr("organization"))
.location(object.getStr("location"))
.email(object.getStr("email"))
.remark(object.getStr("bio"))
.token(authToken)
.source(source.toString())
.build();
}
public Dict parseMap(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
try {
return OBJECT_MAPPER.readValue(text, OBJECT_MAPPER.getTypeFactory().constructType(Dict.class));
} catch (MismatchedInputException e) {
// 类型不匹配说明不是json
return null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}AuthMaxKeySource.java
package org.jeecg.modules.system.maxkey;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.request.AuthDefaultRequest;
/**
* Oauth2 默认接口说明
*
*/
public enum AuthMaxKeySource implements AuthSource {
/**
* 自己搭建的 maxkey 私服
*/
MAXKEY {
/**
* 授权的api
*/
@Override
public String authorize() {
return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/authorize";
}
/**
* 获取accessToken的api
*/
@Override
public String accessToken() {
return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/token";
}
/**
* 获取用户信息的api
*/
@Override
public String userInfo() {
return AuthMaxKeyRequest.SERVER_URL + "/sign/api/oauth/v20/me";
}
};
}SocialUtils.java
package org.jeecg.modules.system.maxkey;
import cn.hutool.core.util.ObjectUtil;
import com.xkcoding.justauth.AuthRequestFactory;
import com.xkcoding.justauth.autoconfigure.JustAuthProperties;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.*;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.modules.system.cache.AuthStateRedisCache;
/**
* 认证授权工具类
*
*/
public class SocialUtils {
private static final AuthStateRedisCache STATE_CACHE = SpringContextUtils.getBean(AuthStateRedisCache.class);
private static final JustAuthProperties JUST_AUTH_PROPERTIES = SpringContextUtils.getBean(JustAuthProperties.class);
private static final AuthRequestFactory FACTORY = SpringContextUtils.getBean(AuthRequestFactory.class);
@SuppressWarnings("unchecked")
public static AuthResponse<AuthUser> loginAuth(String source, String code, String state) throws AuthException {
AuthRequest authRequest = getAuthRequest(source);
AuthCallback callback = new AuthCallback();
callback.setCode(code);
callback.setState(state);
return authRequest.login(callback);
}
public static AuthRequest getAuthRequest(String source) throws AuthException {
// SocialLoginConfigProperties obj = socialProperties.getType().get(source);
AuthConfig authConfig = JUST_AUTH_PROPERTIES.getType().get(source);
if (ObjectUtil.isNull(authConfig)) {
throw new AuthException("不支持的第三方登录类型");
}
AuthRequest authRequest = switch (source.toLowerCase()) {
case "maxkey":
AuthConfig.AuthConfigBuilder builder = AuthConfig.builder()
.clientId(authConfig.getClientId())
.clientSecret(authConfig.getClientSecret())
.redirectUri(authConfig.getRedirectUri())
.scopes(authConfig.getScopes());
yield new org.jeecg.modules.system.maxkey.AuthMaxKeyRequest(builder.build(), STATE_CACHE);
default:
yield FACTORY.get(source);
};
return authRequest;
}
}改造单点跳转接口和回调接口
主要注意的逻辑是回调接口,因为原来框架的逻辑是需要校验该第三方用户表的ThirdUserId是否和用户表的id进行绑定,如果没有,则需要通过手机号进行绑定。而手机号必须存在于用户信息表中,所以对于未存在于手机号不会通过校验,那么单点就会失败。故我去除了这块逻辑,如果需要可以根据实际场景保留。
接口路径:src/main/java/org/jeecg/modules/system/controller/ThirdLoginController.java
需要改造render和loginThird这两个接口。
/**
* 主要作用是根据source来获取maxkey的登录认证地址
*/
@RequestMapping("/render/{source}")
public void render(@PathVariable("source") String source, HttpServletResponse response) throws IOException {
log.info("第三方登录进入render:" + source);
AuthRequest authRequest = SocialUtils.getAuthRequest(source);
String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());
log.info("第三方登录认证地址:" + authorizeUrl);
response.sendRedirect(authorizeUrl);
}
/**
* 用户在maxkey登录后回调该地址进行用户信息的校验和绑定,主要逻辑如下:
* 1.根据maxkey的回调参数进行用户信息查询
* 2.拿到用户信息后先校验第三方登录信息表中是否存在该用户信息,没有则创建,有则拿来直接创建token
* 3.如果没有改用户信息,则同步的在第三方登录信息表和用户信息表中创建该用户信息
*/
@RequestMapping("/{source}/callback")
public String loginThird(@PathVariable("source") String source, AuthCallback callback,ModelMap modelMap) {
log.info("第三方登录进入callback:" + source + " params:" + JSONObject.toJSONString(callback));
AuthResponse response = SocialUtils.loginAuth(source, callback.getCode(), callback.getState());
log.info(JSONObject.toJSONString(response));
Result<JSONObject> result = new Result<JSONObject>();
if(response.getCode()==2000) {
JSONObject data = JSONObject.parseObject(JSONObject.toJSONString(response.getData()));
String username = data.getString("username");
String nickname = data.getString("nickname");
String avatar = data.getString("avatar");
String uuid = data.getString("uuid");
//构造第三方登录信息存储对象
ThirdLoginModel tlm = new ThirdLoginModel(source, uuid, username, avatar);
//判断有没有这个人
LambdaQueryWrapper<SysThirdAccount> query = new LambdaQueryWrapper<SysThirdAccount>();
query.eq(SysThirdAccount::getThirdType, source);
query.eq(SysThirdAccount::getTenantId, CommonConstant.TENANT_ID_DEFAULT_VALUE);
query.and(q -> q.eq(SysThirdAccount::getThirdUserUuid, uuid).or().eq(SysThirdAccount::getThirdUserId, uuid));
List<SysThirdAccount> thridList = sysThirdAccountService.list(query);
SysThirdAccount user = null;
if(thridList==null || thridList.size()==0) {
//否则直接创建新账号
user = sysThirdAccountService.saveThirdUser(tlm,CommonConstant.TENANT_ID_DEFAULT_VALUE);
SysUser sysUser = sysThirdAccountService.createUserBySysThirdAccount("", nickname, user);
String token = saveToken(sysUser);
modelMap.addAttribute("token", token);
}else {
//已存在 只设置用户名 不设置头像
user = thridList.get(0);
SysUser sysUser = sysUserService.getById(user.getSysUserId());
String token = saveToken(sysUser);
modelMap.addAttribute("token", token);
}
}else{
modelMap.addAttribute("token", "登录失败");
}
result.setSuccess(false);
result.setMessage("第三方登录异常,请联系管理员");
return "thirdLogin";
}注意
如果从Maxkey系统单点直接跳转到Jeecgboot的前端,则还需要对前端代码进行改造
src/main/resources/templates/thirdLogin.ftl
<script>
window.onload = function () {
setTimeout(function (){
var thirdLoginInfo = "${token!''}";
if(!thirdLoginInfo){
var thirdLoginModel = '${thirdLoginModel!""}';
if(thirdLoginModel){
thirdLoginInfo = JSON.parse(thirdLoginModel);
thirdLoginInfo['isObj'] = true
}
}
window.opener.postMessage(thirdLoginInfo, "*");
window.close();
},1000)
}
</script>注意
- maxkey的应用配置缓存时间为1小时,1小时后自动刷新配置信息!