7 Revīzijas 37d1f08fca ... 78cda9676c

Autors SHA1 Ziņojums Datums
  leeheejo 78cda9676c 210604 master 병합용 커밋 3 gadi atpakaļ
  leeheejo d876cb307a 210604 최신버전 3 gadi atpakaļ
  leeheejo a041ff52b8 20200309 5 gadi atpakaļ
  leeheejo 7f7a377098 20191001 5 gadi atpakaļ
  leeheejo f1ea28ca8b fix ui 5 gadi atpakaļ
  leeheejo 65dd4c22b2 20190502 중간 커밋 - 갱신 시 헤더 검사 추가 6 gadi atpakaļ
  leeheejo 65995f4f21 중간커밋 6 gadi atpakaļ

+ 185 - 51
java/com/icontrols/oauth/controller/OAuthController.java

@@ -13,6 +13,7 @@ import java.util.Random;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 
+import org.json.JSONObject;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -43,7 +44,9 @@ 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.DanziAuthUtils;
 import com.icontrols.oauth.utils.JWTUtils;
+import com.sun.net.httpserver.HttpServer;
 
 @RestController
 @RequestMapping(value = "/api/oauth2")
@@ -61,7 +64,7 @@ public class OAuthController {
 
 //	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 @ResponseBody ModelAndView Authorize(@RequestHeader HttpHeaders headers,
+	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,
@@ -76,8 +79,14 @@ public class OAuthController {
 		logger.info("*1. response_type: " + responseType);
 		logger.info("*2. client_id: " + clientId);
 		logger.info("*3. state: " + state);
-		logger.info("*4. scope: " + scope);
+//		logger.info("*4. scope: " + scope);
 		logger.info("*5. redirect_uri: " + redirectUri);
+		// 월패드 인증코드 시간이 만료되어 history.go(-1)로 해당 페이지에 들어오는 경우에
+		// 세션에 저장되었던 사용자 정보를 지움.
+		if (httpSession.getAttribute("wallpadCode") != null)
+			httpSession.removeAttribute("wallpadCode");
+		if (httpSession.getAttribute("homeInfo") != null)
+			httpSession.removeAttribute("homeInfo");
 
 		ModelAndView mav = new ModelAndView();
 
@@ -92,11 +101,14 @@ public class OAuthController {
 			if (!responseType.equals("code")) {
 				throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.UNSUPPORTED_GRANT_TYPE);
 			} else {
-
+				// 클라이언트에 따라 endpoint를 변경해야해서 세션에 클라이언트 정보를 저장.
+				httpSession.setAttribute("clientId", clientId);
 				httpSession.setAttribute("redirectUri", redirectUri);
 				httpSession.setAttribute("state", state);
 
-				// TODO scope != null -> httpSession.setAttribute("scope", scope);
+//				if (scope != null)
+//					httpSession.setAttribute("scope", scope);
+
 				List<ComplexInfo> complexInfos = complexInfoRepo.findAll();
 				model.addAttribute("complexInfos", complexInfos);
 
@@ -109,8 +121,8 @@ public class OAuthController {
 		return mav;
 	}
 
-	@RequestMapping(value = "/user/info/submit", method = RequestMethod.POST)
-	public ModelAndView sendCode(HttpSession httpSession, HttpServletResponse httpResponse,
+	@RequestMapping(value = "/user/info/submit", method = RequestMethod.GET)
+	public ModelAndView sendCode(HttpSession httpSession,
 			@RequestParam(value = "complex", required = true) String complex,
 			@RequestParam(value = "dong", required = true) String dong,
 			@RequestParam(value = "ho", required = true) String ho)
@@ -126,7 +138,7 @@ public class OAuthController {
 		logger.info("*2. dong: " + dong);
 		logger.info("*3. ho: " + ho);
 
-		// TODO 단지서버로 인증번호 전송해야함.
+		String clientId = httpSession.getAttribute("clientId").toString();
 
 		// wallpad 코드 생성
 		Random generator = new Random();
@@ -134,20 +146,30 @@ public class OAuthController {
 		for (int i = 0; i < 6; i++) {
 			wallpadCode += Integer.toString(generator.nextInt(10));
 		}
+
 		logger.info("*[wallpadCode]");
 		logger.info("*code: " + wallpadCode);
 
-//		http://61.33.215.54:8002/iotservice/code/request
-//		{
-//			 "dong":"802", 
-//			 "ho": "704",
-//			 "code" :"931206",
-//			}
+		// 세대정보 인코딩
+		String homeInfo = complex + "/" + dong + "/" + ho + "/" + clientId;
+
+		// 2019.05.02 DB저장에서 세션 저장 방식으로 변경
+		httpSession.setAttribute("wallpadCode", wallpadCode);
+		httpSession.setAttribute("homeInfo", homeInfo);
+
+		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";
+
+		ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId);
+		String endPoint = clientInfo.getEndPoint();
+
+//		String url = "http://" + complex + ":8002/kakao/auth";
+		String url = "http://" + complex + endPoint + "/auth";
+
 		logger.info(url);
 
 		// TODO 예외처리 단지통신
@@ -163,15 +185,6 @@ public class OAuthController {
 			new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_HOMEINFO);
 		}
 
-		// 세대정보 인코딩
-		String homeInfo = complex + "/" + dong + "/" + ho;
-		// db에 저장
-		ValueOperations<String, Object> vop = redisTemplate.opsForValue();
-		Duration d = Duration.ofSeconds(180);
-		vop.set(wallpadCode, homeInfo, d);
-		logger.info("*[redis]");
-		logger.info("*(key,value): (" + wallpadCode + "," + homeInfo + ")");
-
 		ModelAndView mav = new ModelAndView();
 		mav.setViewName("InsertCode");
 		return mav;
@@ -190,12 +203,25 @@ public class OAuthController {
 		logger.info("*[parameters]");
 		logger.info("*1. Input code: " + wallpadCode);
 
-		ValueOperations<String, Object> vop = redisTemplate.opsForValue();
-		if (vop.get(wallpadCode) == null) { // 없으면 null
-
+//		ValueOperations<String, Object> vop = redisTemplate.opsForValue();
+//		if (vop.get(wallpadCode) == null) { // 없으면 null
+//
+//			return false;
+//		}
+//		httpSession.setAttribute("homeId", vop.get(wallpadCode));
+
+		// 2019.05.02 DB저장에서 세션 저장 방식으로 변경
+		logger.info(httpSession.getAttributeNames().toString());
+		if (httpSession.getAttribute("wallpadCode") == null) {
+			logger.info("null");
 			return false;
+		} else {
+			if (!httpSession.getAttribute("wallpadCode").toString().equals(wallpadCode)) {
+				logger.info(httpSession.getAttribute("wallpadCode").toString());
+				return false;
+			}
 		}
-		httpSession.setAttribute("homeId", vop.get(wallpadCode));
+		logger.info(httpSession.getAttribute("wallpadCode").toString());
 		return true;
 	}
 
@@ -208,14 +234,14 @@ public class OAuthController {
 		logger.info("**************************************************************");
 		logger.info("**************************************************************");
 		logger.info("*[parameters]");
-		logger.info("*1. home id: " + httpSession.getAttribute("homeId"));
+		logger.info("*1. home id: " + httpSession.getAttribute("homeInfo"));
 
 		String code = "";
 		String homeInfo = "";
 		ValueOperations<String, Object> vop = redisTemplate.opsForValue();
 
 		// 코드 발급
-		homeInfo = httpSession.getAttribute("homeId").toString();
+		homeInfo = httpSession.getAttribute("homeInfo").toString();
 		logger.info("*[home info]");
 		logger.info("*1. homeInfo: " + homeInfo);
 		code = "";
@@ -227,7 +253,13 @@ public class OAuthController {
 		logger.info("*1. code: " + code);
 
 		Duration d = Duration.ofSeconds(180);
-		vop.set(code, homeInfo, d);
+
+		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");
@@ -240,21 +272,23 @@ public class OAuthController {
 	}
 
 	// 토큰 발급하는 부분
-	@RequestMapping(value = "/token", method = RequestMethod.GET, produces = "application/json")
-	public @ResponseBody Token Token(@RequestParam(value = "grant_type", required = true) String grantType,
+	@RequestMapping(value = "/token", method = RequestMethod.POST, produces = "application/json")
+	public @ResponseBody String 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)
+			@RequestParam(value = "refresh_token", required = false) String refreshToken, HttpSession httpSession,
+			@RequestHeader HttpHeaders headers, HttpServletResponse response)
 			throws NoSuchAlgorithmException, UnsupportedEncodingException, GeneralSecurityException {
 
+		response.setStatus(HttpServletResponse.SC_OK);
 		logger.info("[STEP #5] /api/oauth2/token");
 		// TODO Code유효성 검사
 		// TODO token 발급
-		logger.info(code);
-		logger.info(grantType);
+		logger.info("grant_type: " + grantType);
 		ValueOperations<String, Object> vop = redisTemplate.opsForValue();
 
 		String newAccessToken = ""; // 생성해야 함.
 		String newRefreshToken = ""; // 생성해야 함.
+		String answer = null; // 단지서버에 토큰 관련요청 후 수신하는 응답
 		Token token = new Token();
 		if (grantType.equals("authorization_code")) {
 			// TODO 코드 검사
@@ -273,39 +307,133 @@ public class OAuthController {
 				String complexIp = homeInfo.split("/")[0];
 				Encoder encoder = Base64.getEncoder();
 				complexIp = new String(encoder.encode(complexIp.getBytes()));
-				JWTInfo jwtinfo = new JWTInfo(complexIp, encodedHomeInfo);
+				JWTInfo jwtinfo = new JWTInfo(complexIp, encodedHomeInfo, homeInfo.split("/")[3]);
 				newAccessToken = JWTUtils.generateToken(jwtinfo, "accessToken");
 				newRefreshToken = JWTUtils.generateToken(jwtinfo, "refreshToken");
 
+				token.setAccess_token(newAccessToken);
+				token.setRefresh_token(newRefreshToken);
+
+				String clientId = JWTUtils.getClientInfoFromToken(newAccessToken);
+				ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId);
+				String url = clientInfo.getEndPoint();
+
+				answer = DanziAuthUtils.create(token, url);
+
 			}
 		} 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 {
+				// 1. 정상적으로 생성되었는지 확인
+				if (!JWTUtils.validateToken(refreshToken, "refreshToken"))
 					throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_REFRESHTOKEN);
+
+				// client정보검사
+				if (headers.get("Authorization").get(0) == null)
+					throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ERROR.INVALID_CLIENT);
+
+//				if (!validateHeaderAuth(headers.get("Authorization").get(0)))
+//					throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ERROR.INVALID_CLIENT);
+
+				// 단지에 갱신토큰 유효성 확인
+				String clientId = JWTUtils.getClientInfoFromToken(refreshToken);
+				ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId);
+				String url = clientInfo.getEndPoint();
+				String isValidToken = DanziAuthUtils.get(url, refreshToken);
+				logger.info(isValidToken);
+				JSONObject isValidTokenJson = new JSONObject(isValidToken);
+				logger.info(isValidTokenJson.toString());
+
+				if (!isValidTokenJson.get("result").toString().equalsIgnoreCase("success")) {
+
+					if (isValidTokenJson.getJSONObject("error").getInt("code") == -200) // 서버에러인경우
+						throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
+
+					response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+					logger.info(isValidTokenJson.get("result").toString());
+					JSONObject error = new JSONObject();
+					error.put("error", "invalid_grant");
+					error.put("error_description", "invalid token");
+					return error.toString();
 				}
+
+				// 유효한 토큰인 경우 토큰 생성
+				JWTInfo jwtinfo = JWTUtils.getJWTInfoFromToken(refreshToken);
+				newAccessToken = JWTUtils.generateToken(jwtinfo, "accessToken");
+				newRefreshToken = JWTUtils.generateToken(jwtinfo, "refreshToken");
+				token.setAccess_token(newAccessToken);
+				token.setRefresh_token(newRefreshToken);
+
+				// 단지서버에 갱신된 토큰 전달
+				answer = DanziAuthUtils.refresh(url, token, refreshToken);
+				logger.info(answer);
+
 			}
 		} else {
 			throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
 		}
 
-		token.setAccess_token(newAccessToken);
-		token.setRefresh_token(newRefreshToken);
+		// 갱신된 토큰 전달에 대한 응답처리
+		logger.info(answer);
+		if (answer == null) {
+			throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
+		} else {
+			JSONObject obj = new JSONObject(answer);
+			logger.info(obj.get("result").toString());
+			if (!obj.get("result").toString().equalsIgnoreCase("success")) {
+
+				if (obj.getJSONObject("error").getInt("code") == -200) // 서버에러인경우
+					throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
+
+				response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+				JSONObject error = new JSONObject();
+				error.put("error", "invalid_grant");
+				error.put("error_description", "invalid token");
+				return error.toString();
+			}
+		}
+
 		// class 만들어서 사용하는 것으로 변경해야함.
 		logger.info(token.toString());
 
-		return token;
+		// TODO 단지서버로 전송하고 정상 응답 받은 경우에만 정상리턴 -> 아닌경우에는 500
+		// 토큰 생성인 경우와 갱신인 경우 구분해서 처리 create, refresh
+
+		JSONObject tokenResult = new JSONObject();
+		tokenResult.put("access_token", token.getAccess_token());
+		tokenResult.put("refresh_token", token.getRefresh_token());
+		tokenResult.put("token_type", token.getToken_type());
+		tokenResult.put("expires_in", token.getExpires_in());
+
+		return tokenResult.toString();
+
+	}
+
+	public boolean validateHeaderAuth(String authorization) {
+		String splitStr[] = authorization.split(" ");
 
+		// splitStr length 가 2가 아니면 유효하지 않은 authorization 값
+		if (splitStr.length != 2)
+			return false;
+
+		// splitStr이 Basic이 아니어도 에러
+		if (!splitStr[0].equals("Basic"))
+			return false;
+
+		// Base64 디코드
+		String decodeStr = new String(Base64.getDecoder().decode(splitStr[1].trim()));
+
+		splitStr = decodeStr.split(":");
+		// splitStr length 가 2가 아니면 유효하지 않은 authorization 값
+		if (splitStr.length != 2)
+			return false;
+
+		ClientInfo clientInfo = clientInfoRepo.findByClientId(splitStr[0]);
+		if (clientInfo == null)
+			return false;
+		if (!clientInfo.getClientSecret().equals(splitStr[1]))
+			return false;
+
+		return true;
 	}
 
 	// ((Test)) Redirect Destination
@@ -321,4 +449,10 @@ public class OAuthController {
 
 	}
 
+	@RequestMapping(value = "/aws/healthcheck", method = RequestMethod.GET)
+	public void healthCheck(HttpEntity<String> httpEntity) {
+
+		return;
+	}
+
 }

+ 15 - 8
java/com/icontrols/oauth/controller/ViewController.java

@@ -4,6 +4,7 @@ import java.io.UnsupportedEncodingException;
 import java.security.GeneralSecurityException;
 import java.security.NoSuchAlgorithmException;
 import java.time.Duration;
+import java.util.Random;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -31,13 +32,19 @@ public class ViewController {
 	public String validTest(@RequestParam(value = "token", required = false) String token)
 			throws UnsupportedEncodingException, NoSuchAlgorithmException, GeneralSecurityException {
 
-		JWTUtils jwt = new JWTUtils();
-		Boolean bool = jwt.validateToken(token, "accessToken");
-		logger.info("validate :" + bool.toString());
-		String cmpIp = jwt.getCmpIpFromToken(token);
-		logger.info("cmpIp :" + cmpIp);
-		String homeId = jwt.getHomeIdFromToken(token);
-		logger.info("homeId :" + homeId);
-		return bool.toString();
+		ValueOperations<String, Object> vop = redisTemplate.opsForValue();
+		Random generator = new Random();
+		String wallpadCode = "";
+		for (int i = 0; i < 6; i++) {
+			wallpadCode += Integer.toString(generator.nextInt(10));
+		}
+		while (!vop.setIfAbsent(wallpadCode, "value")) {
+			wallpadCode = "";
+			for (int i = 0; i < 6; i++) {
+				wallpadCode += Integer.toString(generator.nextInt(10));
+			}
+		}
+
+		return "";
 	}
 }

+ 20 - 1
java/com/icontrols/oauth/model/ClientInfo.java

@@ -20,6 +20,8 @@ public class ClientInfo {
 	private String clientCode;
 	@Column(name = "client_key")
 	private String clientSecret;
+	@Column(name = "end_point")
+	private String endPoint;
 
 	public String getClientId() {
 		return clientId;
@@ -53,6 +55,14 @@ public class ClientInfo {
 		this.clientSecret = clientSecret;
 	}
 
+	public String getEndPoint() {
+		return endPoint;
+	}
+
+	public void setEndPoint(String endPoint) {
+		this.endPoint = endPoint;
+	}
+
 	public ClientInfo(String clientId, String clientName, String clientCode, String clientSecret) {
 		super();
 		this.clientId = clientId;
@@ -61,6 +71,15 @@ public class ClientInfo {
 		this.clientSecret = clientSecret;
 	}
 
+	public ClientInfo(String clientId, String clientName, String clientCode, String clientSecret, String endPoint) {
+		super();
+		this.clientId = clientId;
+		this.clientName = clientName;
+		this.clientCode = clientCode;
+		this.clientSecret = clientSecret;
+		this.endPoint = endPoint;
+	}
+
 	public ClientInfo() {
 		super();
 	}
@@ -68,7 +87,7 @@ public class ClientInfo {
 	@Override
 	public String toString() {
 		return "ClientInfo [clientId=" + clientId + ", clientName=" + clientName + ", clientCode=" + clientCode
-				+ ", clientSecret=" + clientSecret + "]";
+				+ ", clientSecret=" + clientSecret + ", endPoint=" + endPoint + "]";
 	}
 
 }

+ 14 - 33
java/com/icontrols/oauth/model/JWTInfo.java

@@ -5,6 +5,7 @@ import java.util.Date;
 public class JWTInfo {
 	private String cmpIp;
 	private String homeId;
+	private String clientInfo;
 //	private Date exp;
 //	private Date iat;
 
@@ -12,18 +13,12 @@ public class JWTInfo {
 		super();
 	}
 
-//	public JWTInfo(String cmpIp, String homeId, Date exp, Date iat) {
-//		super();
-//		this.cmpIp = cmpIp;
-//		this.homeId = homeId;
-////		this.exp = exp;
-////		this.iat = iat;
-//	}
+	public String getClientInfo() {
+		return clientInfo;
+	}
 
-	public JWTInfo(String cmpIp, String homeId) {
-		super();
-		this.cmpIp = cmpIp;
-		this.homeId = homeId;
+	public void setClientInfo(String clientInfo) {
+		this.clientInfo = clientInfo;
 	}
 
 	public String getCmpIp() {
@@ -42,30 +37,16 @@ public class JWTInfo {
 		this.homeId = homeId;
 	}
 
+	public JWTInfo(String cmpIp, String homeId, String clientInfo) {
+		super();
+		this.cmpIp = cmpIp;
+		this.homeId = homeId;
+		this.clientInfo = clientInfo;
+	}
+
 	@Override
 	public String toString() {
-		return "JWTInfo [cmpIp=" + cmpIp + ", homeId=" + homeId + "]";
+		return "JWTInfo [cmpIp=" + cmpIp + ", homeId=" + homeId + ", clientInfo=" + clientInfo + "]";
 	}
 
-//	public Date getExp() {
-//		return exp;
-//	}
-//
-//	public void setExp(Date exp) {
-//		this.exp = exp;
-//	}
-//
-//	public Date getIat() {
-//		return iat;
-//	}
-//
-//	public void setIat(Date iat) {
-//		this.iat = iat;
-//	}
-//
-//	@Override
-//	public String toString() {
-//		return "JWTInfo [cmpIp=" + cmpIp + ", homeId=" + homeId + ", exp=" + exp + ", iat=" + iat + "]";
-//	}
-
 }

+ 1 - 1
java/com/icontrols/oauth/model/Token.java

@@ -57,7 +57,7 @@ public class Token {
 
 	public Token() {
 		super();
-		// TODO Auto-generated constructor stub
+		 
 	}
 
 	@Override

+ 2 - 0
java/com/icontrols/oauth/repo/ComplexInfoRepository.java

@@ -3,9 +3,11 @@ package com.icontrols.oauth.repo;
 import java.util.List;
 
 import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
 
 import com.icontrols.oauth.model.ComplexInfo;
 
+@Repository
 public interface ComplexInfoRepository extends CrudRepository<ComplexInfo, Long> {
 
 	List<ComplexInfo> findAll();

+ 127 - 0
java/com/icontrols/oauth/utils/DanziAuthUtils.java

@@ -0,0 +1,127 @@
+package com.icontrols.oauth.utils;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.tomcat.util.json.JSONParser;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+import com.icontrols.oauth.model.ClientInfo;
+import com.icontrols.oauth.model.Token;
+import com.icontrols.oauth.repo.ClientInfoRepository;
+
+public class DanziAuthUtils {
+	private static final Logger logger = LoggerFactory.getLogger(DanziAuthUtils.class);
+
+	@Autowired
+	ClientInfoRepository clientInfoRepo;
+
+	public static String create(Token newToken, String url)
+			throws UnsupportedEncodingException, NoSuchAlgorithmException, GeneralSecurityException {
+
+		Map<String, Object> body = new HashMap<>();
+
+		Map<String, Object> token = new HashMap<>();
+		token.put("accessToken", newToken.getAccess_token());
+		token.put("refreshToken", newToken.getRefresh_token());
+
+		body.put("id", System.currentTimeMillis() + "");
+		body.put("type", "CREATE");
+		body.put("body", token);
+
+		logger.info(body.toString());
+
+		String complex = JWTUtils.getCmpIpFromToken(newToken.getAccess_token());
+		String endpoint = "http://" + complex + url + "/accessToken";
+
+		HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
+		factory.setReadTimeout(3000);
+		factory.setConnectTimeout(3000);
+		RestTemplate template = new RestTemplate(factory);
+		HttpEntity<Object> entity = new HttpEntity<Object>(body);
+		String answer = null;
+		try {
+			answer = template.postForObject(endpoint, entity, String.class);
+		} catch (Exception e) {
+			return null;
+		}
+
+//		logger.info(answer);
+//		JSONObject obj = new JSONObject(answer);
+//		if (!obj.get("result").toString().equalsIgnoreCase("success")) {
+//			return false;
+//		}
+
+		return answer;
+	}
+
+	public static String refresh(String url, Token newToken, String prevToken) {
+
+		Map<String, Object> body = new HashMap<>();
+		Map<String, Object> token = new HashMap<>();
+		token.put("prevRefreshToken", prevToken);
+		token.put("newAccessToken", newToken.getAccess_token());
+		token.put("newRefreshToken", newToken.getRefresh_token());
+
+		body.put("id", System.currentTimeMillis() + "");
+		body.put("type", "REFRESH");
+		body.put("body", token);
+
+		String complex = JWTUtils.getCmpIpFromToken(prevToken);
+		String endpoint = "http://" + complex + url + "/accessToken";
+
+		HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
+		factory.setReadTimeout(3000);
+		factory.setConnectTimeout(3000);
+		RestTemplate template = new RestTemplate(factory);
+		HttpEntity<Object> entity = new HttpEntity<Object>(body);
+		String answer = null;
+		try {
+			answer = template.postForObject(endpoint, entity, String.class);
+		} catch (Exception e) {
+			return null;
+		}
+
+		return answer;
+	}
+
+	public static String get(String url, String refreshToken) {
+
+		Map<String, Object> body = new HashMap<>();
+		Map<String, Object> token = new HashMap<>();
+		token.put("refreshToken", refreshToken);
+
+		body.put("id", System.currentTimeMillis() + "");
+		body.put("type", "GET");
+		body.put("body", token);
+
+		String complex = JWTUtils.getCmpIpFromToken(refreshToken);
+		String endpoint = "http://" + complex + url + "/accessToken";
+
+		HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
+		factory.setReadTimeout(3000);
+		factory.setConnectTimeout(3000);
+		RestTemplate template = new RestTemplate(factory);
+		HttpEntity<Object> entity = new HttpEntity<Object>(body);
+		String answer = null;
+		try {
+			answer = template.postForObject(endpoint, entity, String.class);
+		} catch (Exception e) {
+			return null;
+		}
+
+		logger.info(answer);
+
+		return answer;
+	}
+
+}

+ 40 - 15
java/com/icontrols/oauth/utils/JWTUtils.java

@@ -35,15 +35,15 @@ public class JWTUtils {
 		String jwt = "";
 		if (type.equals("accessToken")) {
 			secretKey = CONSTANTS.ACCESS_TOEKN_SECRET_KEY;
-			jwt = Jwts.builder().setExpiration(new Date(3600000)) // 1시간 -> 설정정보로 변경해야함
+			jwt = Jwts.builder().setExpiration(new Date(43200000)) // 1시간 -> 설정정보로 변경해야함
 					.claim("cmpIp", info.getCmpIp()).claim("homeId", info.getHomeId())
-					.claim("iat", System.currentTimeMillis() / 1000)
+					.claim("iat", System.currentTimeMillis() / 1000).claim("clientInfo", info.getClientInfo())
 					.signWith(SignatureAlgorithm.HS256, secretKey.getBytes("UTF-8")).compact();
 		} else if (type.equals("refreshToken")) {
 			secretKey = CONSTANTS.REFRESH_TOKEN_SECRET_KEY;
-			jwt = Jwts.builder().setExpiration(new Date(43200000)) // 1일 -> 설정정보로 변경해야함
+			jwt = Jwts.builder().setExpiration(new Date(259200000)) // 1일 -> 설정정보로 변경해야함
 					.claim("cmpIp", info.getCmpIp()).claim("homeId", info.getHomeId())
-					.claim("iat", System.currentTimeMillis() / 1000)
+					.claim("iat", System.currentTimeMillis() / 1000).claim("clientInfo", info.getClientInfo())
 					.signWith(SignatureAlgorithm.HS256, secretKey.getBytes("UTF-8")).compact();
 		}
 
@@ -58,27 +58,38 @@ public class JWTUtils {
 		} else if (type.equals("refreshToken")) {
 			secretKey = CONSTANTS.REFRESH_TOKEN_SECRET_KEY;
 		}
-		logger.info(token);
+		logger.info("origin = " + token);
+
+		if (token.split("\\.")[0] == null || token.split("\\.")[1] == null || token.split("\\.")[2] == null)
+			return false;
+
 		Decoder decoder = Base64.getDecoder();
 		// 시그니처가 맞는지 확인
-		String enHeader = token.split("\\.")[0];
-		logger.info("H= " + enHeader);
+//		String enHeader = token.split("\\.")[0];
+//		logger.info("H= " + enHeader);
 		String enPayload = token.split("\\.")[1];
 		String dePayload = new String(decoder.decode(enPayload));
-		logger.info("P= " + enPayload);
-		String signiture = token.split("\\.")[2];
-		logger.info("S= " + signiture);
+//		logger.info("P= " + enPayload);
+//		String signiture = token.split("\\.")[2];
+//		logger.info("S= " + signiture);
 		JSONObject obj = new JSONObject(dePayload);
 
+		if (obj.isNull("cmpIp") || obj.isNull("homeId") || obj.isNull("iat") || obj.isNull("clientInfo"))
+			return false;
+
 		String jwt = Jwts.builder().claim("exp", obj.get("exp")) // 1시간 -> 설정정보로 변경해야함
 				.claim("cmpIp", obj.get("cmpIp")).claim("homeId", obj.get("homeId")).claim("iat", obj.get("iat"))
+				.claim("clientInfo", obj.get("clientInfo"))
 				.signWith(SignatureAlgorithm.HS256, secretKey.getBytes("UTF-8")).compact();
 		logger.info("new = " + jwt);
-		if (token.equals(jwt)) {
-			long iat = Long.parseLong(obj.get("iat").toString());
-			long currentTime = System.currentTimeMillis() / 1000;
-			if (currentTime > iat + Long.parseLong(obj.get("exp").toString())) {
-				return false;
+
+		if (token.equals(jwt)) { // 갱신인 경우에는 시간 검사를 안하기 위해
+			if (type.equals("accessToken")) {
+				long iat = Long.parseLong(obj.get("iat").toString());
+				long currentTime = System.currentTimeMillis() / 1000;
+				if (currentTime > iat + Long.parseLong(obj.get("exp").toString())) {
+					return false;
+				}
 			}
 			return true;
 		}
@@ -97,6 +108,7 @@ public class JWTUtils {
 		JWTInfo jwtInfo = new JWTInfo();
 		jwtInfo.setCmpIp(obj.get("cmpIp").toString());
 		jwtInfo.setHomeId(obj.get("homeId").toString());
+		jwtInfo.setClientInfo(obj.get("clientInfo").toString());
 
 		return jwtInfo;
 	}
@@ -131,4 +143,17 @@ public class JWTUtils {
 		return homeId;
 	}
 
+	static public String getClientInfoFromToken(String token)
+			throws UnsupportedEncodingException, NoSuchAlgorithmException, GeneralSecurityException {
+
+		Decoder decoder = Base64.getDecoder();
+		String enPayload = token.split("\\.")[1];
+		String dePayload = new String(decoder.decode(enPayload));
+		JSONObject obj = new JSONObject(dePayload);
+
+		String clientInfo = obj.get("clientInfo").toString();
+
+		return clientInfo;
+	}
+
 }

+ 22 - 0
pom.xml

@@ -101,6 +101,28 @@
 			<artifactId>httpclient</artifactId>
 			<version>4.5.6</version>
 		</dependency>
+
+		<dependency>
+			<groupId>javax.xml.bind</groupId>
+			<artifactId>jaxb-api</artifactId>
+			<version>2.3.0</version>
+		</dependency>
+
+
+		<dependency>
+			<groupId>org.hibernate</groupId>
+			<artifactId>hibernate-core</artifactId>
+			<version>5.2.3.Final</version>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-jdbc</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.glassfish.web</groupId>
+			<artifactId>el-impl</artifactId>
+			<version>2.2</version>
+		</dependency>
 	</dependencies>
 
 	<build>

+ 16 - 6
src/main/resources/application.properties

@@ -2,19 +2,29 @@ server.port=8003
 spring.mvc.view.prefix=/WEB-INF/jsp/
 spring.mvc.view.suffix=.jsp
 
+spring.mvc.static-path-pattern=/resources/**
+
 spring.redis.lettuce.pool.max-active=10
 spring.redis.lettuce.pool.max-idle=10
 spring.redis.lettuce.pool.min-idle=2
 spring.redis.port=6379
 spring.redis.host=127.0.0.1
 
-spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
-spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/oauth?useUnicode=true&charaterEncoding=utf-8
+#spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
+#spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/oauth?useUnicode=true&charaterEncoding=utf-8
+#
+#spring.datasource.username=admin
+#spring.datasource.password = hdci12#$
+spring.datasource.driverClassName=org.mariadb.jdbc.Driver
+spring.datasource.url=jdbc:mariadb://hdc-iot-dbinstance.c7ozgzzifmtz.ap-northeast-2.rds.amazonaws.com:3306/oauth?useUnicode=true&characterEncoding=utf-8
+spring.datasource.username=root
+spring.datasource.password=HDCiot2019()
+
 spring.jpa.hibernate.ddl-auto=update
 spring.jpa.generate-ddl=true
-spring.datasource.username=admin
-spring.datasource.password = hdci12#$
+#
+server.ssl.key-store=classpath:tomcat.jks
+server.ssl.key-store-password=hdci12#$
 
-#spring.datasource.username=root
-#spring.datasource.password = 2072712l^^
+logging.config=classpath:logback-spring.xml
 

+ 28 - 0
src/main/resources/logback-spring.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <include resource="org/springframework/boot/logging/logback/base.xml"/>
+
+    <appender name="dailyRollingFileAppender"
+              class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <append>true</append>
+        <prudent>true</prudent>
+
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>log/LogFile_%d{yyyy-MM-dd}.log</fileNamePattern>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>DEBUG</level>
+        </filter>
+
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{35} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <logger name="com.icontrols.uplus.interfaces" level="DEBUG"></logger>
+
+    <root level="INFO">
+        <appender-ref ref="dailyRollingFileAppender" />
+    </root>
+</configuration>

BIN
src/main/resources/static/fonts/NotoSansCJKkr-Medium.otf


BIN
src/main/resources/static/fonts/NotoSansCJKkr-Regular.otf


BIN
src/main/resources/static/img/arrow.png


BIN
src/main/resources/static/img/incon_ci_ko.png


BIN
src/main/resources/static/img/logo.png


BIN
src/main/resources/tomcat.jks


+ 120 - 46
src/main/webapp/WEB-INF/jsp/InsertCode.jsp

@@ -1,5 +1,5 @@
-<%@ page language="java" contentType="text/html; charset=EUC-KR"
-	pageEncoding="EUC-KR"%>
+<%@ page language="java" contentType="text/html; charset=UTF-8"
+	pageEncoding="UTF-8"%>
 <!DOCTYPE html>
 <html>
 <head>
@@ -9,65 +9,139 @@
 	href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
 	integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
 	crossorigin="anonymous">
-<title>Insert title here</title>
+<title>사용� ��</title>
 <style type="text/css">
+@font-face {
+  font-family: NotoSansCJKkr-Medium;
+  src: url('/resources/fonts/NotoSansCJKkr-Medium.otf');
+}
+
+@font-face {
+  font-family: NotoSansCJKkr-Regular;
+  src: url('/resources/fonts/NotoSansCJKkr-Regular.otf');
+}
+
 body {
-	padding-top: 30px;
+	background-color: #e6e1da
 }
 
 @media screen and (min-width : 640px) { /* Styles */
-	.myclass {
+	.head {
+		width: 500px;
+		margin: 0 auto;
+	}
+	.content {	
 		width: 500px;
 		margin: 0 auto;
 	}
 }
+
+.content_option {
+	padding-left: 30px;
+	padding-right: 30px;
+}
+
+input[type=text] {
+	border-radius: 3rem;
+	margin-bottom: 10px;
+	height: 50px;
+}
+
+input::placeholder {
+	color: #9e8d76;
+ 	font-size: 20px;
+	font-family: NotoSansCJKkr-Regular;
+}
+
+.button_option {
+	height: 70px;
+	border-radius: 3rem;
+	margin-bottom: 10px;
+	background-color: #9e8d76;
+	color: #ffffff;
+	font-size: 22px;
+}
+
+.title_option {
+	font-family: NotoSansCJKkr-Medium;
+	color: #000000;
+	font-size: 16px;
+}
+
+.input_option {
+font-family: NotoSansCJKkr-Regular;
+	color: #9e8d76;
+	font-size: 20px;
+	display: block;
+    width: 100%;
+    height: calc(1.5em + .75rem + 2px);
+    padding: .375rem .75rem;
+    font-weight: 400;
+    line-height: 1.5;
+    background-color: #fff;
+    background-clip: padding-box;
+    border: 1px solid #ced4da;
+    border-radius: .25rem;
+    transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
+}
+
+
+img {
+	width: 100%;
+}
+
 </style>
 </head>
-<script src="http://code.jquery.com/jquery-1.11.2.min.js"></script>
+<script src="https://code.jquery.com/jquery-1.11.2.min.js"></script>
 <script type="text/javascript" language="javascript">
-	$(document)
-			.ready(
-					function() {
-						$("#btn")
-								.click(
-										function btnClickHandler() {
-											var code = $("#code").val();
-											if (code == "")
-												alert("¿ùÆÐµå¿¡ Ç¥½ÃµÈ Äڵ带 ÀÔ·ÂÇϼ¼¿ä.");
-											else {
-												console.log(code);
-												$
-														.ajax({
-															url : "/api/oauth2/validate",
-															data : {
-																code : code
-															},
-															success : function(
-																	data,
-																	dataType) {
-																if (data) {
-																	console
-																	console.log("*true");
-																	window.location.href = 'http://61.33.215.56:8003/api/oauth2/code/generate';
-																} else {
-																	alert("¿ùÆÐµå ÄÚµå ÀÎÁõ ½ÇÆÐ");
-																}
-
-															}
-														});
-											}
-										});
+	$(document).ready(function() {
+
+		setInterval(function() {
+			alert("시간� 만료�었습니다.");
+			history.go(-1);
+		}, 180000); // 3000ms(3초)가 경과하면 ozit_timer_test() 함수를 실행합니다.
+
+	});
+
+	function btnClickHandler() {
+		var code = $("#code").val();
+		if (code == "")
+			alert("월패드� 표시� ��번호를 입력하세요.");
+		else {
+			console.log(code);
+			$.ajax({
+						type : "GET",
+						url : "/api/oauth2/validate",
+						data : {
+							code : code
+						},
+						success : function(data) {
+							console.log(data);
+							if (data == true) {
+								//window.location.href = 'http://127.0.0.1:8003/api/oauth2/code/generate';
+								window.location.href = 'https://telproxy.hdc-smart.com/api/oauth2/code/generate';
+								//window.location.href = 'https://proxy.hdc-smart.com/api/oauth2/code/generate';
+								//window.location.href = 'https://teldev.hdc-smart.com/api/oauth2/code/generate';
+							} else {
+								alert("�� 실패");
+							}
+						}
 					});
+
+		}
+	}
 </script>
 <body>
+	<div class="head">
+		<img src="/resources/img/logo.png" />
+	</div>
 	<!-- <form method="post" action="validate"> -->
-	<form class = "myclass">
-		<label for="code">ÀÎÁõ¹øÈ£</label> 
-		<input type="text" class="form-control" id="code" name="code" value="704" required>
-		<br>
-		<br>
-		<button class="btn btn-primary btn-block" id="btn" name="btn">Submit</button>
-		<!-- </form> -->
-	</form>
+	<div class="content content_option">
+		<label for="code" class ="title_option">• ��번호</label> 
+		<input type="text" class="input_option"
+			id="code" name="code" placeholder="월패드� 뜬 ��번호를 입력하세요." required>
+		<input type="button" class="btn button_option btn-block"
+			onClick="javascript:btnClickHandler();" value="��하기" />
+	</div>
 </body>
 </html>

+ 123 - 24
src/main/webapp/WEB-INF/jsp/InsertUserInfo.jsp

@@ -1,5 +1,5 @@
-<%@ page language="java" contentType="text/html; charset=EUC-KR"
-	pageEncoding="EUC-KR"%>
+<%@ page language="java" contentType="text/html; charset=UTF-8"
+	pageEncoding="UTF-8"%>
 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
 <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
 <%
@@ -14,43 +14,142 @@
 	href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
 	integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
 	crossorigin="anonymous">
-<title>Insert title here</title>
+<title>사용� ��</title>
 <style type="text/css">
+@font-face {
+	font-family: NotoSansCJKkr-Medium;
+	src: url('/resources/fonts/NotoSansCJKkr-Medium.otf');
+}
+
+@font-face {
+	font-family: NotoSansCJKkr-Regular;
+	src: url('/resources/fonts/NotoSansCJKkr-Regular.otf');
+}
 
 body {
-	padding-top: 30px;
-	
+	background-color: #e6e1da
 }
 
 @media screen and (min-width : 640px) { /* Styles */
-	.myclass {
+	.head {
 		width: 500px;
 		margin: 0 auto;
 	}
+	.content {
+		width: 500px;
+		margin: 0 auto;
+	}
+}
+
+.content_option {
+	padding-left: 30px;
+	padding-right: 30px;
+}
+
+.title_option {
+	font-family: NotoSansCJKkr-Medium;
+	color: #000000;
+	font-size: 16px;
+}
+
+.input_option {
+	font-family: NotoSansCJKkr-Regular;
+	color: #9e8d76;
+	font-size: 20px;
+	display: block;
+	width: 100%;
+	height: calc(1.5em + .75rem + 2px);
+	padding: .375rem .75rem;
+	font-weight: 400;
+	line-height: 1.5;
+	background-color: #fff;
+	background-clip: padding-box;
+	border: 1px solid #ced4da;
+	border-radius: .25rem;
+	transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
+}
+
+input[type=text] {
+	border-radius: 3rem;
+	margin-bottom: 10px;
+	height: 50px;
+}
+
+input::placeholder {
+	color: #9e8d76;
+	font-size: 20px;
+	font-family: NotoSansCJKkr-Regular;
+}
+
+.button_option {
+	height: 70px;
+	border-radius: 3rem;
+	margin-bottom: 10px;
+	background-color: #9e8d76;
+	color: #ffffff;
+	font-size: 22px;
+}
+
+select {
+	background: white;
+}
+
+.select_option {
+	border-radius: 3rem;
+	margin-bottom: 10px;
+	height: 50px;
+	background: white url('/resources/img/arrow.png') no-repeat 95% 50%;
+	background-size: 10px;
+	-webkit-appearance: none;
+}
+
+img {
+	width: 100%;
+}
+
+.test {
+	align: middle;
+}
+
+p {
+	font-size: 12px;
+	text-align: left;
+	color: #705b3d;
 }
 </style>
 </head>
 
 <body>
-	<form method="post" action="user/info/submit" class = "myclass">
-	
-		<label for="complex">´ÜÁö</label> 
-		<select class="form-control" id="complex" name="complex" required>
-			<option value="">´ÜÁö¸í</option>
-			<c:forEach items="${complexInfos}" var="row">
-				<option value="${row.complexIp}">${row.complexName}</option>
-			</c:forEach>
-		</select> 
-		<br>
-		<label for="dong">µ¿</label> 
-		<input type="text" class="form-control" id="dong" name="dong" value="802" required>
-		<br> 
-		<label for="ho">È£</label> 
-		<input type="text" class="form-control" id="ho" name="ho" value="1103" required>
-		<br> 
+	<div class="head">
+		<img src="/resources/img/logo.png" />
+	</div>
+	<div class="content content_option">
+		<form method="get" action="user/info/submit">
+
+			<label for="complex" class="title_option">• 단지정보</label> <select
+				class="select_option input_option" id="complex" name="complex"
+				required>
+				<option value=""> 단지명</option>
+				<c:forEach items="${complexInfos}" var="row">
+					<option value="${row.complexIp}" class="select_option input_option">${row.complexName}</option>
+				</c:forEach>
+			</select> <label for="dong" class="title_option">• 세대정보</label> <input
+				type="text" class="input_option" id="dong" name="dong"
+				placeholder=" �" required> <input type="text"
+				class="input_option" id="ho" name="ho" placeholder=" 호" required>
+			<button type="submit" class="button_option btn btn-block">��번호
+				전송하기</button>
+		</form>
 		<br>
-		<button type="submit" class="btn btn-primary btn-block">Submit</button>
-	</form>
+		<div align="center">
+			<p>
+				[��방법] <br> 1. 월패드�서 "설정">"모바�기기등�" 화면� 진입한 후, 중앙하단� "+등�" 버튼�
+				누르면 �업창� 뜹니다. <br> 2. �업창� �운 �태�서 세대정보를 입력하고 "��번호 전송하기"를 누르면
+				해당 세대 월패드� ��번호가 전송�니다. <br> 3. 월패드� 출력� ��번호를 입력하고 "��하기" 버튼�
+				누르면 ��� 완료�니다.
+			</p>
+		</div>
+	</div>
 
 </body>
 </html>