Springboot与Oracle
一、背景
叮叮
在业内某知名ERP厂商,给某二线末流城市的地铁公司做的HR系统。
一期已上线,二期地铁新引进了PaaS做ESB,需要把HR与PaaS进行集成。
涉及内容有:
•oauth2 单点登录;
•待办同步及回跳
二、单点登录
2.1 问题
PaaS单点登录使用的是oauth2,但是我所就职某ERP的HR系统,偏偏不支持oauth2,然后架构还是很老的一套架构,源码对分公司的研发也不开放,经协商后,选择在ERP服务器上再起一个服务,外挂一套程序,实现与PaaS的oauth2单点登录,同时可登录至ERP。
2.2 实现方式
1. 创建springboot项目
这个没什么可说的,IDEA新建项目...
2. 实现与PaaS单点登录
这个说实话,也是第一次搞oauth2,网上查了各种资料,看了原理,找了样例代码,还去码云上找了开源项目看了看,其实写起来并不复杂,不知道你们看着懵不懵,我一开始是懵的,贴一段PaaS提供的文档内容。







按要求,对接需要使用授权码模式,我看着这个文档和网上查来的资料,一脸懵。
但就这个项目,我作为客户端,其实也就是几次请求,大概流程就是:

具体的实现过程如下:
1.application.yml配置
security:
oauth2:
url: url
client:
client_id: clientId
client_secret: clientSecret
response_type: code
grant_type: authorization_code
authorization_url: ${security.oauth2.url}/authorize
token_url: ${security.oauth2.url}/token
userinfo_url: ${security.oauth2.url}/userinfo
简单说一下yml的内容,url是PaaS提供单点登录的请求地址,也就是oauth2的服务端地址(我是这么理解的),clientid、clientsecret是oauth2服务端配置的,用于验证的,response_type:code 是授权码(这个没理解,就这么用),grant_type,授权码模式是authorization_code,密码模式和客户端模式的值不一样,后边的参数,获取授权码code的地址是authorization_url,获取token的地址是token_url,获取用户信息的地址是userinfo_url。
2.Controller代码
@RequestMapping(value = "login")
public void login(HttpServletRequest request, HttpServletResponse response, String code) {
log.info("---HR_Login,code:" + code + "---");
log.info("---HR_Login,date:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "---");
try {
if (StrUtil.isEmpty(code)) {
log.info("---HR_Login,完犊子---");
getCode(response, true);
} else {
getAccessToken(response, code);
if (StrUtil.isEmpty(response.getHeader("access_token"))) {
return;
}
String accessToken = response.getHeader("access_token");
String userNumber = getUserinfo(accessToken);
log.info("---HR_Login,userNumber:" + userNumber + "---");
String filepath = Objects.requireNonNull(LoginController.class.getClassLoader().getResource("LtpaToken.properties")).getFile();
log.info("---HR_Login,filepath:" + filepath + "---");
String password = LtpaTokenManager.generate(userNumber, filepath).toString();
log.info("---HR_Login,password:" + password + "---");
String sysUrl = shrConfig.getSys_url();
String redirectTo = URLEncoder.encode(sysUrl + "/shr/home.do");
log.info("---HR_Login,redirectTo:" + redirectTo + "---");
String redirectUrl =
sysUrl + "/不可见的地址?username=" + userNumber + "&password=" + password + "&userAuthPattern=OTP&isNotCheckRelogin=true&redirectTo="
+ redirectTo;
log.info("---HR_Login,redirectUrl:" + redirectUrl + "---");
response.sendRedirect(redirectUrl);
}
} catch (IOException e) {
e.printStackTrace();
log.error("---HR_Login,Exception:" + e.getMessage() + "---");
throw new RuntimeException(e);
}
}
首先这是Controller的请求地址,需要在PaaS那边做配置。如图:

这张图片上,登录回调地址,就是Controller的地址,用户在PaaS门户点击HR系统时,PaaS所调用的地址。
PaaS调用后,HR需先判断,是否有code,即是否有授权,如果没有,则需要先获取授权码code,也就是代码中完犊子的部分。
获取code的代码如下:
private void getCode(HttpServletResponse response, boolean isShr) throws IOException {
String clientId = oauthConfig.getClient_id();
String authorizationUrl = oauthConfig.getAuthorization_url();
String responseType = oauthConfig.getResponse_type();
StringBuilder urlStr = new StringBuilder();
urlStr.append(authorizationUrl).append("?response_type=" + responseType).append("&client_id=" + clientId);
if (isShr) {
urlStr.append("&redirect_uri=" + shrConfig.getLogin_url()).append("&state=1");
} else {
urlStr.append("&redirect_uri=" + shrConfig.getPortal_url()).append("&state=1");
}
String getCodeUrl = urlStr.toString();
log.info("---HR_Login,getCodeUrl:" + getCodeUrl + "---");
response.sendRedirect(getCodeUrl);
}
获取code的回调地址,依然是Controller里的地址(这个我一直好奇,可不可以是两个地址,但没试过,不知道支持不支持,有机会试试),再次回调的时候,就是带着code来的,就可以依据code来获取token了。
获取token代码如下:
private void getAccessToken(HttpServletResponse response, String code) throws IOException {
String clientId = oauthConfig.getClient_id();
String tokenUrl = oauthConfig.getToken_url();
String grantType = oauthConfig.getGrant_type();
String clientSecret = oauthConfig.getClient_secret();
Map<String, Object> tokenValueMap = new HashMap<>();
tokenValueMap.put("code", code);
tokenValueMap.put("grant_type", grantType);
tokenValueMap.put("redirect_uri", shrConfig.getLogin_url());
log.info("---HR_Login,tokenUrl:" + tokenUrl + "---");
String tokenResult = HttpRequest.post(tokenUrl).basicAuth(clientId, clientSecret).form(tokenValueMap).execute().body();
log.info("---HR_Login,token_result:" + tokenResult + "---");
if (StrUtil.isEmpty(tokenResult)) {
response.sendRedirect(shrConfig.getLogin_url());
return;
}
JSONObject tokenResultObj = JSONUtil.parseObj(tokenResult);
if (tokenResultObj.containsKey("error") || !tokenResultObj.containsKey("access_token")) {
response.sendRedirect(shrConfig.getLogin_url());
return;
}
String accessToken = tokenResultObj.getStr("access_token");
log.info("---HR_Login,access_token:" + accessToken + "---");
response.setHeader("access_token", accessToken);
}
获取token就是普通的webapi请求,特殊的是我第一次写basicAuth这种东西,这个请求在postman里是这样的:


在这里再一次感谢hutool,我不用头疼怎么写这个请求合适。
获取到token之后,就可以登录HR系统了(讲道理,我感觉这个token应该是得校验一下或者判断一下超时啥的,但我没写,好像也没事...)。
登录HR系统的时候,需要知道是哪个用户登录,所以需要访问用户信息接口,获取用户信息。
获取用户信息代码如下:
private String getUserinfo(String accessToken) {
String userinfoUrl = oauthConfig.getUserinfo_url();
log.info("---HR_Login,userinfoUrl:" + userinfoUrl + "---");
String userResult = HttpRequest.get(userinfoUrl).bearerAuth(accessToken).execute().body();
log.info("---HR_Login,userResult:" + userResult + "---");
if (StrUtil.isEmpty(userResult)) {
return null;
}
JSONObject userResultObj = JSONUtil.parseObj(userResult);
String userNumber = userResultObj.getStr("preferred_username");
return userNumber;
}
controller代码中,从16行之后,都是为了满足ERP的登录需要的代码,可以不用考虑,网上更多的是拿到用户后,需要在自己本身的系统里查询用户是否存在。
3. 实现单点登录至ERP
这部分内容属于内部,先不往这贴了。
三、待办回跳
3.1 问题
待办ERP有定时任务,将HR系统产生的待办及办结信息同步至PaaS门户,用户在门户展示,这部分内容很简单,就是照着接口文档获取数据推送就行。其中涉及的一部分内容是待办的回跳,用户在门户看到待办信息后,直接在门户点击待办可以回跳到HR系统中待办处理的界面,没必要在走一次单点登录的流程,跳到HR系统首页去找待办。
3.2 实现方式
1. 待办推送时携带回跳地址信息
这个没啥说的,重点在下一步。
2. 待办回跳
跟单点登录类似的问题,HR系统是没办法直接跳过登录打开待办的界面的(这个估计大部分系统都不行),所以必须得走一遍单点,所以就想着待办的回跳地址也是单点的地址,不过参数需要按打开待办界面所需的内容来匹配,这块问了一下,说直接把待办的回跳地址配置的和单点地址一样不太好,所以单独做了一个Controller,用户接收待办的回跳。
controller代码如下:
@RequestMapping("todoHandler")
public void todoHandler(HttpServletRequest request, HttpServletResponse response, String todoId) {
log.info("---todoHandler,todoId:" + todoId + "---");
log.info("---todoHandler,date:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "---");
try {
if (!StrUtil.isEmpty(todoId)) {
String url = shrConfig.getWeb_url() + "/loginPortal?todoId=" + todoId;
log.info("---todoHandler,todoUrl:" + url + "---");
response.sendRedirect(url);
}
} catch (IOException e) {
e.printStackTrace();
}
}
这个其实就是获取了下待办的参数(HR系统所需要的待办流程ID,即代码中的todoId),然后因为此HR系统,待办和单点的门户其实不是一套东西,所以单独又写了一个处理待办的方法,也可以写一起,嫌乱。
待办的Controller代码如下:
@RequestMapping(value = "loginPortal")
public void loginPortal(HttpServletRequest request, HttpServletResponse response, String code, String todoId) {
log.info("---HR_LoginPortal,code:" + code + "---");
log.info("---HR_LoginPortal,todoId:" + todoId + "---");
log.info("---HR_LoginPortal,date:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "---");
try {
if (StrUtil.isEmpty(code)) {
TODO_ID = todoId;
getCode(response, false);
} else {
getAccessToken(response, code);
if (StrUtil.isEmpty(response.getHeader("access_token"))) {
return;
}
String accessToken = response.getHeader("access_token");
String userNumber = getUserinfo(accessToken);
log.info("---HR_LoginPortal,userNumber:" + userNumber + "---");
String filepath = Objects.requireNonNull(LoginController.class.getClassLoader().getResource("LtpaToken.properties")).getFile();
log.info("---HR_LoginPortal,filepath:" + filepath + "---");
String password = LtpaTokenManager.generate(userNumber, filepath).toString();
String sysUrl = shrConfig.getSys_url();
StringBuffer prefixUrl = new StringBuffer().append(
sysUrl + "/portal/index2sso.jsp?username=" + userNumber + "&password=" + password + "&userAuthPattern=OTP&isNotCheckRelogin=true");
if (StrUtil.isEmpty(todoId) && StrUtil.isEmpty(TODO_ID)) {
String redirectUrl = prefixUrl.append("&redirectTo=//").toString();
response.sendRedirect(redirectUrl);
} else {
if (StrUtil.isEmpty(todoId) && StrUtil.isNotEmpty(TODO_ID)) {
todoId = TODO_ID;
}
String redirectTo = setTodoRedirectToUrl(todoId);
log.info("---HR_LoginPortal,redirectTo:" + redirectTo + "---");
String redirectUrl = prefixUrl.append("&redirectTo=").append(redirectTo).toString();
log.info("---HR_LoginPortal,redirectUrl:" + redirectUrl + "---");
response.sendRedirect(redirectUrl);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
这里面做了几件事哈:
1.code为空的情况下,还是先需要去请求PaaS来获取授权,但是直接重定向的话,会把todoId丢掉,这个我能力有限,没想到啥好办法,就在Controller里面设置了个全局的变量,第一次访问的时候会把todoId更新上去,再进来不丢(这个说实话,是蒙的),获取授权码之后,还是获取token,然后走单点登录。HR系统单点是支持直接单点调整业务单据的界面的,就是在代码中,地址后边拼redirectURL的内容,所以按照todoId获取到他实际业务待办的地址后,直接走HR的登录就可以了。
3.3 过程中的问题
处理这个待办回跳的时候,遇到几个问题,标注一下方便后续类似项目处理:
1. 需要通过SQL来查询信息
想法是通过mybatis,写mapper和xml来实现SQL查询,代码更规范一些。
代码包路径:

mapper接口代码如下:
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TestMapper {
/**
* 获取人员ID
*
* @param number 人员编码
* @return 人员ID
*/
String getPersonId(String number);
/**
* 获取工作流未完成信息
*
* @param assignId
* @return
*/
String getAssignBizFunction(String assignId);
/**
* 获取工作流已完成信息
*
* @param assignId
* @return
*/
String getAssignDetailBizFunction(String assignId);
/**
* 获取工作流单据对象ID
*
* @param assignId
* @return
*/
String getBizObjectId(String assignId);
String getBillEntryType(String assignId);
}
mapper.xml代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kd.metrologin.sso.mapper.TestMapper">
<select id="getPersonId" resultType="String">
SELECT FID
FROM T_BD_Person
WHERE FNumber = #{number}
</select>
<select id="getAssignBizFunction" resultType="String">
SELECT FBIZFUNCTION
FROM T_WFR_ASSIGN
WHERE FASSIGNID = #{assignId}
</select>
<select id="getAssignDetailBizFunction" resultType="String">
SELECT FBIZFUNCTION
FROM T_WFR_ASSIGNDETAIL
WHERE FASSIGNID = #{assignId}
</select>
<select id="getBizObjectId" resultType="String">
SELECT FBIZOBJID
FROM T_WFR_ASSIGN
WHERE FASSIGNID = #{assignId}
</select>
<select id="getBillEntryType" resultType="String">
SELECT FBILLENTITY
FROM T_WFR_ASSIGN
WHERE FASSIGNID = #{assignId}
</select>
</mapper>
Controller引用代码如下:
@Autowired
private TestMapper testMapper;
@RequestMapping(value = "getPersonId")
public String getPersonId(@RequestParam String number) {
String personId = = testMapper.getPersonId(number);
log.info("---getPersonId,personId:" + personId + "---");
return personId;
}
遇到的第一个问题:我新建了个service,想着controller依赖service,service里面来引用mapper,不知道哪里写错了,最后报错,springboot起不来,后边也不带的研究了,就直接controller依赖了mapper;
第二个问题,刚开始一直报mapper.xml读不到的问题,网上搜了,说接口的命名和xml的命名得保持一致,然后xml里面的namespace的配置得是全路径,还有的是说xml不能和mapper接口在同路径,得放到resource里的静态文件下(路径这个事,因为我一开始搞springboot的时候,xml就是和mapper统一包下面,习惯了,也不想着往resource放,所以没去试)。后边发现,我读不到xml,是因为我的maven项目编译后,里面不包含xml文件,后边搜了之后,需要在yml里面配置mapper.xml的路径:
mybatis:
mapper-locations: classpath:**/mapper/*.xml
加上这个配置后,mapper.xml可以一起编译打包,就可以正常用了。
2. Oracle12C和springboot的问题
我本地数据库是Oracle11g,在yml里面配置了之后,mapper接口里写代码,xml里面写SQL,直接可以用,部署到客户的服务器后,客户数据库是Oracle12C,可以正常启用服务,但是需要数据库查询的时候,一直报错:

数据库yml配置:
spring:
datasource:
driver-class-name: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@127.0.0.1:1521:pdb2
username: username
password: password
网上搜,连接池的问题,看process,看session,我看了之后,没啥问题...久攻不下,就自己手撸了一套JDBC,想着先用吧,保证项目进度,后边同事反馈说,我们这个数据库,当时因为12C,必须在PDB下创建,所以链接时候用的不是SID,是service,所以url的配置,应该是用/不是:,然后我试了下...成了...
网上搜到的回复:

修改之后的yml:
spring:
datasource:
driver-class-name: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@//127.0.0.1:1521/pdb2
username: username
password: password

浙公网安备 33010602011771号