package com.icontrols.oauth.controller; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import java.time.Duration; import java.util.Base64; import java.util.Base64.Encoder; import java.util.List; import java.util.Random; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.view.RedirectView; import com.icontrols.oauth.constants.ERROR; import com.icontrols.oauth.model.ClientInfo; import com.icontrols.oauth.model.ComplexInfo; import com.icontrols.oauth.model.JWTInfo; import com.icontrols.oauth.model.Token; import com.icontrols.oauth.model.WallpadCode; import com.icontrols.oauth.repo.ClientInfoRepository; import com.icontrols.oauth.repo.ComplexInfoRepository; import com.icontrols.oauth.utils.AES256Util; import com.icontrols.oauth.utils.JWTUtils; @RestController @RequestMapping(value = "/api/oauth2") public class OAuthController { private static final Logger logger = LoggerFactory.getLogger(OAuthController.class); @Autowired ClientInfoRepository clientInfoRepo; @Autowired ComplexInfoRepository complexInfoRepo; @Autowired RedisTemplate redisTemplate; // 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 @RequestMapping(value = "/authorize", method = RequestMethod.GET) public ModelAndView Authorize(@RequestHeader HttpHeaders headers, @RequestParam(value = "response_type", required = true) String responseType, @RequestParam(value = "client_id", required = true) String clientId, @RequestParam(value = "state", required = true) String state, @RequestParam(value = "scope") String scope, @RequestParam(value = "redirect_uri", required = true) String redirectUri, HttpSession httpSession, Model model) { logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("[STEP #1] /api/oauth2/authorize"); logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("*[parameters]"); logger.info("*1. response_type: " + responseType); logger.info("*2. client_id: " + clientId); logger.info("*3. state: " + state); logger.info("*4. scope: " + scope); logger.info("*5. redirect_uri: " + redirectUri); ModelAndView mav = new ModelAndView(); ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId); if (clientInfo == null) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ERROR.INVALID_CLIENT); } else { logger.info("*[clientinfo]"); logger.info("*" + clientInfo.toString()); if (!responseType.equals("code")) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.UNSUPPORTED_GRANT_TYPE); } else { httpSession.setAttribute("redirectUri", redirectUri); httpSession.setAttribute("state", state); // TODO scope != null -> httpSession.setAttribute("scope", scope); List complexInfos = complexInfoRepo.findAll(); model.addAttribute("complexInfos", complexInfos); // \src\main\webapp\WEB-INF\jsp\InsertUserInfo.jsp mav.setViewName("InsertUserInfo"); } } return mav; } @RequestMapping(value = "/user/info/submit", method = RequestMethod.GET) public ModelAndView sendCode(HttpSession httpSession, HttpServletResponse httpResponse, @RequestParam(value = "complex", required = true) String complex, @RequestParam(value = "dong", required = true) String dong, @RequestParam(value = "ho", required = true) String ho) throws IOException, NoSuchAlgorithmException, GeneralSecurityException { logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("[STEP #2] /api/oauth2/user/submit"); logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("*[parameters]"); logger.info("*1. complex: " + complex); logger.info("*2. dong: " + dong); logger.info("*3. ho: " + ho); // TODO 단지서버로 인증번호 전송해야함. // wallpad 코드 생성 Random generator = new Random(); String wallpadCode = ""; for (int i = 0; i < 6; i++) { wallpadCode += Integer.toString(generator.nextInt(10)); } logger.info("*[wallpadCode]"); logger.info("*code: " + wallpadCode); // 세대정보 인코딩 String homeInfo = complex + "/" + dong + "/" + ho; // db에 저장 ValueOperations vop = redisTemplate.opsForValue(); Duration d = Duration.ofSeconds(180); while (!vop.setIfAbsent(wallpadCode, homeInfo, d)) { wallpadCode = ""; for (int i = 0; i < 6; i++) { wallpadCode += Integer.toString(generator.nextInt(10)); } } logger.info("*[redis]"); logger.info("*(key,value): (" + wallpadCode + "," + homeInfo + ")"); // 단지서버로 월패드인증번호 전송 WallpadCode body = new WallpadCode(dong, ho, wallpadCode); logger.info(body.toString()); String url = "http://" + complex + ":8002/kakao/auth"; logger.info(url); // TODO 예외처리 단지통신 HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); factory.setReadTimeout(3000); factory.setConnectTimeout(3000); RestTemplate template = new RestTemplate(factory); HttpEntity entity = new HttpEntity(body); try { String answer = template.postForObject(url, entity, String.class); logger.info(answer); } catch (Exception e) { new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_HOMEINFO); } ModelAndView mav = new ModelAndView(); mav.setViewName("InsertCode"); return mav; } // InsertCode.jsp에서 월패드 인증번호를 입력받아 ajax로 실행하는 부분, // 사용자가 입력한 인증번호의 유효성을 검증함. @RequestMapping(value = "/validate", method = RequestMethod.GET) public @ResponseBody Boolean validate(HttpSession httpSession, @RequestParam(value = "code", required = true) String wallpadCode) { logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("[STEP #3] /api/oauth2/auth"); logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("*[parameters]"); logger.info("*1. Input code: " + wallpadCode); ValueOperations vop = redisTemplate.opsForValue(); if (vop.get(wallpadCode) == null) { // 없으면 null return false; } httpSession.setAttribute("homeId", vop.get(wallpadCode)); return true; } // 월패드 인증이 실행된 경우, 인증 코드를 발급해 리다이렉트하는 부분 @RequestMapping(value = "/code/generate", method = RequestMethod.GET) public RedirectView validateCode(HttpSession httpSession) { logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("[STEP #4] /code/generate"); logger.info("**************************************************************"); logger.info("**************************************************************"); logger.info("*[parameters]"); logger.info("*1. home id: " + httpSession.getAttribute("homeId")); String code = ""; String homeInfo = ""; ValueOperations vop = redisTemplate.opsForValue(); // 코드 발급 homeInfo = httpSession.getAttribute("homeId").toString(); logger.info("*[home info]"); logger.info("*1. homeInfo: " + homeInfo); code = ""; Random rnd = new Random(); for (int i = 0; i < 10; i++) { code += String.valueOf((char) ((int) (rnd.nextInt(26)) + 65)); } logger.info("*[code]"); logger.info("*1. code: " + code); Duration d = Duration.ofSeconds(180); while (!vop.setIfAbsent(code, homeInfo, d)) { code = ""; for (int i = 0; i < 10; i++) { code += String.valueOf((char) ((int) (rnd.nextInt(26)) + 65)); } } String redirectUri = (String) httpSession.getAttribute("redirectUri"); redirectUri += "?state=" + httpSession.getAttribute("state"); redirectUri += "&code=" + code; RedirectView redirectView = new RedirectView(); redirectView.setUrl(redirectUri); return redirectView; } // 토큰 발급하는 부분 @RequestMapping(value = "/token", method = RequestMethod.POST, produces = "application/json") public @ResponseBody Token Token(@RequestParam(value = "grant_type", required = true) String grantType, @RequestParam(value = "code", required = false) String code, @RequestParam(value = "refresh_token", required = false) String refreshToken, HttpSession httpSession) throws NoSuchAlgorithmException, UnsupportedEncodingException, GeneralSecurityException { logger.info("[STEP #5] /api/oauth2/token"); // TODO Code유효성 검사 // TODO token 발급 logger.info(code); logger.info(grantType); ValueOperations vop = redisTemplate.opsForValue(); String newAccessToken = ""; // 생성해야 함. String newRefreshToken = ""; // 생성해야 함. Token token = new Token(); if (grantType.equals("authorization_code")) { // TODO 코드 검사 if (vop.get(code) == null) { // 인증코드가 없는 경우 // 에러페이지 리턴 throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_GRANT); } else { AES256Util aes = new AES256Util(); String homeInfo = vop.get(code).toString(); String jwt_homeInfo = homeInfo.split("/")[1] + "/" + homeInfo.split("/")[2]; String encodedHomeInfo = aes.encrypt(jwt_homeInfo); logger.info("encodedHomeInfo =" + encodedHomeInfo); logger.info("encoded = " + encodedHomeInfo); String complexIp = homeInfo.split("/")[0]; Encoder encoder = Base64.getEncoder(); complexIp = new String(encoder.encode(complexIp.getBytes())); JWTInfo jwtinfo = new JWTInfo(complexIp, encodedHomeInfo); newAccessToken = JWTUtils.generateToken(jwtinfo, "accessToken"); newRefreshToken = JWTUtils.generateToken(jwtinfo, "refreshToken"); } } else if (grantType.equals("refresh_token")) { // TODO refreshToken 검사 // jwt토큰 디코딩해서 내역 검사해야됨. // 시그니처 확인해야됨 if (refreshToken != null) { Boolean bool = JWTUtils.validateToken(refreshToken, "refreshToken"); logger.info(bool.toString()); if (bool) { // TODO token의 payload에서 compelxCd, homeId 가져오고 새로 토큰 생성한다 JWTInfo jwtinfo = JWTUtils.getJWTInfoFromToken(refreshToken); newAccessToken = JWTUtils.generateToken(jwtinfo, "accessToken"); newRefreshToken = JWTUtils.generateToken(jwtinfo, "refreshToken"); } else { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_REFRESHTOKEN); } } } else { throw new ResponseStatusException(HttpStatus.BAD_REQUEST); } token.setAccess_token(newAccessToken); token.setRefresh_token(newRefreshToken); // class 만들어서 사용하는 것으로 변경해야함. logger.info(token.toString()); return token; } // ((Test)) Redirect Destination // 여기는 리다이렉트 받는 URl임 테스트용. Client를 대신함. @RequestMapping(value = "/redirect", method = RequestMethod.GET) public @ResponseBody String RedirectTester(HttpSession httpSession, @RequestParam(value = "code", required = true) String code) { logger.info("/api/oauth2/redirect"); String redirectUri = "http://61.33.215.56:8003/api/oauth2/token?grant_type=authorization_code&code=" + code; RedirectView redirectView = new RedirectView(); redirectView.setUrl(redirectUri); return code; } }