OAuthController.java 18 KB

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