feat(iam): 单点登录

This commit is contained in:
yjz
2023-10-17 14:19:36 +08:00
parent 8d112db4fc
commit 05163f1b03
10 changed files with 473 additions and 6 deletions
@@ -3,7 +3,6 @@ package com.jero.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
@@ -20,7 +19,11 @@ public class RestTemplateConfig {
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
/**
* SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
*/
// 解决https请求证书问题
SSL factory = new SSL();
factory.setReadTimeout(5000);//ms
factory.setConnectTimeout(15000);//ms
return factory;
@@ -0,0 +1,72 @@
package com.jero.config;
import com.jero.common.exception.JeroBootException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import javax.net.ssl.*;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
/**
* @author nzh
* @date 2022/8/22 16:45
*/
@Slf4j
public class SSL extends SimpleClientHttpRequestFactory {
@Override
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
if (connection instanceof HttpsURLConnection) {
prepareHttpsConnection((HttpsURLConnection) connection);
}
super.prepareConnection(connection, httpMethod);
}
private void prepareHttpsConnection(HttpsURLConnection connection) {
connection.setHostnameVerifier(new SkipHostnameVerifier());
try {
connection.setSSLSocketFactory(createSslSocketFactory());
} catch (Exception ex) {
// Ignore
}
}
private SSLSocketFactory createSslSocketFactory() throws JeroBootException, NoSuchAlgorithmException, KeyManagementException {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[] { new SkipX509TrustManager() }, new SecureRandom());
return context.getSocketFactory();
}
private class SkipHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String s, SSLSession sslSession) {
log.info("checkClientTrusted");
return true;
}
}
private static class SkipX509TrustManager implements X509TrustManager {
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
log.info("checkClientTrusted");
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
log.info("checkServerTrusted");
}
}
}
@@ -76,6 +76,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除
filterChainDefinitionMap.put("/sys/getRSAPublicKey", "anon"); //获取RSA公钥接口排除
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
filterChainDefinitionMap.put("/iam/loginByOauth2", "anon"); //单点登录接口排除
filterChainDefinitionMap.put("/sys/mLogin", "anon"); //登录接口排除
filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
filterChainDefinitionMap.put("/sys/thirdLogin/**", "anon"); //第三方登录
@@ -0,0 +1,68 @@
package com.jero.modules.system.entity;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.validation.constraints.NotBlank;
/**
* @Author: 31318
* @Date: 2023/09/26/10:07
* @Description:
*/
@Data
@Component
@ConfigurationProperties(prefix = "oauth")
public class Oauth {
/**
* 客户端应用注册ID
*/
@ApiModelProperty(value = "客户端应用注册ID")
private String clientId;
/**
* 客户端应用注册密钥
*/
@ApiModelProperty(value = "客户端应用注册密钥")
private String clientSecret;
/**
* authorization_code4种验证方式(authorization_codepasswordrefresh_tokenclient_credentials),采用授权码验证)
*/
@ApiModelProperty(value = "authorization_code4种验证方式(authorization_codepasswordrefresh_tokenclient_credentials),采用授权码验证)")
private String grantType;
/**
* 必需参数。授权码
*/
@ApiModelProperty(value = "必需参数。授权码")
@NotBlank(message = "授权码不能为空!")
private String code;
/**
* 重定向URI
*/
@ApiModelProperty(value = "重定向URI")
@NotBlank(message = "重定向URI不能为空!")
private String redirectUri;
/**
* 通过授权码获取token请求地址
*/
@ApiModelProperty(value = "通过授权码获取token请求地址")
private String accessTokenUrl;
/**
* 通过accessToken获取账号信息请求地址
*/
@ApiModelProperty(value = "通过accessToken获取用户信息请求地址")
private String userInfoUrl;
/**
* 防止跨站请求伪造(CSRF)标识
*/
@ApiModelProperty(value = "防止跨站请求伪造(CSRF)标识")
private String state;
}
@@ -0,0 +1,58 @@
package com.jero.modules.system.entity;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @Author: 31318
* @Date: 2023/09/26/10:42
* @Description:
*/
@Data
public class OauthAccessToken {
/** -----------正确响应返回结果----------- */
/**
* 由授权服务器分发的访问令牌
*/
@JsonAlias("access_token")
@ApiModelProperty(value = "由授权服务器分发的访问令牌")
private String accessToken;
/**
* 访问令牌生命周期的秒数
*/
@JsonAlias("expires_in")
@ApiModelProperty(value = "访问令牌生命周期的秒数")
private String expiresIn;
/**
*用来获取新的访问令牌的刷新令牌,使用相同的终端用户访问许可
*/
@JsonAlias("refresh_token")
@ApiModelProperty(value = "可用来获取新的访问令牌的刷新令牌,使用相同的终端用户访问许可")
private String refreshToken;
/**
* 登录用户uid
*/
@JsonAlias("uid")
@ApiModelProperty(value = "登录用户uid")
private String uId;
/** -----------错误响应返回结果----------- */
/**
* 错误码
*/
@JsonAlias("errcode")
@ApiModelProperty(value = "错误码")
private String errCode;
/**
* 错误信息
*/
@ApiModelProperty(value = "错误信息")
private String msg;
}
@@ -0,0 +1,80 @@
package com.jero.modules.system.entity;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
* @Author: 31318
* @Date: 2023/09/26/11:14
* @Description:
*/
@Data
public class OauthUserInfo {
/** -----------正确响应返回结果----------- */
/**
* 邮箱
*/
@ApiModelProperty(value = "邮箱")
private String mail;
/**
* 性别
*/
@ApiModelProperty(value = "性别")
private Integer gender;
/**
*
*/
@ApiModelProperty(value = "")
private String loginType;
/**
* 名称
*/
@ApiModelProperty(value = "名称")
private String displayName;
/**
*
*/
@ApiModelProperty(value = "")
private String title;
/**
* 账号名
*/
@ApiModelProperty(value = "账号名")
private List<String> spRoleList;
/**
* 联系方式
*/
@ApiModelProperty(value = "联系方式")
private String mobile;
/**
*
*/
@ApiModelProperty(value = "")
private List<String> spNameList;
/** -----------错误响应返回结果----------- */
/**
* 错误码
*/
@JsonAlias("errcode")
@ApiModelProperty(value = "错误码")
private String errCode;
/**
* 错误信息
*/
@ApiModelProperty(value = "错误信息")
private String msg;
}
@@ -2,6 +2,7 @@ package com.jero.modules.system.service;
import com.alibaba.fastjson.JSONObject;
import com.jero.common.api.vo.Result;
import com.jero.modules.system.entity.Oauth;
import com.jero.modules.system.entity.SysUser;
import com.jero.modules.system.model.SysLoginModel;
import org.apache.poi.ss.formula.functions.T;
@@ -27,6 +28,13 @@ public interface ILoginService{
*/
public Result<JSONObject> login(SysLoginModel sysLoginModel);
/**
*
* @param oauth
* @return
*/
public Result<JSONObject> loginByOauth2(Oauth oauth);
/**
* 退出登录
* @param request response
@@ -1,14 +1,15 @@
package com.jero.modules.system.service.impl;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.asymmetric.RSA;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.exceptions.ClientException;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jero.common.api.vo.Result;
import com.jero.common.constant.CacheConstant;
import com.jero.common.constant.CommonConstant;
import com.jero.common.exception.JeroBootException;
import com.jero.common.system.api.ISysBaseAPI;
import com.jero.common.system.util.JwtUtil;
import com.jero.common.system.vo.LoginUser;
@@ -25,12 +26,16 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.formula.functions.T;
import org.apache.shiro.SecurityUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.beans.PropertyDescriptor;
import java.util.*;
import java.util.stream.Collectors;
@@ -63,7 +68,10 @@ public class LoginServiceImpl implements ILoginService {
private ISysUserRoleService sysUserRoleService;
@Resource
private ISysRoleService sysRoleService;
@Autowired
private Oauth oauth;
@Autowired
private RestTemplate restTemplate;
private static final String BASE_CHECK_CODES = "qwertyuiplkjhgfdsazxcvbnmQWERTYUPLKJHGFDSAZXCVBNM1234567890";
//密码登录错误的次数前缀
public static final String RETRY_LOGIN_PREFIX = "login:retryLoginCount_";
@@ -73,6 +81,7 @@ public class LoginServiceImpl implements ILoginService {
private static final String UTF_8 = "utf-8";
private static final String USER_NAME = "用户名: ";
private static final String USER_INFO = "userInfo";
private static final String LOGIN_SUCCEED = ",登录成功!";
@Override
public Result<JSONObject> login(SysLoginModel sysLoginModel) {
@@ -146,12 +155,130 @@ public class LoginServiceImpl implements ILoginService {
//update-begin--Author:wangshuai Date:20200714 for:登录日志没有记录人员
LoginUser loginUser = new LoginUser();
BeanUtils.copyProperties(sysUser, loginUser);
baseCommonService.addLog(USER_NAME + username + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
baseCommonService.addLog(USER_NAME + username + LOGIN_SUCCEED, CommonConstant.LOG_TYPE_1, null,loginUser);
//update-end--Author:wangshuai Date:20200714 for:登录日志没有记录人员
return result;
}
@Override
public Result<JSONObject> loginByOauth2(Oauth oauth) {
Result<JSONObject> result;
BeanUtils.copyProperties(this.oauth, oauth, getNullPropertyNames(this.oauth));
OauthAccessToken oauthAccessToken;
try {
// 通过授权码获取accessToken
oauthAccessToken = getAccessToken(oauth);
}catch (Exception e){
log.error(e.getMessage());
return Result.error("获取accessToken失败!");
}
OauthUserInfo oauthUserInfo;
try {
// 通过accessToken获取账号信息
oauthUserInfo = getUserInfo(oauth, oauthAccessToken);
}catch (Exception e){
log.error(e.getMessage());
return Result.error("获取账号信息失败!");
}
// 获取用户信息
String userName = oauthUserInfo.getSpRoleList().get(0);
SysUser sysUser = sysUserService.getUserByName(userName);
// 校验用户是否有效
result = sysUserService.checkUserIsEffective(sysUser);
if(!result.isSuccess()) {
return result;
}
if (checkSysPermission(sysUser.getUsername())) {
return Result.error("该用户无权限,请联系管理员");
}
//用户登录信息
//用户登录信息
userInfo(sysUser, result);
//update-begin--Author:wangshuai Date:20200714 for:登录日志没有记录人员
LoginUser loginUser = new LoginUser();
BeanUtils.copyProperties(sysUser, loginUser);
baseCommonService.addLog(USER_NAME + userName + LOGIN_SUCCEED, CommonConstant.LOG_TYPE_1, null,loginUser);
return result;
}
private OauthUserInfo getUserInfo(Oauth oauth, OauthAccessToken oauthAccessToken) {
String userInfoUrl = oauth.getUserInfoUrl() + "?access_token=" + oauthAccessToken.getAccessToken() + "&client_id=" + oauth.getClientId();
/**
* 如果返回json
* OauthUserInfo oauthUserInfo = restTemplate.getForObject(userInfoUrl, OauthUserInfo.class);
*/
// 返回String --> jsonString --> jsonObject --> 实体
String resultStr = restTemplate.getForObject(userInfoUrl, String.class);
JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(resultStr));
OauthUserInfo oauthUserInfo = JSON.toJavaObject(jsonObject, OauthUserInfo.class);
if (Objects.isNull(oauthUserInfo) || Objects.isNull(oauthUserInfo.getSpRoleList()) || oauthUserInfo.getSpRoleList().isEmpty()){
throw new JeroBootException("获取用户信息返回信息为空!");
}
if (StringUtils.isBlank(oauthUserInfo.getErrCode())){
throw new JeroBootException(oauthUserInfo.getMsg());
}
return oauthUserInfo;
}
private OauthAccessToken getAccessToken(Oauth oauth) {
/**
// 1.表单提交方式
// 设置为表单提交,按需求加
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type","application/x-www-form-urlencoded");
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 组装请求信息
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("client_id", oauth.getClientId());
params.add("client_secret", oauth.getClientSecret());
params.add("grant_type", oauth.getGrantType());
params.add("redirect_uri", oauth.getRedirectUri());
params.add("code", oauth.getCode());
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(params, headers);
OauthAccessToken oauthAccessToken = restTemplate.postForObject(oauth.getAccessTokenUrl(), requestEntity, OauthAccessToken.class);
*/
// 2.params传参
String accessTokenUrl = oauth.getAccessTokenUrl();
// 拼接参数
accessTokenUrl = accessTokenUrl + "?client_id=" + oauth.getClientId()
+ "&grant_type=" + oauth.getGrantType()
+ "&code=" + oauth.getCode()
+ "&client_secret" + oauth.getClientSecret();
OauthAccessToken oauthAccessToken = restTemplate.postForObject(accessTokenUrl, null, OauthAccessToken.class);
if (Objects.isNull(oauthAccessToken) || StringUtils.isBlank(oauthAccessToken.getAccessToken())){
throw new JeroBootException("获取AccessToken返回信息为空!");
}
if (StringUtils.isBlank(oauthAccessToken.getErrCode())){
throw new JeroBootException(oauthAccessToken.getMsg());
}
return oauthAccessToken;
}
/**
* 解决BeanUtils.copyProperties() 的 null值覆盖问题
* @param source
* @return
*/
public static String[] getNullPropertyNames (Object source) {
final BeanWrapper src = new BeanWrapperImpl(source);
PropertyDescriptor[] pds = src.getPropertyDescriptors();
Set<String> emptyNames = new HashSet<>();
for(PropertyDescriptor pd : pds) {
Object srcValue = src.getPropertyValue(pd.getName());
if (srcValue == null) emptyNames.add(pd.getName());
}
String[] result = new String[emptyNames.size()];
return emptyNames.toArray(result);
}
/**
* 登录重试信息存入redis
* @param username
@@ -0,0 +1,35 @@
package com.jero.modules.docking.iam.controller;
import com.alibaba.fastjson.JSONObject;
import com.jero.common.api.vo.Result;
import com.jero.modules.system.entity.Oauth;
import com.jero.modules.system.service.ILoginService;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: 31318
* @Date: 2023/10/17/10:09
* @Description:
*/
@RestController
@RequestMapping("/iam")
@Slf4j
public class IamLoginController {
private final ILoginService loginService;
public IamLoginController(ILoginService loginService) {
this.loginService = loginService;
}
@ApiOperation("单点登录")
@PostMapping("/loginByOauth2")
public Result<JSONObject> loginByOauth2(@Validated @RequestBody Oauth oauth){
return loginService.loginByOauth2(oauth);
}
}
@@ -384,4 +384,19 @@ hiwork:
# 统一待办集成
todoUrl: taskapi/task.basedata/integrationCall/workflowIntegration
# 统一消息集成
messageUrl: taskapi/task.basedata/notice/noticeCalls/send
messageUrl: taskapi/task.basedata/notice/noticeCalls/send
# 单点登录配置(所有配置信息需要协调注册,现在都为假)
oauth:
# 客户端应用注册ID
clientId: 50448-a720-903ed
# 客户端应用注册密钥
clientSecret: 0e222cb-9df9-7b97-121b4eaa
# 授权码验证
grantType: authorization_code
# 通过授权码获取accessToken请求地址
accessTokenUrl: https://认证接口地址/idp/oauth2/getToken
# 通过accessToken获取账号信息请求地址
userInfoUrl: https://认证接口地址/idp/oauth2/getUserInfo
# 防止跨站请求伪造(CSRF)标识(暂时不用)
state: zhongQi