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
posted @ 2023-03-02 00:39  Wang的男人  阅读(312)  评论(0)    收藏  举报