JWT 작업중

dev_tymeleaf
kkw29 2 years ago
parent 4c89e1d524
commit a588ca3f59

43
.gitignore vendored

@ -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/

@ -129,6 +129,14 @@ dependencies {
implementation files("lib/ASRLIB-2.4.0.2.jar") implementation files("lib/ASRLIB-2.4.0.2.jar")
implementation files("lib/pttsnet_class.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') { tasks.named('test') {

@ -29,6 +29,9 @@ spring:
dialect: com.icomsys.main_vm.common.util.CustomDialect dialect: com.icomsys.main_vm.common.util.CustomDialect
generate_statistics: true generate_statistics: true
jwt:
secret: f2d7e5002d67c8d118ebf800274b6a5c83ed7b3d3518b2cddcd7226f7484eb34
#https://docs.jboss.org/hibernate/orm/3.5/javadocs/org/hibernate/dialect/package-summary.html #https://docs.jboss.org/hibernate/orm/3.5/javadocs/org/hibernate/dialect/package-summary.html
feign: feign:

@ -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;
}

@ -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<? extends GrantedAuthority> 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();
}
}
}

@ -59,7 +59,7 @@ public class LoginController {
@PostMapping("/adm/main/check") @PostMapping("/adm/main/check")
@ResponseBody @ResponseBody
public ResponseEntity loginCheck(@RequestBody LoginCheckReq dto) throws CustomNotFoundException, CustomBadRequestException { public String loginCheck(@RequestBody LoginCheckReq dto) throws CustomNotFoundException, CustomBadRequestException {
return loginService.loginCheck(dto); return loginService.loginCheck(dto);
} }

@ -1,10 +1,15 @@
package com.icomsys.main_vm.biz.common.login.service; 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.google.gson.Gson;
import com.icomsys.main_vm.biz.advice.excep.CustomBadRequestException; import com.icomsys.main_vm.biz.advice.excep.CustomBadRequestException;
import com.icomsys.main_vm.biz.advice.excep.CustomNotFoundException; 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.LogService;
import com.icomsys.main_vm.biz.common.common.service.LogVO; 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.LoginReq;
import com.icomsys.main_vm.biz.common.login.req.MainOprReq; import com.icomsys.main_vm.biz.common.login.req.MainOprReq;
import com.icomsys.main_vm.biz.common.login.req.PwdUpdateReq; 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.entity.system.TbBotUser;
import com.icomsys.main_vm.db.jpa.repo.system.*; import com.icomsys.main_vm.db.jpa.repo.system.*;
import com.icomsys.main_vm.db.mybatis.alias.LoginVO; import com.icomsys.main_vm.db.mybatis.alias.LoginVO;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
@ -37,6 +43,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
@ -70,6 +77,8 @@ public class LoginService {
private final ClientIp clientIp; private final ClientIp clientIp;
private final TokenProvider tokenProvider;
//LoginVo의 경우 Egov 레거시매퍼의존성이 잡혀있어. req에 분리 불가능 //LoginVo의 경우 Egov 레거시매퍼의존성이 잡혀있어. req에 분리 불가능
@Transactional @Transactional
public String LoginValidService(LoginReq dto, ModelMap model) { public String LoginValidService(LoginReq dto, ModelMap model) {
@ -125,7 +134,10 @@ public class LoginService {
List<MenuVo> menuVos = new ArrayList<>(); List<MenuVo> menuVos = new ArrayList<>();
String url = ""; String url = "";
// LoginVO loginVO = (LoginVO) httpServletRequest.getSession().getAttribute(SessionResource.LoginVO.getName()); // 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)); log.info("action main session uservo- {}", new Gson().toJson(userVo));
// if (loginVO != null && loginVO.getUserId() != null && !loginVO.getUserId().equals("")) { // if (loginVO != null && loginVO.getUserId() != null && !loginVO.getUserId().equals("")) {
if (userVo != null && userVo.getUserId() != null && !userVo.getUserId().equals("")) { if (userVo != null && userVo.getUserId() != null && !userVo.getUserId().equals("")) {
@ -160,7 +172,48 @@ public class LoginService {
} }
public UserVo getUserVo() { 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<String, Object> 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("")) { // if (user == null || user.equals("")) {
// httpServletResponse.setStatus(401); // httpServletResponse.setStatus(401);
// } // }
@ -354,8 +407,13 @@ public class LoginService {
} }
@Transactional @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()); TbBotUser user = tbBotUserRepo.findByUserId(dto.getUserId()).orElseThrow(() -> new CustomNotFoundException());
if (user.getLoginCheck() >= 5) { if (user.getLoginCheck() >= 5) {
throw new CustomBadRequestException(); throw new CustomBadRequestException();
} }
@ -363,7 +421,7 @@ public class LoginService {
throw new CustomBadRequestException(); throw new CustomBadRequestException();
} }
if (LocalDateTime.now().isAfter(user.getUpdateDate().plusDays(90))) { if (LocalDateTime.now().isAfter(user.getUpdateDate().plusDays(90))) {
return ResponseEntity.status(401).build(); ResponseEntity.status(401).build();
} }
if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) { if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
user.loginFailCheck(); user.loginFailCheck();
@ -386,8 +444,22 @@ public class LoginService {
); );
user.resetLoginFailCheck(); 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();
} }

@ -29,6 +29,9 @@ spring:
dialect: com.icomsys.main_vm.common.util.CustomDialect dialect: com.icomsys.main_vm.common.util.CustomDialect
generate_statistics: true generate_statistics: true
jwt:
secret: f2d7e5002d67c8d118ebf800274b6a5c83ed7b3d3518b2cddcd7226f7484eb34
#https://docs.jboss.org/hibernate/orm/3.5/javadocs/org/hibernate/dialect/package-summary.html #https://docs.jboss.org/hibernate/orm/3.5/javadocs/org/hibernate/dialect/package-summary.html
feign: feign:

@ -24,6 +24,7 @@ var StatisticsDashboard = {
datatype: "JSON", datatype: "JSON",
contentType : "application/json; charset=utf-8", contentType : "application/json; charset=utf-8",
url: "/adm/menu/oprmng", url: "/adm/menu/oprmng",
headers: {'Authorization': getToken()},
data: {menuval: 'root'}, data: {menuval: 'root'},
// data: {serviceType: 'S'}, // data: {serviceType: 'S'},
success: (function(data) { success: (function(data) {
@ -70,6 +71,7 @@ var StatisticsDashboard = {
datatype: "JSON", datatype: "JSON",
contentType : "application/json; charset=utf-8", contentType : "application/json; charset=utf-8",
url: "/admin/common/dashboard/callinfolist.do", url: "/admin/common/dashboard/callinfolist.do",
headers: {'Authorization': getToken()},
data: JSON.stringify(param), data: JSON.stringify(param),
success: (function(data) { success: (function(data) {
StatisticsDashboard.totCallList = []; StatisticsDashboard.totCallList = [];
@ -107,6 +109,7 @@ var StatisticsDashboard = {
datatype: "JSON", datatype: "JSON",
contentType : "application/json; charset=utf-8", contentType : "application/json; charset=utf-8",
url: "/admin/common/dashboard/intentlist.do", url: "/admin/common/dashboard/intentlist.do",
headers: {'Authorization': getToken()},
data: JSON.stringify(param), data: JSON.stringify(param),
success: (function(data) { success: (function(data) {
StatisticsDashboard.totIntentList = []; StatisticsDashboard.totIntentList = [];
@ -144,6 +147,7 @@ var StatisticsDashboard = {
datatype: "JSON", datatype: "JSON",
contentType : "application/json; charset=utf-8", contentType : "application/json; charset=utf-8",
url: "/admin/common/dashboard/scenariolist.do", url: "/admin/common/dashboard/scenariolist.do",
headers: {'Authorization': getToken()},
data: JSON.stringify(param), data: JSON.stringify(param),
success: (function(data) { success: (function(data) {
StatisticsDashboard.useScenarioList = []; StatisticsDashboard.useScenarioList = [];
@ -194,6 +198,7 @@ var StatisticsDashboard = {
datatype: "JSON", datatype: "JSON",
contentType : "application/json; charset=utf-8", contentType : "application/json; charset=utf-8",
url: "/admin/common/dashboard/callcntlist.do", url: "/admin/common/dashboard/callcntlist.do",
headers: {'Authorization': getToken()},
data: JSON.stringify(param), data: JSON.stringify(param),
success: (function(data) { success: (function(data) {
StatisticsDashboard.todayCallCntList = []; StatisticsDashboard.todayCallCntList = [];
@ -222,6 +227,7 @@ var StatisticsDashboard = {
datatype: "JSON", datatype: "JSON",
contentType : "application/json; charset=utf-8", contentType : "application/json; charset=utf-8",
url: "/admin/common/dashboard/curcallcnt.do", url: "/admin/common/dashboard/curcallcnt.do",
headers: {'Authorization': getToken()},
data: JSON.stringify(param), data: JSON.stringify(param),
success: (function(data) { success: (function(data) {
if (data != null && data != '') { if (data != null && data != '') {

@ -43,7 +43,64 @@
<script th:src="@{/js/utils.js}"></script> <script th:src="@{/js/utils.js}"></script>
<script type="text/javascript"> <script type="text/javascript">
function getToken() { function getToken() {
document.cookie = "accessToken="; var cookies = document.cookie.split(';');
var token = '';
cookies.forEach(function(cookie) {
if(cookie.split('=')[0] == 'accessToken') {
token = cookie.split('=')[1];
}
});
//시간체크
var tmpVal = parseJwt(token);
var now = Date.now();
if (tmpVal.exp < parseInt(now.toString().substring(0, 10))) {
//시간 지났으면 checkToken -> 서버사이드 토큰 갱신
checkToken();
cookies = document.cookie.split(';');
cookies.forEach(function(cookie) {
if(cookie.split('=')[0] == 'accessToken') {
token = cookie.split('=')[1];
}
});
}
return token;
}
function checkToken() {
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
datatype: "JSON",
url: "<c:url value='토큰 갱신 url'/>",
data: JSON.stringify(data)
}).complete(function (data) {
if (data.status == 200) {
//refreshToken 유효 시
document.cookie = 'accessToken=' + data.responseText + '; path=/;';
}
if (data.status == 400) {
// 로그인 페이지로 이동
window.location.href = "<c:url value='/adm/signin/signin'/>";
}
});
}
//function accessToken 갱신요청
function parseJwt(token) {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
} }
</script> </script>
</head> </head>

@ -140,8 +140,7 @@
}) })
.complete(function (data) { .complete(function (data) {
if (data.status == 200) { if (data.status == 200) {
document.cookie = 'accessToken=' + data.responseText + '; path=/;';
//set cookie
document.signinInfoForm.action = '/adm/main/actionSecurityLogin.do'; document.signinInfoForm.action = '/adm/main/actionSecurityLogin.do';
document.signinInfoForm.submit(); document.signinInfoForm.submit();

Loading…
Cancel
Save