OAuthController.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. package com.icontrols.oauth.controller;
  2. import java.io.IOException;
  3. import java.io.UnsupportedEncodingException;
  4. import java.security.GeneralSecurityException;
  5. import java.security.NoSuchAlgorithmException;
  6. import java.time.Duration;
  7. import java.util.Base64;
  8. import java.util.Base64.Encoder;
  9. import java.util.List;
  10. import java.util.Random;
  11. import javax.servlet.http.HttpServletResponse;
  12. import javax.servlet.http.HttpSession;
  13. import org.json.JSONObject;
  14. import org.slf4j.Logger;
  15. import org.slf4j.LoggerFactory;
  16. import org.springframework.beans.factory.annotation.Autowired;
  17. import org.springframework.data.redis.core.RedisTemplate;
  18. import org.springframework.data.redis.core.ValueOperations;
  19. import org.springframework.http.HttpEntity;
  20. import org.springframework.http.HttpHeaders;
  21. import org.springframework.http.HttpStatus;
  22. import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
  23. import org.springframework.ui.Model;
  24. import org.springframework.web.bind.annotation.RequestHeader;
  25. import org.springframework.web.bind.annotation.RequestMapping;
  26. import org.springframework.web.bind.annotation.RequestMethod;
  27. import org.springframework.web.bind.annotation.RequestParam;
  28. import org.springframework.web.bind.annotation.ResponseBody;
  29. import org.springframework.web.bind.annotation.RestController;
  30. import org.springframework.web.client.RestTemplate;
  31. import org.springframework.web.server.ResponseStatusException;
  32. import org.springframework.web.servlet.ModelAndView;
  33. import org.springframework.web.servlet.view.RedirectView;
  34. import com.icontrols.oauth.constants.ERROR;
  35. import com.icontrols.oauth.model.ClientInfo;
  36. import com.icontrols.oauth.model.ComplexInfo;
  37. import com.icontrols.oauth.model.JWTInfo;
  38. import com.icontrols.oauth.model.Token;
  39. import com.icontrols.oauth.model.WallpadCode;
  40. import com.icontrols.oauth.repo.ClientInfoRepository;
  41. import com.icontrols.oauth.repo.ComplexInfoRepository;
  42. import com.icontrols.oauth.utils.AES256Util;
  43. import com.icontrols.oauth.utils.DanziAuthUtils;
  44. import com.icontrols.oauth.utils.JWTUtils;
  45. import com.sun.net.httpserver.HttpServer;
  46. @RestController
  47. @RequestMapping(value = "/api/oauth2")
  48. public class OAuthController {
  49. private static final Logger logger = LoggerFactory.getLogger(OAuthController.class);
  50. @Autowired
  51. ClientInfoRepository clientInfoRepo;
  52. @Autowired
  53. ComplexInfoRepository complexInfoRepo;
  54. @Autowired
  55. RedisTemplate<String, Object> redisTemplate;
  56. // http://127.0.0.1:8080/api/oauth2/authorize?client_id=clientid&redirect_uri=http://127.0.0.1:8080/api/oauth2/redirect&scope=scope&response_type=code&state=state
  57. @RequestMapping(value = "/authorize", method = RequestMethod.GET)
  58. public ModelAndView Authorize(@RequestHeader HttpHeaders headers,
  59. @RequestParam(value = "response_type", required = true) String responseType,
  60. @RequestParam(value = "client_id", required = true) String clientId,
  61. @RequestParam(value = "state", required = true) String state, @RequestParam(value = "scope") String scope,
  62. @RequestParam(value = "redirect_uri", required = true) String redirectUri, HttpSession httpSession,
  63. Model model) {
  64. logger.info("**************************************************************");
  65. logger.info("**************************************************************");
  66. logger.info("[STEP #1] /api/oauth2/authorize");
  67. logger.info("**************************************************************");
  68. logger.info("**************************************************************");
  69. logger.info("*[parameters]");
  70. logger.info("*1. response_type: " + responseType);
  71. logger.info("*2. client_id: " + clientId);
  72. logger.info("*3. state: " + state);
  73. // logger.info("*4. scope: " + scope);
  74. logger.info("*5. redirect_uri: " + redirectUri);
  75. // 월패드 인증코드 시간이 만료되어 history.go(-1)로 해당 페이지에 들어오는 경우에
  76. // 세션에 저장되었던 사용자 정보를 지움.
  77. if (httpSession.getAttribute("wallpadCode") != null)
  78. httpSession.removeAttribute("wallpadCode");
  79. if (httpSession.getAttribute("homeInfo") != null)
  80. httpSession.removeAttribute("homeInfo");
  81. ModelAndView mav = new ModelAndView();
  82. ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId);
  83. if (clientInfo == null) {
  84. throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ERROR.INVALID_CLIENT);
  85. } else {
  86. logger.info("*[clientinfo]");
  87. logger.info("*" + clientInfo.toString());
  88. if (!responseType.equals("code")) {
  89. throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.UNSUPPORTED_GRANT_TYPE);
  90. } else {
  91. // 클라이언트에 따라 endpoint를 변경해야해서 세션에 클라이언트 정보를 저장.
  92. httpSession.setAttribute("clientId", clientId);
  93. httpSession.setAttribute("redirectUri", redirectUri);
  94. httpSession.setAttribute("state", state);
  95. // TODO
  96. // if (scope != null)
  97. // httpSession.setAttribute("scope", scope);
  98. List<ComplexInfo> complexInfos = complexInfoRepo.findAll();
  99. model.addAttribute("complexInfos", complexInfos);
  100. // \src\main\webapp\WEB-INF\jsp\InsertUserInfo.jsp
  101. mav.setViewName("InsertUserInfo");
  102. }
  103. }
  104. return mav;
  105. }
  106. @RequestMapping(value = "/user/info/submit", method = RequestMethod.GET)
  107. public ModelAndView sendCode(HttpSession httpSession,
  108. @RequestParam(value = "complex", required = true) String complex,
  109. @RequestParam(value = "dong", required = true) String dong,
  110. @RequestParam(value = "ho", required = true) String ho)
  111. throws IOException, NoSuchAlgorithmException, GeneralSecurityException {
  112. logger.info("**************************************************************");
  113. logger.info("**************************************************************");
  114. logger.info("[STEP #2] /api/oauth2/user/submit");
  115. logger.info("**************************************************************");
  116. logger.info("**************************************************************");
  117. logger.info("*[parameters]");
  118. logger.info("*1. complex: " + complex);
  119. logger.info("*2. dong: " + dong);
  120. logger.info("*3. ho: " + ho);
  121. String clientId = httpSession.getAttribute("clientId").toString();
  122. // wallpad 코드 생성
  123. Random generator = new Random();
  124. String wallpadCode = "";
  125. for (int i = 0; i < 6; i++) {
  126. wallpadCode += Integer.toString(generator.nextInt(10));
  127. }
  128. logger.info("*[wallpadCode]");
  129. logger.info("*code: " + wallpadCode);
  130. // 세대정보 인코딩
  131. String homeInfo = complex + "/" + dong + "/" + ho + "/" + clientId;
  132. // 2019.05.02 DB저장에서 세션 저장 방식으로 변경
  133. httpSession.setAttribute("wallpadCode", wallpadCode);
  134. httpSession.setAttribute("homeInfo", homeInfo);
  135. logger.info("*[redis]");
  136. logger.info("*(key,value): (" + wallpadCode + "," + homeInfo + ")");
  137. // 단지서버로 월패드인증번호 전송
  138. WallpadCode body = new WallpadCode(dong, ho, wallpadCode);
  139. logger.info(body.toString());
  140. ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId);
  141. String endPoint = clientInfo.getEndPoint();
  142. // String url = "http://" + complex + ":8002/kakao/auth";
  143. String url = "http://" + complex + endPoint + "/auth";
  144. logger.info(url);
  145. // TODO 예외처리 단지통신
  146. HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
  147. factory.setReadTimeout(3000);
  148. factory.setConnectTimeout(3000);
  149. RestTemplate template = new RestTemplate(factory);
  150. HttpEntity<Object> entity = new HttpEntity<Object>(body);
  151. try {
  152. String answer = template.postForObject(url, entity, String.class);
  153. logger.info(answer);
  154. } catch (Exception e) {
  155. new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_HOMEINFO);
  156. }
  157. ModelAndView mav = new ModelAndView();
  158. mav.setViewName("InsertCode");
  159. return mav;
  160. }
  161. // InsertCode.jsp에서 월패드 인증번호를 입력받아 ajax로 실행하는 부분,
  162. // 사용자가 입력한 인증번호의 유효성을 검증함.
  163. @RequestMapping(value = "/validate", method = RequestMethod.GET)
  164. public @ResponseBody Boolean validate(HttpSession httpSession,
  165. @RequestParam(value = "code", required = true) String wallpadCode) {
  166. logger.info("**************************************************************");
  167. logger.info("**************************************************************");
  168. logger.info("[STEP #3] /api/oauth2/auth");
  169. logger.info("**************************************************************");
  170. logger.info("**************************************************************");
  171. logger.info("*[parameters]");
  172. logger.info("*1. Input code: " + wallpadCode);
  173. // ValueOperations<String, Object> vop = redisTemplate.opsForValue();
  174. // if (vop.get(wallpadCode) == null) { // 없으면 null
  175. //
  176. // return false;
  177. // }
  178. // httpSession.setAttribute("homeId", vop.get(wallpadCode));
  179. // 2019.05.02 DB저장에서 세션 저장 방식으로 변경
  180. logger.info(httpSession.getAttributeNames().toString());
  181. if (httpSession.getAttribute("wallpadCode") == null) {
  182. logger.info("null");
  183. return false;
  184. } else {
  185. if (!httpSession.getAttribute("wallpadCode").toString().equals(wallpadCode)) {
  186. logger.info(httpSession.getAttribute("wallpadCode").toString());
  187. return false;
  188. }
  189. }
  190. logger.info(httpSession.getAttribute("wallpadCode").toString());
  191. return true;
  192. }
  193. // 월패드 인증이 실행된 경우, 인증 코드를 발급해 리다이렉트하는 부분
  194. @RequestMapping(value = "/code/generate", method = RequestMethod.GET)
  195. public RedirectView validateCode(HttpSession httpSession) {
  196. logger.info("**************************************************************");
  197. logger.info("**************************************************************");
  198. logger.info("[STEP #4] /code/generate");
  199. logger.info("**************************************************************");
  200. logger.info("**************************************************************");
  201. logger.info("*[parameters]");
  202. logger.info("*1. home id: " + httpSession.getAttribute("homeInfo"));
  203. String code = "";
  204. String homeInfo = "";
  205. ValueOperations<String, Object> vop = redisTemplate.opsForValue();
  206. // 코드 발급
  207. homeInfo = httpSession.getAttribute("homeInfo").toString();
  208. logger.info("*[home info]");
  209. logger.info("*1. homeInfo: " + homeInfo);
  210. code = "";
  211. Random rnd = new Random();
  212. for (int i = 0; i < 10; i++) {
  213. code += String.valueOf((char) ((int) (rnd.nextInt(26)) + 65));
  214. }
  215. logger.info("*[code]");
  216. logger.info("*1. code: " + code);
  217. Duration d = Duration.ofSeconds(180);
  218. while (!vop.setIfAbsent(code, homeInfo, d)) {
  219. code = "";
  220. for (int i = 0; i < 10; i++) {
  221. code += String.valueOf((char) ((int) (rnd.nextInt(26)) + 65));
  222. }
  223. }
  224. String redirectUri = (String) httpSession.getAttribute("redirectUri");
  225. redirectUri += "?state=" + httpSession.getAttribute("state");
  226. redirectUri += "&code=" + code;
  227. RedirectView redirectView = new RedirectView();
  228. redirectView.setUrl(redirectUri);
  229. return redirectView;
  230. }
  231. // 토큰 발급하는 부분
  232. @RequestMapping(value = "/token", method = RequestMethod.POST, produces = "application/json")
  233. public @ResponseBody String Token(@RequestParam(value = "grant_type", required = true) String grantType,
  234. @RequestParam(value = "code", required = false) String code,
  235. @RequestParam(value = "refresh_token", required = false) String refreshToken, HttpSession httpSession,
  236. @RequestHeader HttpHeaders headers, HttpServletResponse response)
  237. throws NoSuchAlgorithmException, UnsupportedEncodingException, GeneralSecurityException {
  238. response.setStatus(HttpServletResponse.SC_OK);
  239. logger.info("[STEP #5] /api/oauth2/token");
  240. // TODO Code유효성 검사
  241. // TODO token 발급
  242. logger.info("grant_type: " + grantType);
  243. ValueOperations<String, Object> vop = redisTemplate.opsForValue();
  244. String newAccessToken = ""; // 생성해야 함.
  245. String newRefreshToken = ""; // 생성해야 함.
  246. String answer = null; // 단지서버에 토큰 관련요청 후 수신하는 응답
  247. Token token = new Token();
  248. if (grantType.equals("authorization_code")) {
  249. // TODO 코드 검사
  250. if (vop.get(code) == null) { // 인증코드가 없는 경우
  251. // 에러페이지 리턴
  252. throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_GRANT);
  253. } else {
  254. AES256Util aes = new AES256Util();
  255. String homeInfo = vop.get(code).toString();
  256. String jwt_homeInfo = homeInfo.split("/")[1] + "/" + homeInfo.split("/")[2];
  257. String encodedHomeInfo = aes.encrypt(jwt_homeInfo);
  258. logger.info("encodedHomeInfo =" + encodedHomeInfo);
  259. logger.info("encoded = " + encodedHomeInfo);
  260. String complexIp = homeInfo.split("/")[0];
  261. Encoder encoder = Base64.getEncoder();
  262. complexIp = new String(encoder.encode(complexIp.getBytes()));
  263. JWTInfo jwtinfo = new JWTInfo(complexIp, encodedHomeInfo, homeInfo.split("/")[3]);
  264. newAccessToken = JWTUtils.generateToken(jwtinfo, "accessToken");
  265. newRefreshToken = JWTUtils.generateToken(jwtinfo, "refreshToken");
  266. token.setAccess_token(newAccessToken);
  267. token.setRefresh_token(newRefreshToken);
  268. String clientId = JWTUtils.getClientInfoFromToken(newAccessToken);
  269. ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId);
  270. String url = clientInfo.getEndPoint();
  271. answer = DanziAuthUtils.create(token, url);
  272. }
  273. } else if (grantType.equals("refresh_token")) {
  274. if (refreshToken != null) {
  275. // 1. 정상적으로 생성되었는지 확인
  276. if (!JWTUtils.validateToken(refreshToken, "refreshToken"))
  277. throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_REFRESHTOKEN);
  278. // client정보검사
  279. if (headers.get("Authorization").get(0) == null)
  280. throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ERROR.INVALID_CLIENT);
  281. // if (!validateHeaderAuth(headers.get("Authorization").get(0)))
  282. // throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ERROR.INVALID_CLIENT);
  283. // 단지에 갱신토큰 유효성 확인
  284. String clientId = JWTUtils.getClientInfoFromToken(refreshToken);
  285. ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId);
  286. String url = clientInfo.getEndPoint();
  287. String isValidToken = DanziAuthUtils.get(url, refreshToken);
  288. logger.info(isValidToken);
  289. JSONObject isValidTokenJson = new JSONObject(isValidToken);
  290. logger.info(isValidTokenJson.toString());
  291. if (!isValidTokenJson.get("result").toString().equalsIgnoreCase("success")) {
  292. if (isValidTokenJson.getJSONObject("error").getInt("code") == -200) // 서버에러인경우
  293. throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
  294. response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
  295. logger.info(isValidTokenJson.get("result").toString());
  296. JSONObject error = new JSONObject();
  297. error.put("error", "invalid_grant");
  298. error.put("error_description", "invalid token");
  299. return error.toString();
  300. }
  301. // 유효한 토큰인 경우 토큰 생성
  302. JWTInfo jwtinfo = JWTUtils.getJWTInfoFromToken(refreshToken);
  303. newAccessToken = JWTUtils.generateToken(jwtinfo, "accessToken");
  304. newRefreshToken = JWTUtils.generateToken(jwtinfo, "refreshToken");
  305. token.setAccess_token(newAccessToken);
  306. token.setRefresh_token(newRefreshToken);
  307. // 단지서버에 갱신된 토큰 전달
  308. answer = DanziAuthUtils.refresh(url, token, refreshToken);
  309. logger.info(answer);
  310. }
  311. } else {
  312. throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
  313. }
  314. // 갱신된 토큰 전달에 대한 응답처리
  315. logger.info(answer);
  316. if (answer == null) {
  317. throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
  318. } else {
  319. JSONObject obj = new JSONObject(answer);
  320. logger.info(obj.get("result").toString());
  321. if (!obj.get("result").toString().equalsIgnoreCase("success")) {
  322. if (obj.getJSONObject("error").getInt("code") == -200) // 서버에러인경우
  323. throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
  324. response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
  325. JSONObject error = new JSONObject();
  326. error.put("error", "invalid_grant");
  327. error.put("error_description", "invalid token");
  328. return error.toString();
  329. }
  330. }
  331. // class 만들어서 사용하는 것으로 변경해야함.
  332. logger.info(token.toString());
  333. // TODO 단지서버로 전송하고 정상 응답 받은 경우에만 정상리턴 -> 아닌경우에는 500
  334. // 토큰 생성인 경우와 갱신인 경우 구분해서 처리 create, refresh
  335. JSONObject tokenResult = new JSONObject();
  336. tokenResult.put("access_token", token.getAccess_token());
  337. tokenResult.put("refresh_token", token.getRefresh_token());
  338. tokenResult.put("token_type", token.getToken_type());
  339. tokenResult.put("expires_in", token.getExpires_in());
  340. return tokenResult.toString();
  341. }
  342. public boolean validateHeaderAuth(String authorization) {
  343. String splitStr[] = authorization.split(" ");
  344. // splitStr length 가 2가 아니면 유효하지 않은 authorization 값
  345. if (splitStr.length != 2)
  346. return false;
  347. // splitStr이 Basic이 아니어도 에러
  348. if (!splitStr[0].equals("Basic"))
  349. return false;
  350. // Base64 디코드
  351. String decodeStr = new String(Base64.getDecoder().decode(splitStr[1].trim()));
  352. splitStr = decodeStr.split(":");
  353. // splitStr length 가 2가 아니면 유효하지 않은 authorization 값
  354. if (splitStr.length != 2)
  355. return false;
  356. ClientInfo clientInfo = clientInfoRepo.findByClientId(splitStr[0]);
  357. if (clientInfo == null)
  358. return false;
  359. if (!clientInfo.getClientSecret().equals(splitStr[1]))
  360. return false;
  361. return true;
  362. }
  363. // ((Test)) Redirect Destination
  364. // 여기는 리다이렉트 받는 URl임 테스트용. Client를 대신함.
  365. @RequestMapping(value = "/redirect", method = RequestMethod.GET)
  366. public @ResponseBody String RedirectTester(HttpSession httpSession,
  367. @RequestParam(value = "code", required = true) String code) {
  368. logger.info("/api/oauth2/redirect");
  369. String redirectUri = "http://61.33.215.56:8003/api/oauth2/token?grant_type=authorization_code&code=" + code;
  370. RedirectView redirectView = new RedirectView();
  371. redirectView.setUrl(redirectUri);
  372. return code;
  373. }
  374. @RequestMapping(value = "/aws/healthcheck", method = RequestMethod.GET)
  375. public void healthCheck(HttpEntity<String> httpEntity) {
  376. return;
  377. }
  378. }