diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7814793 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +HELP.md +.gradle +.idea +build/ +**/.gradle +**/.idea +**/build + +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath + +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/main_vm/build.gradle b/main_vm/build.gradle index 91dea0f..bed2430 100644 --- a/main_vm/build.gradle +++ b/main_vm/build.gradle @@ -129,6 +129,14 @@ dependencies { implementation files("lib/ASRLIB-2.4.0.2.jar") implementation files("lib/pttsnet_class.jar") + + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + //자바 역직렬화 문제 해결 패키지 + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' } tasks.named('test') { diff --git a/main_vm/build/classes/java/main/com/icomsys/main_vm/biz/common/login/controller/LoginController.class b/main_vm/build/classes/java/main/com/icomsys/main_vm/biz/common/login/controller/LoginController.class index 3cd4881..016a213 100644 Binary files a/main_vm/build/classes/java/main/com/icomsys/main_vm/biz/common/login/controller/LoginController.class and b/main_vm/build/classes/java/main/com/icomsys/main_vm/biz/common/login/controller/LoginController.class differ diff --git a/main_vm/build/classes/java/main/com/icomsys/main_vm/biz/common/login/service/LoginService.class b/main_vm/build/classes/java/main/com/icomsys/main_vm/biz/common/login/service/LoginService.class index 9820417..a0f7e00 100644 Binary files a/main_vm/build/classes/java/main/com/icomsys/main_vm/biz/common/login/service/LoginService.class and b/main_vm/build/classes/java/main/com/icomsys/main_vm/biz/common/login/service/LoginService.class differ diff --git a/main_vm/build/resources/main/application-local.yml b/main_vm/build/resources/main/application-local.yml index 5c7a9a3..2dc72ce 100644 --- a/main_vm/build/resources/main/application-local.yml +++ b/main_vm/build/resources/main/application-local.yml @@ -29,6 +29,9 @@ spring: dialect: com.icomsys.main_vm.common.util.CustomDialect generate_statistics: true + jwt: + secret: f2d7e5002d67c8d118ebf800274b6a5c83ed7b3d3518b2cddcd7226f7484eb34 + #https://docs.jboss.org/hibernate/orm/3.5/javadocs/org/hibernate/dialect/package-summary.html feign: @@ -56,4 +59,4 @@ Globals: # stack: # auto: false # region: -# static: ap-northeast-2 \ No newline at end of file +# static: ap-northeast-2 diff --git a/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/CinnamonToken.java b/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/CinnamonToken.java new file mode 100644 index 0000000..635a17f --- /dev/null +++ b/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/CinnamonToken.java @@ -0,0 +1,15 @@ +package com.icomsys.main_vm.biz.common.login; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +@AllArgsConstructor +public class CinnamonToken { + + private String grantType; + private String accessToken; + private String refreshToken; +} diff --git a/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/TokenProvider.java b/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/TokenProvider.java new file mode 100644 index 0000000..71b0f41 --- /dev/null +++ b/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/TokenProvider.java @@ -0,0 +1,153 @@ +package com.icomsys.main_vm.biz.common.login; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.icomsys.main_vm.db.jpa.entity.system.TbBotUser; +import com.icomsys.main_vm.db.jpa.repo.system.TbUserAuthGroupRepo; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletResponse; +import java.security.Key; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.stream.Collectors; + + +@Slf4j +@Component +public class TokenProvider { + private final TbUserAuthGroupRepo tbUserAuthGroupRepo; + + private static final long ACCESS_TOKEN_EXPIRE_TIME = 20 * 60 * 1000L; + private static final long REFRESH_TOKEN_EXPIRE_TIME = 24 * 60 * 60 * 1000L; + + private final Key key; + + public TokenProvider(@Value("${spring.jwt.secret}") String secretKey, TbUserAuthGroupRepo tbUserAuthGroupRepo) { + this.tbUserAuthGroupRepo = tbUserAuthGroupRepo; + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + public String generateToken(Authentication authentication, TbBotUser user) throws JsonProcessingException { + // 권한 가져오기 + String authorities = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + long now = (new Date()).getTime(); + // Access Token 생성 + // Todo: Access Token 필요정보 추가 개발 필요 + Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME); // Todo: Access Token 만료 기간 설정 파일 추가 + + Claims claims = Jwts.claims() + .setSubject(authentication.getName()) + .setExpiration(accessTokenExpiresIn); + + claims.put("auth", authorities); + + ObjectMapper mapper = new ObjectMapper(); + + // claims.put("UserVO", mapper.registerModule(new JavaTimeModule()).writeValueAsString(user.toUserVO())); + try { + JavaTimeModule javaTimeModule = new JavaTimeModule(); + + LocalDateTimeSerializer localDateTimeSerializer = new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + + javaTimeModule.addSerializer(LocalDateTime.class, localDateTimeSerializer); + + // claims.put("UserVO", mapper.registerModule(javaTimeModule).readValue(user.toUserVO(), UserVo.class)); + claims.put("UserVO", mapper.registerModule(javaTimeModule).writeValueAsString(user.toUserVO())); + // claims.put("PolicyList", tbUserAuthGroupRepo.userPolicyListSelect(user.getUserSeq(), user.getLastUseServiceGroup())); + } + catch (Exception e) { + e.printStackTrace(); + } + + String accessToken = Jwts.builder() + .setSubject(authentication.getName()) + .setClaims(claims) + .setExpiration(accessTokenExpiresIn) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + // Refresh Token 생성 + String refreshToken = Jwts.builder() + .setExpiration(new Date(now + REFRESH_TOKEN_EXPIRE_TIME)) // Todo: Refresh Token 만료 기간 설정 파일에 추가 + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + CinnamonToken token = CinnamonToken.builder() + .grantType("Bearer") + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + + return accessToken; + } + + // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 메서드 + public Authentication getAuthentication(String accessToken) { + // 토큰 복호화 + Claims claims = parseClaims(accessToken); + + // Todo: 권한 관련 정보 처리 + if (claims.get("auth") == null) { + throw new RuntimeException("권한 정보가 없는 토큰입니다."); + } + + // Todo: 사용자 정보와 권한 관련 정보를 읽어와 리턴 + + // 클레임에서 권한 정보 가져오기 + Collection authorities = + Arrays.stream(claims.get("auth").toString().split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + // UserDetails 객체를 만들어서 Authentication 리턴 + // UserDetails principal = new User(claims.getSubject(), "", authorities); + return new UsernamePasswordAuthenticationToken(/*principal*/ "", "", authorities); + } + + // 토큰 정보를 검증하는 메서드 + public boolean validateToken(String token) { + // Todo: 내부 토큰 사용 정책에 따라 추후 수정 필요!! + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { + log.info("Invalid JWT Token", e); + } catch (ExpiredJwtException e) { + log.info("Expired JWT Token", e); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT Token", e); + } catch (IllegalArgumentException e) { + log.info("JWT claims string is empty.", e); + } + return false; + } + + public Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } + +} diff --git a/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/controller/LoginController.java b/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/controller/LoginController.java index 729da2c..c499c5b 100644 --- a/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/controller/LoginController.java +++ b/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/controller/LoginController.java @@ -59,7 +59,7 @@ public class LoginController { @PostMapping("/adm/main/check") @ResponseBody - public ResponseEntity loginCheck(@RequestBody LoginCheckReq dto) throws CustomNotFoundException, CustomBadRequestException { + public String loginCheck(@RequestBody LoginCheckReq dto) throws CustomNotFoundException, CustomBadRequestException { return loginService.loginCheck(dto); } diff --git a/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/service/LoginService.java b/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/service/LoginService.java index c4a6afc..c162812 100644 --- a/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/service/LoginService.java +++ b/main_vm/src/main/java/com/icomsys/main_vm/biz/common/login/service/LoginService.java @@ -1,10 +1,15 @@ package com.icomsys.main_vm.biz.common.login.service; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.google.gson.Gson; import com.icomsys.main_vm.biz.advice.excep.CustomBadRequestException; import com.icomsys.main_vm.biz.advice.excep.CustomNotFoundException; import com.icomsys.main_vm.biz.common.common.service.LogService; import com.icomsys.main_vm.biz.common.common.service.LogVO; +import com.icomsys.main_vm.biz.common.login.CinnamonToken; +import com.icomsys.main_vm.biz.common.login.TokenProvider; import com.icomsys.main_vm.biz.common.login.req.LoginReq; import com.icomsys.main_vm.biz.common.login.req.MainOprReq; import com.icomsys.main_vm.biz.common.login.req.PwdUpdateReq; @@ -20,6 +25,7 @@ import com.icomsys.main_vm.db.jpa.entity.conversation.TbIcsLog; import com.icomsys.main_vm.db.jpa.entity.system.TbBotUser; import com.icomsys.main_vm.db.jpa.repo.system.*; import com.icomsys.main_vm.db.mybatis.alias.LoginVO; +import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.MessageSource; @@ -37,6 +43,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.ui.ModelMap; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -70,6 +77,8 @@ public class LoginService { private final ClientIp clientIp; + private final TokenProvider tokenProvider; + //LoginVo의 경우 Egov 레거시매퍼의존성이 잡혀있어. req에 분리 불가능 @Transactional public String LoginValidService(LoginReq dto, ModelMap model) { @@ -125,7 +134,10 @@ public class LoginService { List menuVos = new ArrayList<>(); String url = ""; // LoginVO loginVO = (LoginVO) httpServletRequest.getSession().getAttribute(SessionResource.LoginVO.getName()); - UserVo userVo = (UserVo) httpServletRequest.getSession().getAttribute(SessionResource.UserVO.getName()); + +// UserVo userVo = (UserVo) httpServletRequest.getSession().getAttribute(SessionResource.UserVO.getName()); + UserVo userVo = getUserVo(); + log.info("action main session uservo- {}", new Gson().toJson(userVo)); // if (loginVO != null && loginVO.getUserId() != null && !loginVO.getUserId().equals("")) { if (userVo != null && userVo.getUserId() != null && !userVo.getUserId().equals("")) { @@ -160,7 +172,48 @@ public class LoginService { } public UserVo getUserVo() { - UserVo user = (UserVo) httpServletRequest.getSession().getAttribute(SessionResource.UserVO.getName()); + String payloadJWT = ""; + String accessToken = ""; + + // String bearerToken = httpServletRequest.getHeader("Authorization"); + // + // if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) { + // payload = bearerToken.substring(7); + // } + + // todo request header에서 token 값 가져오기 + Cookie[] cookies = httpServletRequest.getCookies(); + + if(cookies!=null){ + for (Cookie c : cookies) { + String name = c.getName(); // 쿠키 이름 가져오기 + String value = c.getValue(); // 쿠키 값 가져오기 + if (name.equals("accessToken")) { + payloadJWT = value.split("[.]")[1]; + accessToken = value; + } + } + } + + Claims claims = tokenProvider.parseClaims(accessToken); + + // Base64.Decoder decoder = Base64.getUrlDecoder(); + // final String payload = new String(decoder.decode(payloadJWT)); + // JsonParser jsonParser = new BasicJsonParser(); + // Map jsonArray = jsonParser.parseMap(payload); + + Gson gson =new Gson(); + Map map =new HashMap(); + // map = gson.fromJson((String) jsonArray.get("UserVO"), map.getClass()); + map = gson.fromJson((String) claims.get("UserVO"), map.getClass()); + + map.put("registDate", map.get("registDate").toString().replace(" ", "T")); + map.put("updateDate", map.get("updateDate").toString().replace(" ", "T")); + + ObjectMapper objectMapper = new ObjectMapper(); + UserVo user = objectMapper.registerModule(new JavaTimeModule()).convertValue(map, UserVo.class); + +// UserVo user = (UserVo) httpServletRequest.getSession().getAttribute(SessionResource.UserVO.getName()); // if (user == null || user.equals("")) { // httpServletResponse.setStatus(401); // } @@ -354,8 +407,13 @@ public class LoginService { } @Transactional - public ResponseEntity loginCheck(LoginCheckReq dto) throws CustomNotFoundException, CustomBadRequestException { + public String loginCheck(LoginCheckReq dto) throws CustomNotFoundException, CustomBadRequestException { + // 1. Login ID/PW 를 기반으로 Authentication 객체 생성 + // 이때 authentication 는 인증 여부를 확인하는 authenticated 값이 false + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(dto.getUserId(), dto.getPassword()); + TbBotUser user = tbBotUserRepo.findByUserId(dto.getUserId()).orElseThrow(() -> new CustomNotFoundException()); + if (user.getLoginCheck() >= 5) { throw new CustomBadRequestException(); } @@ -363,7 +421,7 @@ public class LoginService { throw new CustomBadRequestException(); } if (LocalDateTime.now().isAfter(user.getUpdateDate().plusDays(90))) { - return ResponseEntity.status(401).build(); + ResponseEntity.status(401).build(); } if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) { user.loginFailCheck(); @@ -386,8 +444,22 @@ public class LoginService { ); user.resetLoginFailCheck(); + + ResponseEntity.ok().build(); + + // 2. 실제 검증 (사용자 비밀번호 체크)이 이루어지는 부분 + // authenticate 매서드가 실행될 때 CustomUserDetailsService 에서 만든 loadUserByUsername 메서드가 실행 + try { + Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); + + // 3. 인증 정보를 기반으로 JWT 토큰 생성 + String accessToken = tokenProvider.generateToken(authentication, user); + + return accessToken; + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } - return ResponseEntity.ok().build(); } diff --git a/main_vm/src/main/resources/application-local.yml b/main_vm/src/main/resources/application-local.yml index 5c7a9a3..2dc72ce 100644 --- a/main_vm/src/main/resources/application-local.yml +++ b/main_vm/src/main/resources/application-local.yml @@ -29,6 +29,9 @@ spring: dialect: com.icomsys.main_vm.common.util.CustomDialect generate_statistics: true + jwt: + secret: f2d7e5002d67c8d118ebf800274b6a5c83ed7b3d3518b2cddcd7226f7484eb34 + #https://docs.jboss.org/hibernate/orm/3.5/javadocs/org/hibernate/dialect/package-summary.html feign: @@ -56,4 +59,4 @@ Globals: # stack: # auto: false # region: -# static: ap-northeast-2 \ No newline at end of file +# static: ap-northeast-2 diff --git a/main_vm/src/main/resources/static/aajs/statisticsDashboard.js b/main_vm/src/main/resources/static/aajs/statisticsDashboard.js index cf38077..2789f30 100644 --- a/main_vm/src/main/resources/static/aajs/statisticsDashboard.js +++ b/main_vm/src/main/resources/static/aajs/statisticsDashboard.js @@ -24,6 +24,7 @@ var StatisticsDashboard = { datatype: "JSON", contentType : "application/json; charset=utf-8", url: "/adm/menu/oprmng", + headers: {'Authorization': getToken()}, data: {menuval: 'root'}, // data: {serviceType: 'S'}, success: (function(data) { @@ -70,6 +71,7 @@ var StatisticsDashboard = { datatype: "JSON", contentType : "application/json; charset=utf-8", url: "/admin/common/dashboard/callinfolist.do", + headers: {'Authorization': getToken()}, data: JSON.stringify(param), success: (function(data) { StatisticsDashboard.totCallList = []; @@ -107,6 +109,7 @@ var StatisticsDashboard = { datatype: "JSON", contentType : "application/json; charset=utf-8", url: "/admin/common/dashboard/intentlist.do", + headers: {'Authorization': getToken()}, data: JSON.stringify(param), success: (function(data) { StatisticsDashboard.totIntentList = []; @@ -144,6 +147,7 @@ var StatisticsDashboard = { datatype: "JSON", contentType : "application/json; charset=utf-8", url: "/admin/common/dashboard/scenariolist.do", + headers: {'Authorization': getToken()}, data: JSON.stringify(param), success: (function(data) { StatisticsDashboard.useScenarioList = []; @@ -194,6 +198,7 @@ var StatisticsDashboard = { datatype: "JSON", contentType : "application/json; charset=utf-8", url: "/admin/common/dashboard/callcntlist.do", + headers: {'Authorization': getToken()}, data: JSON.stringify(param), success: (function(data) { StatisticsDashboard.todayCallCntList = []; @@ -222,6 +227,7 @@ var StatisticsDashboard = { datatype: "JSON", contentType : "application/json; charset=utf-8", url: "/admin/common/dashboard/curcallcnt.do", + headers: {'Authorization': getToken()}, data: JSON.stringify(param), success: (function(data) { if (data != null && data != '') { diff --git a/main_vm/src/main/resources/templates/layout/fragments/head.html b/main_vm/src/main/resources/templates/layout/fragments/head.html index 681dd82..68c418a 100644 --- a/main_vm/src/main/resources/templates/layout/fragments/head.html +++ b/main_vm/src/main/resources/templates/layout/fragments/head.html @@ -42,8 +42,65 @@ @@ -109,4 +166,4 @@ - \ No newline at end of file + diff --git a/main_vm/src/main/resources/templates/layout/signin.html b/main_vm/src/main/resources/templates/layout/signin.html index e637a59..d2f95b0 100644 --- a/main_vm/src/main/resources/templates/layout/signin.html +++ b/main_vm/src/main/resources/templates/layout/signin.html @@ -140,8 +140,7 @@ }) .complete(function (data) { if (data.status == 200) { - - //set cookie + document.cookie = 'accessToken=' + data.responseText + '; path=/;'; document.signinInfoForm.action = '/adm/main/actionSecurityLogin.do'; document.signinInfoForm.submit(); @@ -192,4 +191,4 @@ - \ No newline at end of file +