From 05163f1b0396c6c518930ef7c8575e6385e58621 Mon Sep 17 00:00:00 2001 From: yjz <3131811435@qq.com> Date: Tue, 17 Oct 2023 14:19:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(iam):=20=E5=8D=95=E7=82=B9=E7=99=BB?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/jero/config/RestTemplateConfig.java | 7 +- .../src/main/java/com/jero/config/SSL.java | 72 ++++++++++ .../com/jero/config/shiro/ShiroConfig.java | 1 + .../com/jero/modules/system/entity/Oauth.java | 68 +++++++++ .../system/entity/OauthAccessToken.java | 58 ++++++++ .../modules/system/entity/OauthUserInfo.java | 80 +++++++++++ .../modules/system/service/ILoginService.java | 8 ++ .../system/service/impl/LoginServiceImpl.java | 133 +++++++++++++++++- .../iam/controller/IamLoginController.java | 35 +++++ .../src/main/resources/application-dev.yml | 17 ++- 10 files changed, 473 insertions(+), 6 deletions(-) create mode 100644 laws-base/laws-base-core/src/main/java/com/jero/config/SSL.java create mode 100644 laws-module-system/src/main/java/com/jero/modules/system/entity/Oauth.java create mode 100644 laws-module-system/src/main/java/com/jero/modules/system/entity/OauthAccessToken.java create mode 100644 laws-module-system/src/main/java/com/jero/modules/system/entity/OauthUserInfo.java create mode 100644 laws-modules-docking/src/main/java/com/jero/modules/docking/iam/controller/IamLoginController.java diff --git a/laws-base/laws-base-core/src/main/java/com/jero/config/RestTemplateConfig.java b/laws-base/laws-base-core/src/main/java/com/jero/config/RestTemplateConfig.java index 45707853..739adb5b 100644 --- a/laws-base/laws-base-core/src/main/java/com/jero/config/RestTemplateConfig.java +++ b/laws-base/laws-base-core/src/main/java/com/jero/config/RestTemplateConfig.java @@ -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; diff --git a/laws-base/laws-base-core/src/main/java/com/jero/config/SSL.java b/laws-base/laws-base-core/src/main/java/com/jero/config/SSL.java new file mode 100644 index 00000000..8baced6c --- /dev/null +++ b/laws-base/laws-base-core/src/main/java/com/jero/config/SSL.java @@ -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"); + } + + } +} diff --git a/laws-base/laws-base-core/src/main/java/com/jero/config/shiro/ShiroConfig.java b/laws-base/laws-base-core/src/main/java/com/jero/config/shiro/ShiroConfig.java index f9e0d0c7..69e62087 100644 --- a/laws-base/laws-base-core/src/main/java/com/jero/config/shiro/ShiroConfig.java +++ b/laws-base/laws-base-core/src/main/java/com/jero/config/shiro/ShiroConfig.java @@ -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"); //第三方登录 diff --git a/laws-module-system/src/main/java/com/jero/modules/system/entity/Oauth.java b/laws-module-system/src/main/java/com/jero/modules/system/entity/Oauth.java new file mode 100644 index 00000000..c4fecff8 --- /dev/null +++ b/laws-module-system/src/main/java/com/jero/modules/system/entity/Oauth.java @@ -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_code(4种验证方式(authorization_code,password,refresh_token,client_credentials),采用授权码验证) + */ + @ApiModelProperty(value = "authorization_code(4种验证方式(authorization_code,password,refresh_token,client_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; +} diff --git a/laws-module-system/src/main/java/com/jero/modules/system/entity/OauthAccessToken.java b/laws-module-system/src/main/java/com/jero/modules/system/entity/OauthAccessToken.java new file mode 100644 index 00000000..e0a0408e --- /dev/null +++ b/laws-module-system/src/main/java/com/jero/modules/system/entity/OauthAccessToken.java @@ -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; +} diff --git a/laws-module-system/src/main/java/com/jero/modules/system/entity/OauthUserInfo.java b/laws-module-system/src/main/java/com/jero/modules/system/entity/OauthUserInfo.java new file mode 100644 index 00000000..1457e4b6 --- /dev/null +++ b/laws-module-system/src/main/java/com/jero/modules/system/entity/OauthUserInfo.java @@ -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 spRoleList; + + /** + * 联系方式 + */ + @ApiModelProperty(value = "联系方式") + private String mobile; + + /** + * + */ + @ApiModelProperty(value = "") + private List spNameList; + + /** -----------错误响应返回结果----------- */ + + /** + * 错误码 + */ + @JsonAlias("errcode") + @ApiModelProperty(value = "错误码") + private String errCode; + + /** + * 错误信息 + */ + @ApiModelProperty(value = "错误信息") + private String msg; +} diff --git a/laws-module-system/src/main/java/com/jero/modules/system/service/ILoginService.java b/laws-module-system/src/main/java/com/jero/modules/system/service/ILoginService.java index 4b5008d7..ff3610b9 100644 --- a/laws-module-system/src/main/java/com/jero/modules/system/service/ILoginService.java +++ b/laws-module-system/src/main/java/com/jero/modules/system/service/ILoginService.java @@ -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 login(SysLoginModel sysLoginModel); + /** + * + * @param oauth + * @return + */ + public Result loginByOauth2(Oauth oauth); + /** * 退出登录 * @param request response diff --git a/laws-module-system/src/main/java/com/jero/modules/system/service/impl/LoginServiceImpl.java b/laws-module-system/src/main/java/com/jero/modules/system/service/impl/LoginServiceImpl.java index 49e51f0f..816d5c90 100644 --- a/laws-module-system/src/main/java/com/jero/modules/system/service/impl/LoginServiceImpl.java +++ b/laws-module-system/src/main/java/com/jero/modules/system/service/impl/LoginServiceImpl.java @@ -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 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 loginByOauth2(Oauth oauth) { + Result 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 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> 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 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 diff --git a/laws-modules-docking/src/main/java/com/jero/modules/docking/iam/controller/IamLoginController.java b/laws-modules-docking/src/main/java/com/jero/modules/docking/iam/controller/IamLoginController.java new file mode 100644 index 00000000..3ffb83cb --- /dev/null +++ b/laws-modules-docking/src/main/java/com/jero/modules/docking/iam/controller/IamLoginController.java @@ -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 loginByOauth2(@Validated @RequestBody Oauth oauth){ + return loginService.loginByOauth2(oauth); + } +} diff --git a/laws-single-startup/src/main/resources/application-dev.yml b/laws-single-startup/src/main/resources/application-dev.yml index 0c77e741..7e39da3b 100644 --- a/laws-single-startup/src/main/resources/application-dev.yml +++ b/laws-single-startup/src/main/resources/application-dev.yml @@ -384,4 +384,19 @@ hiwork: # 统一待办集成 todoUrl: taskapi/task.basedata/integrationCall/workflowIntegration # 统一消息集成 - messageUrl: taskapi/task.basedata/notice/noticeCalls/send \ No newline at end of file + 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 \ No newline at end of file