OAuthController.java 16 KB

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