웹취약점, 모의해킹 조치

This commit is contained in:
USER
2022-08-24 14:04:30 +09:00
parent d0e0ef7020
commit a2273154d1
92 changed files with 1193 additions and 1246 deletions

View File

@@ -143,9 +143,10 @@ public class LoginController {
if (authNumDto.getOprtrId().contains(authenticationWithoutId.get(i).trim())) {
loginSuccessHandler.process(request, response, authentication);
// access 토큰 생성
jwtSvc.generatePrivateToken(response, authentication);
// Auth 토큰 생성
String authToken = jwtSvc.generatePrivateToken(response, authentication);
// jwtSvc.generatePrivateToken(response, authentication);
String nextUrl = getReturnUrl(request, response);
logger.debug("login SUCCESS - nextUrl = [{}]", nextUrl);
AuthUser authUser = loginService.getUser(authentication.getName());
@@ -157,10 +158,27 @@ public class LoginController {
Menu rootMenu = menu.get(0);
String rootUrl = rootMenu.getChildren().get(0).getMenuUrl();
ArrayList<String> menuUrls = new ArrayList<String>();
for(int j=0; j< menu.size(); j++) {
Menu parentMenu = menu.get(j);
if(parentMenu.getChildren().size() > 0) {
ArrayList<Menu> childrenMenu = (ArrayList<Menu>)parentMenu.getChildren();
for(int k=0; k< childrenMenu.size(); k++) {
Menu childMenu = childrenMenu.get(k);
String menuUrl = childMenu.getMenuUrl();
menuUrls.add(menuUrl);
}
}
//String menuUrl = rootMenu.getChildren().get(j).getMenuUrl();
// menuUrls.add(menuUrl);
}
LoginRes loginRes = new LoginRes(rootUrl);
loginRes.setUserId(authUser.getOprtrId());
loginRes.setUserNm(authUser.getOprtrNm());
loginRes.setMenuUrls(menuUrls);
loginRes.setAuthToken(authToken);
ChkAuthNumResDto result = new ChkAuthNumResDto(ApiResponseCode.SUCCESS, loginRes);
return result;
}
@@ -172,10 +190,11 @@ public class LoginController {
// 2차인증후 시큐리티 성공핸들러
rCode = loginSuccessHandler.process(request, response, authentication);
// access 토큰 생성
jwtSvc.generatePrivateToken(response, authentication);
// loginToken
String authToken = jwtSvc.generatePrivateToken(response, authentication);
// jwtSvc.generatePrivateToken(response, authentication);
String nextUrl = getReturnUrl(request, response);
logger.debug("login SUCCESS - nextUrl = [{}]", nextUrl);
AuthUser authUser = loginService.getUser(authentication.getName());
@@ -187,11 +206,26 @@ public class LoginController {
Menu rootMenu = menu.get(0);
String rootUrl = rootMenu.getChildren().get(0).getMenuUrl();
// 메뉴 url data 생성
ArrayList<String> menuUrls = new ArrayList<String>();
for(int j=0; j< menu.size(); j++) {
Menu parentMenu = menu.get(j);
if(parentMenu.getChildren().size() > 0) {
ArrayList<Menu> childrenMenu = (ArrayList<Menu>)parentMenu.getChildren();
for(int k=0; k< childrenMenu.size(); k++) {
Menu childMenu = childrenMenu.get(k);
String menuUrl = childMenu.getMenuUrl();
menuUrls.add(menuUrl);
}
}
}
LoginRes loginRes = new LoginRes(rootUrl);
//LoginRes loginRes = new LoginRes(SecurityConfig.LOGIN_SUCC_URL);
loginRes.setUserId(authUser.getOprtrId());
loginRes.setUserNm(authUser.getOprtrNm());
loginRes.setAuthToken(authToken);
loginRes.setMenuUrls(menuUrls);
ChkAuthNumResDto result = new ChkAuthNumResDto(rCode, loginRes);
return result;
}else {
@@ -255,8 +289,24 @@ public class LoginController {
if (auth != null){
new SecurityContextLogoutHandler().logout(request, response, auth);
}
jwtSvc.destroyPrivateToken(request, response);
// jwtSvc.destroyPrivateToken(request, response);
return new LogoutResDto();
}
/**
* date : 2022. 8. 17.
* auth : ckr
* desc : 2차 인증 페이지 진입시 accessToken 체크
* @param loginCheckDto
* @param request
* @param response
* @return LoginResDto
*/
@PostMapping("loginCheck")
@ResponseBody
public LoginResDto loginCheck(@RequestBody LoginReqDto loginCheckDto, HttpServletRequest request, HttpServletResponse response) {
return loginService.loginCheck(loginCheckDto);
}
}

View File

@@ -1,12 +1,9 @@
package kr.co.uplus.ez.api.login;
import kr.co.uplus.ez.api.login.dto.*;
import kr.co.uplus.ez.common.data.ApiResponseCode;
import kr.co.uplus.ez.common.data.Const;
import kr.co.uplus.ez.common.utils.DateUtils;
import kr.co.uplus.ez.common.utils.EncryptionUtil;
import kr.co.uplus.ez.common.utils.TextUtils;
import kr.co.uplus.ez.config.SecurityConfig;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.commons.codec.binary.StringUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.mybatis.spring.SqlSessionTemplate;
@@ -17,8 +14,27 @@ import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import io.jsonwebtoken.Claims;
import kr.co.uplus.ez.api.login.dto.AuthNum;
import kr.co.uplus.ez.api.login.dto.AuthNumReqDto;
import kr.co.uplus.ez.api.login.dto.AuthNumResDto;
import kr.co.uplus.ez.api.login.dto.AuthUser;
import kr.co.uplus.ez.api.login.dto.ChkAuthNumReqDto;
import kr.co.uplus.ez.api.login.dto.LoginReqDto;
import kr.co.uplus.ez.api.login.dto.LoginRes;
import kr.co.uplus.ez.api.login.dto.LoginResDto;
import kr.co.uplus.ez.api.login.dto.ResetPasswordReqDto;
import kr.co.uplus.ez.api.login.dto.ResetPasswordResDto;
import kr.co.uplus.ez.api.login.dto.SendMsgDto;
import kr.co.uplus.ez.api.login.dto.UpdatePasswordReqDto;
import kr.co.uplus.ez.api.login.dto.UpdatePasswordResDto;
import kr.co.uplus.ez.common.data.ApiResponseCode;
import kr.co.uplus.ez.common.data.Const;
import kr.co.uplus.ez.common.jwt.JwtService;
import kr.co.uplus.ez.common.utils.DateUtils;
import kr.co.uplus.ez.common.utils.EncryptionUtil;
import kr.co.uplus.ez.common.utils.TextUtils;
import kr.co.uplus.ez.config.SecurityConfig;
@Service
public class LoginService {
@@ -49,6 +65,9 @@ public class LoginService {
@Value("${authentication.cntLimit: 10}")
private int cntLimit;
@Autowired
private JwtService jwtSvc;
/**
* 1차 로그인 인증
*
@@ -105,7 +124,13 @@ public class LoginService {
return new LoginResDto(ApiResponseCode.LOGIN_UPDATE_PWD, new LoginRes(SecurityConfig.LOGIN_UPDATE_PWD_URL));
}
return new LoginResDto(ApiResponseCode.SUCCESS, new LoginRes(SecurityConfig.LOGIN_SUCC_URL));
// access 토큰 생성
LoginRes loginRes = new LoginRes();
String accessToken = jwtSvc.accessToken(user);
loginRes.setAccessToken(accessToken);
loginRes.setNextUrl(SecurityConfig.VUE_URL_ARRY[0].toString());
return new LoginResDto(ApiResponseCode.SUCCESS, loginRes);
}
/**
@@ -433,5 +458,74 @@ public class LoginService {
dbUserParam.setOprtrId(userId);
return authUserMapper.getUser(dbUserParam);
}
/**
* access 토큰 확인
*
* @param loginCheckDto
* @return LoginResDto
*/
@SuppressWarnings("unchecked")
public LoginResDto loginCheck(LoginReqDto loginCheckDto){
String accessTokenStr = "";
try {
accessTokenStr = loginCheckDto.getAccessToken();
// validate check
if(accessTokenStr == null || accessTokenStr.equals("")) {
// 에러 코드 처리
return new LoginResDto(ApiResponseCode.CE_AUTH_ERROR);
}
String oprtrId = loginCheckDto.getOprtrId();
if(oprtrId == null || oprtrId.equals("")) {
// 에러 코드 처리
return new LoginResDto(ApiResponseCode.CE_AUTH_ERROR);
}
Claims claims = jwtSvc.parseToken(accessTokenStr);
String tokenId = claims.get("jti").toString();
if(!oprtrId.equals(tokenId)) {
// 에러 코드 처리
return new LoginResDto(ApiResponseCode.CE_AUTH_ERROR);
}
// 토큰 파싱
Map<String, Object> principal = (Map<String, Object>) claims.get("principal");
String parsingOprtrId = principal.get("oprtrId").toString();
String parsingOprtrPwd = principal.get("pwd").toString();
if(!parsingOprtrId.equals(tokenId)) {
// 에러 코드 처리
return new LoginResDto(ApiResponseCode.CE_AUTH_ERROR);
}
// get user info
LoginMapper loginMapper = sqlSessionMaster.getMapper(LoginMapper.class);
String userId = parsingOprtrId;
AuthUser dbUserParam = new AuthUser();
dbUserParam.setOprtrId(userId);
AuthUser user = loginMapper.getUser(dbUserParam);
if(user == null) {
// 사용자 조회 결과 없음
return new LoginResDto(ApiResponseCode.CE_AUTH_ERROR);
}
if(!parsingOprtrPwd.equals(user.getPwd())) {
// 에러 코드 처리
return new LoginResDto(ApiResponseCode.CE_AUTH_ERROR);
}
}catch(Exception e) {
logger.info(e.getMessage());
return null;
}
LoginRes loginRes = new LoginRes();
loginRes.setAccessToken(accessTokenStr);
return new LoginResDto(ApiResponseCode.SUCCESS, loginRes);
}
}

View File

@@ -13,4 +13,7 @@ public class LoginReqDto implements Serializable {
private String oprtrId;
@ApiModelProperty(example = "어드민 사용자 Pw", name = "어드민 사용자 Pw", dataType = "String")
private String oprtrPw;
@ApiModelProperty(hidden = true)
private String accessToken;
}

View File

@@ -1,10 +1,11 @@
package kr.co.uplus.ez.api.login.dto;
import java.io.Serializable;
import java.util.ArrayList;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@SuppressWarnings("serial")
@Data
public class LoginRes implements Serializable {
@@ -18,7 +19,13 @@ public class LoginRes implements Serializable {
@ApiModelProperty(name = "사용자명", example = "사용자명", dataType = "String")
private String userNm;
/** 접근 토큰*/
private String accessToken;
/** 권한 토큰*/
private String authToken;
/** 메뉴 url*/
private ArrayList<String> menuUrls;
public LoginRes(String nextUrl) {
super();
this.nextUrl = nextUrl;

View File

@@ -65,6 +65,9 @@ public enum ApiResponseCode {
,CE_SENDMGT_DUPLICATE_SENDNUM("4021", "이미 등록된 발신번호로 저장 불가.")
/** 인증번호 발송 요청 임계값 초과 분당 10회 */
,CE_AUTHNUM_STAT_LOCK("4022", "인증요청 제한")
/** 토큰 인증 에러*/
,CE_AUTH_ERROR("4023", "토큰 인증 에러")
// 시스템
/** 알 수 없는 에러. */
,SE_UNKNOWN("9999", "알 수 없는 에러");

View File

@@ -1,58 +1,67 @@
package kr.co.uplus.ez.common.jwt;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
public class JwtAuthCookieFilter extends JwtAuthFilter {
@Autowired
private JwtService jwtSvc;
public JwtAuthCookieFilter(JwtProperties jwtProps) {
super(jwtProps);
}
@Override
public String getToken(HttpServletRequest request) {
String payload = null, signature = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
// 1. 쿠키에서 jwt 토큰 header.paload 부분 읽기
if (jwtProps.getPart1().equals(cookie.getName())) {
payload = cookie.getValue();
}
// 2. 쿠키에서 jwt 토큰 signature 부분 읽기
else if (jwtProps.getPart2().equals(cookie.getName())) {
signature = cookie.getValue();
}
}
}
if (cookies == null || payload == null || signature == null) {
return null;
}
String token = payload + "." + signature;
return token;
}
@Override
public void onValidateSuccess(HttpServletRequest request, HttpServletResponse response, Claims claims) {
// 토큰 업데이트 - Sliding Sessions
jwtSvc.updatePrivateToken(response, claims);
}
@Override
public void onValidateException(HttpServletRequest request, HttpServletResponse response, JwtException exception) {
if (exception instanceof ExpiredJwtException) {
jwtSvc.destroyPrivateToken(request, response);
}
}
}
//package kr.co.uplus.ez.common.jwt;
//
//import javax.servlet.http.Cookie;
//import javax.servlet.http.HttpServletRequest;
//import javax.servlet.http.HttpServletResponse;
//
//import org.springframework.beans.factory.annotation.Autowired;
//
//import io.jsonwebtoken.Claims;
//import io.jsonwebtoken.ExpiredJwtException;
//import io.jsonwebtoken.JwtException;
//
//public class JwtAuthCookieFilter extends JwtAuthFilter {
// @Autowired
// private JwtService jwtSvc;
//
//
// public JwtAuthCookieFilter(JwtProperties jwtProps) {
// super(jwtProps);
// }
//
// @Override
// public String getToken(HttpServletRequest request) {
//// String payload = null, signature = null;
//// Cookie[] cookies = request.getCookies();
//// if (cookies != null) {
//// for (Cookie cookie : cookies) {
//// // 1. 쿠키에서 jwt 토큰 header.paload 부분 읽기
//// if (jwtProps.getPart1().equals(cookie.getName())) {
//// payload = cookie.getValue();
//// }
//// // 2. 쿠키에서 jwt 토큰 signature 부분 읽기
//// else if (jwtProps.getPart2().equals(cookie.getName())) {
//// signature = cookie.getValue();
//// }
//// }
//// }
//// if (cookies == null || payload == null || signature == null) {
//// return null;
//// }
//// String token = payload + "." + signature;
//// return token;
// // 1. get jwtToken
// String header = request.getHeader(jwtProps.getHeader().toLowerCase());
// // 2. 헤더 값 검사
// if(header == null || !header.startsWith(jwtProps.getPrefix())) {
// return null;
// }
//
// // 3. Authorization 헤더에서 토큰 추출
// return header.replace(jwtProps.getPrefix(), "");
// }
//
// @Override
// public void onValidateSuccess(HttpServletRequest request, HttpServletResponse response, Claims claims) {
// // 토큰 업데이트 - Sliding Sessions
// jwtSvc.updatePrivateToken(response, claims);
// }
//
// @Override
// public void onValidateException(HttpServletRequest request, HttpServletResponse response, JwtException exception) {
// if (exception instanceof ExpiredJwtException) {
// jwtSvc.destroyPrivateToken(request, response);
// }
// }
//
//}

View File

@@ -1,13 +1,23 @@
package kr.co.uplus.ez.common.jwt;
import java.util.Enumeration;
import java.util.HashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class JwtAuthHeaderFilter extends JwtAuthFilter {
@Autowired
private JwtService jwtSvc;
public JwtAuthHeaderFilter(JwtProperties jwtProps) {
super(jwtProps);
}
@@ -15,9 +25,22 @@ public class JwtAuthHeaderFilter extends JwtAuthFilter {
@Override
public String getToken(HttpServletRequest request) {
log.info("uri : {}", request.getRequestURI().toString());
// 1. 공통 헤더 체크
HashMap<String, String> headerMap = new HashMap<String, String>();
Enumeration<String> enumeration = request.getHeaderNames();
if (enumeration != null) {
while (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
headerMap.put(name.toLowerCase(), request.getHeader(name));
}
}
log.info("headerMap : {}",headerMap.toString());
// 1. access token이 저장된 헤더 읽기
String header = request.getHeader(jwtProps.getHeader());
String header = request.getHeader(jwtProps.getHeader().toLowerCase());
log.info("header authToken: {}" , header);
// 2. 헤더 값 검사
if(header == null || !header.startsWith(jwtProps.getPrefix())) {
return null;
@@ -29,10 +52,16 @@ public class JwtAuthHeaderFilter extends JwtAuthFilter {
@Override
public void onValidateSuccess(HttpServletRequest request, HttpServletResponse response, Claims claims) {
// jwtSvc.updatePrivateToken(response, claims);
}
@Override
public void onValidateException(HttpServletRequest request, HttpServletResponse response, JwtException exception) {
if (exception instanceof ExpiredJwtException) {
//jwtSvc.destroyPrivateToken(request, response);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
}

View File

@@ -35,7 +35,7 @@ public class JwtService {
@Qualifier("sqlSessionTemplateDb2")
private SqlSessionTemplate sqlSessionSlave;
public void generatePrivateToken(HttpServletResponse response, Authentication auth) {
public String generatePrivateToken(HttpServletResponse response, Authentication auth) {
Claims claims = coreClaims(auth, jwtProps.getPrivateTokenExpiration());
// 필요하면 다른 정보 추가
@@ -45,7 +45,8 @@ public class JwtService {
String token = generateToken(claims);
// 쿠키에 토큰 추가 - 보안 강화
setTokenToCookie(response, token);
//setTokenToCookie(response, token);
return token;
}
private Claims coreClaims(Authentication auth, int expire) {
@@ -161,13 +162,29 @@ public class JwtService {
return generateToken(claims);
}
public String accessToken(AuthUser authUser) {
String setId = authUser.getOprtrId();
DateTime now = DateTime.now();
Date expiration = now.plusSeconds(jwtProps.getAccessTokenExpiration()).toDate();
Claims claims = Jwts.claims()
.setId(setId)
.setIssuedAt(now.toDate())
.setExpiration(expiration);
JwtUser jwtUser = JwtUser.createJwtUser(authUser);
claims.put("principal", jwtUser);
return generateToken(claims);
}
private String refreshToken(Authentication auth) {
Claims claims = coreClaims(auth, jwtProps.getRefreshTokenExpiration());
return generateToken(claims);
}
private Claims parseToken(String token) {
public Claims parseToken(String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey(TextCodec.BASE64.decode(jwtProps.getKeyString()))

View File

@@ -24,6 +24,7 @@ public class JwtUser extends AuthUser {
u.setOprtrNm(user.getUsername());
u.setHpNo(user.getHpNo());
u.setAuthorities(user.getAuthorities());
u.setPwd(user.getPassword());
return u;
}

View File

@@ -20,7 +20,7 @@ import org.springframework.web.cors.CorsUtils;
import kr.co.uplus.ez.api.login.LoginFailureHandler;
import kr.co.uplus.ez.api.login.LoginSuccessHandler;
import kr.co.uplus.ez.common.jwt.JwtAuthCookieFilter;
//import kr.co.uplus.ez.common.jwt.JwtAuthCookieFilter;
import kr.co.uplus.ez.common.jwt.JwtAuthHeaderFilter;
import kr.co.uplus.ez.common.jwt.JwtExceptionFilter;
import kr.co.uplus.ez.common.jwt.JwtProperties;
@@ -31,7 +31,7 @@ import kr.co.uplus.ez.config.filter.VueStaticFilter;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String LOGIN_FORM_URL = "/login";
public static final String LOGIN_API_URL = "/api/v1/bo/login";
public static final String LOGIN_API_URL = "/api/v1/bo/login/**";
public static final String LOGIN_FAIL_URL = "/login?error=true";
public static final String LOGIN_SUCC_URL = "/";
public static final String LOGIN_UPDATE_PWD_URL = "/view/login/updatePassword";
@@ -40,6 +40,15 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
public static final String PUBLIC_API_URL = "/api/v1/bo/**"; // 내부에서 인증없이 호출하는 API
public static final String[] REST_API_URLS = {API_URL, TEST_PERMIT_URL};
public static final String[] VUE_URL_ARRY = {
"/view/login/auth"
,"/custMgt/subsList"
,"/custMgt/memberList"
,"/custMgt/subsDetail"
,"/custMgt/memberDetail"
,"/custMgt/memberAdminDetail"
};
private static final String[] PERMIT_URL_ARRAY = {
"/login",
"/api/v1/bo/login/**",
@@ -47,7 +56,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
"/swagger-resources/**",
"/v3/api-docs",
"/v3/api-docs/**",
"/"
"/",
"/socket/**"
};
private static final String[] AUTH_URL_ARRAY = {
@@ -59,7 +69,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
"/api/v1/bo/sendNumMgt/**",
"/api/v1/bo/mntrng/**",
"/api/v1/bo/riskMgt/sendNum/**",
"/api/v1/bo/stats/**"
"/api/v1/bo/stats/**",
"/view/error/**"
};
@@ -78,17 +89,17 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
.antMatchers("/static/**", "/assets/**");
}
@Bean
public Filter jwtAuthFilter() {
return new JwtAuthCookieFilter(jwtProps);
}
// @Bean
// public Filter jwtAuthFilter() {
// return new JwtAuthCookieFilter(jwtProps);
// }
@Override
public void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(new VueStaticFilter(), UsernamePasswordAuthenticationFilter.class) // Vue에서 호출시 화면관련 URL은 / forward
.addFilterBefore(new JwtExceptionFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
// .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new JwtAuthHeaderFilter(jwtProps), UsernamePasswordAuthenticationFilter.class);
http