From 94d9f7e90bc337005669d994a14ace88e92bc491 Mon Sep 17 00:00:00 2001 From: kkw29 Date: Wed, 7 Jun 2023 13:15:21 +0900 Subject: [PATCH] =?UTF-8?q?JWT=20=EC=9E=91=EC=97=85=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main_vm/build.gradle | 3 + .../biz/common/login/TokenProvider.java | 76 ++++++---- .../login/controller/LoginController.java | 21 ++- .../common/login/service/LoginService.java | 130 ++++++++++++++++-- .../db/jpa/entity/system/TbBotUser.java | 5 + .../WEB-INF/jsp/adm/common/dashboard.jsp | 2 +- .../webapp/WEB-INF/jsp/adm/include/header.jsp | 70 +++++++++- .../jsp/adm/rcp/monitoring/consulting.jsp | 2 +- .../webapp/WEB-INF/jsp/adm/signin/signin.jsp | 17 ++- main_vm/src/main/webapp/aajs/consulting.js | 70 +++++----- .../main/webapp/aajs/statisticsDashboard.js | 1 - 11 files changed, 300 insertions(+), 97 deletions(-) diff --git a/main_vm/build.gradle b/main_vm/build.gradle index 4d5e649..75bf828 100644 --- a/main_vm/build.gradle +++ b/main_vm/build.gradle @@ -124,6 +124,9 @@ dependencies { 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/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 index 0bc3ac9..50c2b6f 100644 --- 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 @@ -1,9 +1,16 @@ 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; @@ -11,9 +18,10 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Component; -import javax.servlet.http.Cookie; 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; @@ -23,17 +31,20 @@ import java.util.stream.Collectors; @Slf4j @Component public class TokenProvider { - private final HttpServletResponse httpServletResponse; + 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, HttpServletResponse httpServletResponse) { - this.httpServletResponse = httpServletResponse; + 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 CinnamonToken generateToken(Authentication authentication) { + public CinnamonToken generateToken(Authentication authentication, TbBotUser user) throws JsonProcessingException { // 권한 가져오기 String authorities = authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) @@ -42,39 +53,50 @@ public class TokenProvider { long now = (new Date()).getTime(); // Access Token 생성 // Todo: Access Token 필요정보 추가 개발 필요 - Date accessTokenExpiresIn = new Date(now + 86400000); // 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()) - .claim("auth", authorities) + .setClaims(claims) .setExpiration(accessTokenExpiresIn) .signWith(key, SignatureAlgorithm.HS256) .compact(); // Refresh Token 생성 String refreshToken = Jwts.builder() - .setExpiration(new Date(now + 86400000)) // Todo: Refresh Token 만료 기간 설정 파일에 추가 + .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(); - - // create a cookie - Cookie cookie = new Cookie("JWT", token.toString()); - - // expires in 7 days - cookie.setMaxAge(7 * 24 * 60 * 60); - - // optional properties - cookie.setSecure(true); - cookie.setHttpOnly(true); - cookie.setPath("/"); - - // add cookie to response - httpServletResponse.addCookie(cookie); + .grantType("Bearer") + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); return token; } @@ -120,7 +142,7 @@ public class TokenProvider { return false; } - private Claims parseClaims(String accessToken) { + public Claims parseClaims(String accessToken) { try { return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody(); } catch (ExpiredJwtException e) { 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 ae8233d..d016452 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 @@ -26,6 +26,7 @@ import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.io.IOException; import java.util.*; @@ -38,6 +39,9 @@ public class LoginController { * 메인 컨트롤러 */ private final LoginService loginService; + + private final HttpServletResponse httpServletResponse; + private final FileService fileService; private final HttpServletRequest httpServletRequest; @@ -71,10 +75,15 @@ public class LoginController { @PostMapping("/adm/tokenLogin") @ResponseBody - public CinnamonToken tokenLogin(@RequestBody LoginCheckReq dto) { + public String tokenLogin(@RequestBody LoginCheckReq dto, HttpServletResponse httpServletResponse) { String id = dto.getUserId(); String pwd = dto.getPassword(); - return loginService.tokenLogin(dto); + + CinnamonToken cinnamonToken = loginService.tokenLogin(dto); + + httpServletResponse.setHeader("Authorization", cinnamonToken.getRefreshToken()); + + return cinnamonToken.getAccessToken(); } @@ -158,10 +167,10 @@ public class LoginController { public ResponseEntity getMainOprmngCode() throws CustomNoSuchFieldException, NoSuchFieldException { //List - if (httpServletRequest.getSession() == null) { - log.info("SESSION NULL"); - return ResponseEntity.badRequest().build(); - } +// if (httpServletRequest.getSession() == null) { +// log.info("SESSION NULL"); +// return ResponseEntity.badRequest().build(); +// } if (loginService.getUserVo() == null) { log.info("getMainOprmngCode NULL"); return ResponseEntity.badRequest().build(); 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 65af366..bb5477b 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,6 +1,11 @@ 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.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.google.gson.Gson; +import com.google.gson.JsonObject; 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; @@ -22,8 +27,11 @@ 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.boot.json.BasicJsonParser; +import org.springframework.boot.json.JsonParser; import org.springframework.context.MessageSource; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.http.ResponseEntity; @@ -39,11 +47,18 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.ui.ModelMap; +import org.springframework.util.StringUtils; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Reader; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -98,7 +113,7 @@ public class LoginService { SecurityContext securityContext = SecurityContextHolder.getContext(); securityContext.setAuthentication(authentication); - sessionSetting(user); +// sessionSetting(user); LoginVO userResult = user.toLoginVO(); return "forward:/adm/main/actionMain.do"; @@ -115,30 +130,66 @@ public class LoginService { session.setAttribute(SessionResource.UserVO.getName(), tbu.toUserVO()); session.setMaxInactiveInterval(60 * 60); // session.setMaxInactiveInterval(30); - } +// private void jwtSetting(LoginReq dto, TbBotUser user) { +// // 1. Login ID/PW 를 기반으로 Authentication 객체 생성 +// // 이때 authentication 는 인증 여부를 확인하는 authenticated 값이 false +// UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(dto.getUserId(), dto.getPassword()); +// +// // 2. 실제 검증 (사용자 비밀번호 체크)이 이루어지는 부분 +// // authenticate 매서드가 실행될 때 CustomUserDetailsService 에서 만든 loadUserByUsername 메서드가 실행 +// try { +// Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); +// +// // 3. 인증 정보를 기반으로 JWT 토큰 생성 +// CinnamonToken tokenInfo = tokenProvider.generateToken(authentication, user); +// +// // create a cookie +// Cookie cookie = new Cookie("accessToken", tokenInfo.getAccessToken()); +// +// // expires in 7 days +// cookie.setMaxAge(7 * 24 * 60 * 60); +// +// // optional properties +// cookie.setSecure(true); +// cookie.setHttpOnly(true); +// cookie.setPath("/"); +// +// // add cookie to response +// httpServletResponse.addCookie(cookie); +// } catch ( AuthenticationException e) { +// log.info(e.getMessage()); +// throw e; +// } +// } + @Transactional public CinnamonToken tokenLogin(LoginCheckReq dto){ // 1. Login ID/PW 를 기반으로 Authentication 객체 생성 // 이때 authentication 는 인증 여부를 확인하는 authenticated 값이 false UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(dto.getUserId(), dto.getPassword()); + TbBotUser user = tbBotUserRepo.findByUserIdAndUseYn(dto.getUserId(), "Y") + .orElseThrow(() -> new UsernameNotFoundException("로그인 실패")); + // 2. 실제 검증 (사용자 비밀번호 체크)이 이루어지는 부분 // authenticate 매서드가 실행될 때 CustomUserDetailsService 에서 만든 loadUserByUsername 메서드가 실행 try { Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); // 3. 인증 정보를 기반으로 JWT 토큰 생성 - CinnamonToken tokenInfo = tokenProvider.generateToken(authentication); - return tokenInfo; + CinnamonToken cinnamonToken = tokenProvider.generateToken(authentication, user); + + return cinnamonToken; } catch ( AuthenticationException e) { log.info(e.getMessage()); throw e; + } catch (JsonProcessingException e) { + throw new RuntimeException(e); } } - public String actionMain(ModelMap model) { log.info("action main init "); @@ -147,7 +198,9 @@ 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("")) { @@ -182,7 +235,47 @@ 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); +// } + + 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); // } @@ -191,6 +284,24 @@ public class LoginService { public List getSessionPolicy() { return (ArrayList) httpServletRequest.getSession().getAttribute(SessionResource.PolicyList.getName()); + +// String accessToken = ""; +// +// Cookie[] cookies = httpServletRequest.getCookies(); +// +// if(cookies!=null){ +// for (Cookie c : cookies) { +// String name = c.getName(); // 쿠키 이름 가져오기 +// String value = c.getValue(); // 쿠키 값 가져오기 +// if (name.equals("accessToken")) { +// accessToken = value; +// } +// } +// } +// +// Claims claims = tokenProvider.parseClaims(accessToken); +// +// return (ArrayList) claims.get("PolicyList"); } @Transactional @@ -307,8 +418,7 @@ public class LoginService { public void LastUserServiceGroupUpdate(Long userSeq, String updateService) throws CustomNotFoundException { tbBotUserRepo.findById(userSeq) .orElseThrow(() -> new CustomNotFoundException()) - .updateLastService(updateService) - ; + .updateLastService(updateService); } @Transactional @@ -369,7 +479,6 @@ public class LoginService { } public List getOprmngCodeAdmin(String serviceType) { - List result = tbServiceGrouopRepo.getOprmngCodeAdmin(getUserVo(), serviceType); log.info("result - {}", new Gson().toJson(result)); return result; @@ -412,7 +521,6 @@ public class LoginService { return ResponseEntity.ok().build(); } - @Transactional public ResponseEntity PwdUpdate(PwdUpdateReq dto) throws CustomNotFoundException, CustomBadRequestException { TbBotUser user = tbBotUserRepo.findByUserId(dto.getId()).orElseThrow(() -> new CustomNotFoundException()); diff --git a/main_vm/src/main/java/com/icomsys/main_vm/db/jpa/entity/system/TbBotUser.java b/main_vm/src/main/java/com/icomsys/main_vm/db/jpa/entity/system/TbBotUser.java index b737ab5..95332ae 100644 --- a/main_vm/src/main/java/com/icomsys/main_vm/db/jpa/entity/system/TbBotUser.java +++ b/main_vm/src/main/java/com/icomsys/main_vm/db/jpa/entity/system/TbBotUser.java @@ -1,5 +1,8 @@ package com.icomsys.main_vm.db.jpa.entity.system; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.icomsys.main_vm.biz.common.login.res.UserVo; import com.icomsys.main_vm.biz.common.system.vo.SystemBotUserUpdateReq; import com.icomsys.main_vm.db.mybatis.alias.LoginVO; @@ -49,10 +52,12 @@ public class TbBotUser { private String useYn; @Column(name = "REGIST_ID", nullable = false, length = 20) private String registId; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Column(name = "REGIST_DATE", nullable = false) private LocalDateTime registDate; @Column(name = "UPDATE_ID", nullable = true, length = 20) private String updateId; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @Column(name = "UPDATE_DATE", nullable = true) private LocalDateTime updateDate; diff --git a/main_vm/src/main/webapp/WEB-INF/jsp/adm/common/dashboard.jsp b/main_vm/src/main/webapp/WEB-INF/jsp/adm/common/dashboard.jsp index 355d57b..e0b87d9 100644 --- a/main_vm/src/main/webapp/WEB-INF/jsp/adm/common/dashboard.jsp +++ b/main_vm/src/main/webapp/WEB-INF/jsp/adm/common/dashboard.jsp @@ -25,7 +25,7 @@ - +<%-- --%> diff --git a/main_vm/src/main/webapp/WEB-INF/jsp/adm/include/header.jsp b/main_vm/src/main/webapp/WEB-INF/jsp/adm/include/header.jsp index 5ddc4e4..5c6cc5a 100644 --- a/main_vm/src/main/webapp/WEB-INF/jsp/adm/include/header.jsp +++ b/main_vm/src/main/webapp/WEB-INF/jsp/adm/include/header.jsp @@ -16,12 +16,10 @@ document.location.href = ""; } if (xhr.status == "401") { - document.location.href = ""; } }); - $(document).ready(function () { getMainOpr(); getUv(); @@ -42,7 +40,6 @@ // console.log("로그아웃"); document.location.href = ""; }); - }); function getUv() { @@ -61,7 +58,6 @@ } }) return uv; - } function getMainOpr() { @@ -107,7 +103,6 @@ }); }; - function getContextPath() { return sessionStorage.contextPath; } @@ -119,6 +114,69 @@ return sPolicyList.indexOf(policyName) === -1 ? false : true; } + function getToken() { + 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))) { + window.location.href = ""; + + //시간 지났으면 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: "", + data: JSON.stringify(data) + }).complete(function (data) { + if (data.status == 200) { + //refreshToken 유효 시 + document.cookie = 'accessToken=' + '새로운 accessToken'; + } + if (data.status == 400) { + // 로그인 페이지로 이동 + window.location.href = ""; + } + }); + } + + //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); + } +