반응형

앞선 2020/08/19 - [개발] - JAVA Spring Validation에 사용된 User Class에 있는 annotation을 포스팅하려고 해요

대략적인 User class입니다.

@EqualFields(baseField = "loginPwd", matchField = "loginPwdRe", message = "비밀번호랑 비밀번호확인이 일치하지 않습니다.")
public class User {
    
	private String rprsntYn;
    
	private Integer id;
    
	private Integer userType;
    
	private String name;
	
	@IdCheck(message = "아이디 체크버튼을 눌러주세요.")
	@NotBlank(message = "아이디를 입력하세요.")
	private String loginId;
   
}

@NotBlank,@EqualFields는 기본적으로 제공되는 annotation입니다.

@NotBlank 사용하기에 앞서 비슷한 2가지 annotation이 있습니다.

@NotBlank @NotNull @NotEmpty
null 체크 and "" 빈값허용불가 null 만체크 "" 빈값만 체크

상황에 맞게 사용하시면 돼요.

EqualFields annotation는 필드에 선언이 아니고 Class상단에 선언이 되어야 작동을 해요.

728x90

이제 @IdCheck custom annoation을 JAVA Spring Validation에 사용된 User Class에 있는 annotation을 포스팅하려고 해요

@Target({ElementType.METHOD, ElementType.FIELD}) //사용할 타켓은 메소드랑 필드를 지정했어요
@Retention(RetentionPolicy.RUNTIME) // 컴파일 이후에도 JVM에 의해서 참조가 가능합니다.
//@Retention(RetentionPolicy.CLASS) // 컴파일러가 클래스를 참조할 때까지 유효합니다.
//@Retention(RetentionPolicy.SOURCE) // 어노테이션 정보는 컴파일 이후 없어집니다.
@Constraint(validatedBy = {IdValidator.class})
public @interface IdCheck {
    String message() default "";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

----------------------------------------------------
//아이디가 이메일방식 또는 일반문자로 받고있어서 검증을 다르게하였습니다.
public class IdValidator implements ConstraintValidator<IdCheck, String> {
	
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (!StringUtils.isEmpty(value)) {
            Pattern p = null;
            if (value.contains("@")) {
                p = Pattern.compile("^[_a-z0-9-]+(.[_a-z0-9-]+)*@(?:\\w+\\.)+\\w+$");
                //새로운 메세지추가
                context.buildConstraintViolationWithTemplate("message.validation.join.loginId.verification2").addConstraintViolation();
                //기존메세지 비활성
                context.disableDefaultConstraintViolation();
            } else {
               
                p = Pattern.compile("^[a-zA-Z]{1}[a-zA-Z0-9_]{5,19}$");
            }
            Matcher m = p.matcher(value);
            return m.matches();
        } else {
            return true;
        }
    }
    
    @Override
    public void initialize(IdCheck constraintAnnotation) {
    	// TODO Auto-generated method stub
    	
    }
	
}

return값이 true, false에 따라서 loginId필드 검증 데이터가 BindingResult에 담겨서 Controller에 전달을 하고있습니다.

이제 기본  annoation에서 제공이 안 되는 부분은 직접 구현을 하여 적용을 할 수가 있습니다.

반응형
반응형

프로젝트에서 적용한 Java vaildation을 포스팅하려고 해요.

 

보통 Front단에서 vaildation을 하는데 Server단에서도 적용을 하자고 하여 org.springframework.validation을 사용하게 되었어요

 

예를 들어 Controller에서 사용자 정보 추가를 한다고 하면

public ResponseEntity<HttpResponse<String>> add(
        HttpServletRequest req,
            HttpServletResponse res,
            HttpSession session,
            ModelMap model,
            @Valid @RequestBody User user,
            BindingResult result,
            )

저기 소스에서 @Valid, BindingResult는 꼭 순서를 지켜야 해요.

User class안에 정의된 annotation으로 규칙에 어긋난 값은 BinBindingResult에 담겨서 Controller로 자동으로 넘어와요.

 

List<ObjectError> allErrors = result.getAllErrors();
	
for (ObjectError error : allErrors) {

	String errorMessage = error.getDefaultMessage();
	String key = errorMessage.substring(0, errorMessage.lastIndexOf(".")).replaceAll(CommonConstants.VALIDATION_MESSASGE_PREFIX, "");
	
	System.out.println("key=="+key);
 		
}

이런 식으로 하면 에러가 발생한 User class변수명을 가져올 수 있어요. 그러면 변수명으로 각 프로젝트에 맞게 사용이 가능해요. 만약 annotation으로 check가 불가능한 경우

BindingResult r = new DirectFieldBindingResult(User.builder().build(), "user");
if (조건) {                  
	
	objectError = new ObjectError("user", "메세지");
	r.addError(objectError);

}

위 코드처럼 조건을 추가하여 BindingResult를 넘기면 annotation으로 넘어온 것처럼 메시지가 추가되어서 넘아가요.

 

화면으로 넘길 때는

Failure failure = Failure.VALIDATION;
throw new HttpException(BeanConverter.toJson(errors),HttpStatus.BAD_REQUEST,failure);

public enum Failure {
    ERROR("error"), VALIDATION("validation");
    private String value;
    private Failure(String value) {
        this.value = value;
    }
    public String value() {
        return value;
    }
}

public class HttpException extends ServletException {
    
    private static final long serialVersionUID = 1L;
    private HttpStatus httpStatus;
    private Failure failure = Failure.ERROR;
    private String message;
   
    public HttpException(String message, HttpStatus httpStatus, Failure failure) {
        super(message);
        this.message = message;
        this.httpStatus = httpStatus;
        this.failure = failure;
    }
    
}
728x90

BeanConverter.toJson는 JSON으로 만들어서 화면으로 넘겨주고 있어요

public String toJson(Object bean) throws Exception {
	ObjectMapper mapper = new ObjectMapper();
	String jsonString = null;
	if (StringUtils.isEmpty(bean)) {
		jsonString = null;
	} else {
		jsonString = mapper.writeValueAsString(bean);
	}
	return StringUtils.isEmpty(jsonString) ? "{}" : jsonString;
}

화면에서는 failure가 "validation"값이면 넘어온 Json값을 찍어주고 있어요.

$( document ).ajaxError(function( event, request, settings ) {
	
	if (request.status == 401) {
		
	} else if (request.status == 500) {

		
	} else if (request.status == 400) {

		if (settings.dataType == "json") {
			var result = JSON.parse(request.responseText);

			var body = result.body;

			try {
				body = eval(result.body);
			} catch(e) {}

			if (result.failure == "validation") {

				for (var i = 0; i < body.length; i++) {
					$._common.alert(body[i].message);
				}
			} else {
				$._common.alert(body);
			}
		} else {
			$._common.alert("잘못된 요청입니다.");
		}
	} else if (request.status == 412) {
		
	} else {
		
		
	}
});

다음에 User Class에 걸린 Annotation을 예제로 포스팅할게요.

2020/08/19 - [개발] - Java Custom Annotation

반응형
반응형

개발코드를 그냥 복사해서 붙여버리면 일반 텍스트처럼 보이기 때문에 보는데 불편하여 코드 강조하는 syntaxhighlighter를 적용하려고 해요. 사이트 가서 받아도 되지만 첨부파일 받아오되요

syntaxhighlighter_3.0.83.zip
0.17MB

압축을 풀고 scripts, styles 두 개 폴더가 필요해요

먼저 스킨에 위 두개폴더 js, css파일을 업로드해야 해요

스킨 편집 이동 후

html 편집

추가를 클릭 한 다음에 script, css파일을 업로드해주면 돼요

업로드 완료 후 HTML탭에 가서

<script type="text/javascript" src="./images/shCore.js"></script>
<script type="text/javascript" src="./images/shLegacy.js"></script>
<script type="text/javascript" src="./images/shBrushBash.js"></script>
<script type="text/javascript" src="./images/shBrushCpp.js"></script>
<script type="text/javascript" src="./images/shBrushCSharp.js"></script>
<script type="text/javascript" src="./images/shBrushCss.js"></script>
<script type="text/javascript" src="./images/shBrushDelphi.js"></script>
<script type="text/javascript" src="./images/shBrushDiff.js"></script>
<script type="text/javascript" src="./images/shBrushGroovy.js"></script>
<script type="text/javascript" src="./images/shBrushJava.js"></script>
<script type="text/javascript" src="./images/shBrushJScript.js"></script>
<script type="text/javascript" src="./images/shBrushPhp.js"></script>
<script type="text/javascript" src="./images/shBrushPlain.js"></script>
<script type="text/javascript" src="./images/shBrushPython.js"></script>
<script type="text/javascript" src="./images/shBrushRuby.js"></script>
<script type="text/javascript" src="./images/shBrushScala.js"></script>
<script type="text/javascript" src="./images/shBrushSql.js"></script>
<script type="text/javascript" src="./images/shBrushVb.js"></script>
<script type="text/javascript" src="./images/shBrushXml.js"></script>
<link type="text/css" rel="stylesheet" href="./images/shCore.css">
<link type="text/css" rel="stylesheet" href="./images/shThemeEclipse.css">
<script type="text/javascript">
SyntaxHighlighter.all();
</script>

head에 추가하시면 돼요 전 이클립스 테마를 사용했어요

<pre class="brush: 언어">

String a;

</pre>

사용하시면 돼요.

 

만약 중간에 스킨을 변경을 하게 되면 <meta><script>등이 날아가기 때문에 변경 전 검색 태그나 그런 건 복사를 해두시고 스킨 변경을 하셔야 해요.

첨 설정을 했다가 스킨 변경으로 날려먹어 다시 설정을 했어요

반응형

'개발' 카테고리의 다른 글

이클립스에서 한글이 깨질경우  (3) 2020.12.20
톰켓 Symbolic link(Windows)  (0) 2020.10.27
spring security 중복 로그인 방지  (3) 2020.08.12
반응형

예전 사이트에 적용한 security를 사용한 중복 로그인 방지를 작성해 보려고 해요

 

요구사항

  • 관리자 계정은 로그인이 한 명만 가능하고 중복 로그인하면 이전 사용자는 강제 로그아웃
  • 일반 사용자는 설정에 맞게 중복 로그인이 가능 로그인 사용자가 꽉 차면 로그인 불가

그래서 org.springframework.security.core.session.SessionRegistry를 이용하여 구현을 했어요.

 

protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests().antMatchers("/ex/**").permitAll().anyRequest().authenticated();
	http.authorizeRequests().anyRequest().authenticated().and().formLogin().successHandler(
	        defaultLoginSuccessHandler).failureHandler(defaultLoginFailureHandler).loginPage(loginPage).failureUrl(
	                loginPage + "?error").permitAll().usernameParameter("id").passwordParameter("pwd");
	http.logout().logoutUrl("/logout").logoutSuccessUrl(loginPage).addLogoutHandler(defaultLogoutHandler)
	        .invalidateHttpSession(true).permitAll();
	// X-Frame-Options: DENY 설정하지 않음(다운로드컴포넌트를 위해)
	http.headers().frameOptions().disable();
	http.addFilterBefore(duplicatLoginFilter(), UsernamePasswordAuthenticationFilter.class);
	http.sessionManagement().maximumSessions(500).maxSessionsPreventsLogin(true).sessionRegistry(sessionRegistry)
	        .expiredUrl("/");
	http.csrf().disable();
}

protected DuplicateLoginFilter duplicatLoginFilter() throws Exception {
	
    return new DuplicateLoginFilter(sessionRegistry, loginSecurityService, loginPage, siteMapper);
    
}

@Bean
public PasswordEncoder passwordEncoder() {
	PasswordEncoder encoder = new CustomPasswordEncoder();
    return encoder;
    
}

 

세션은 총 500개로 잡았어요. 패스워드는 SHA256를 사용했어요.

 

public class DuplicateLoginFilter extends GenericFilterBean {

	private SessionRegistry sessionRegistry;
	private UserDetailsService userDetailsService;
	private String loginProcessUrl;
	private SiteMapper siteMapper;
	public DuplicateLoginFilter(final SessionRegistry sessionRegistry, final UserDetailsService userDetailsService,
	    final String loginProcessUrl, final SiteMapper siteMapper) {
		this.sessionRegistry = sessionRegistry;
		this.userDetailsService = userDetailsService;
		this.loginProcessUrl = loginProcessUrl;
		this.siteMapper = siteMapper;
	}
	@Override
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
	    throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		// post 방식에 로그인 요청이 아닌 경우 작업종료
		if (!new AntPathRequestMatcher(loginProcessUrl, "POST").matches(request)) {
			chain.doFilter(req, res);
			return;
		}
		log.info("login check filter has been requested.");
		boolean error = false;
		log.debug("로그인 세션수 -> {}", String.valueOf(sessionRegistry.getAllPrincipals().size()));
		try {
			String username = request.getParameter("id");
			String password = request.getParameter("pwd");
			// 패스워드 암호화
			byte pbCipher[] = new byte[32];
			SHA256Utils.SHA256_Encrpyt(password.getBytes("UTF-8"), password.length(), pbCipher);
			StringBuffer encPassword = new StringBuffer();
			for (int i = 0; i < 32; i++) {
				String hex = Integer.toHexString(0xff & pbCipher[i]);
				if (hex.length() == 1) {
					encPassword.append('0');
				}
				encPassword.append(hex);
			}
			UserDetails details = userDetailsService.loadUserByUsername(username);
			LoginUser principal = (LoginUser) details;
			// 사용자 정보 일치여부
			if (details.getPassword().equals(encPassword.toString())) {
				// 동일한 로그인 세션 체크
				log.debug(principal.toString());
				//public 공용, private 개인
				String idType = principal.getIdType();
				//공용일경우 로그인 제한 count
				int connCnt = principal.getConnCnt();
				log.debug("connCnt==" + connCnt);
				List<SessionInformation> sessionInfo = sessionRegistry.getAllSessions(principal, false);
			
				if ("private".equals(idType)) {
					//개인용 일경우 기존 정보가 있을경우 만료 후 로그인
					Iterator<SessionInformation> session = sessionInfo.iterator();
					while (session.hasNext()) {
						SessionInformation sessionInformation = session.next();
						LoginUser user = (LoginUser) sessionInformation.getPrincipal();
						if (user.getUsername().equals(username)) {
							sessionInformation.expireNow();
							break;
						}
					}
				} else {
					Iterator<SessionInformation> session = sessionInfo.iterator();
					int loginCnt = 1;
					while (session.hasNext()) {
						SessionInformation sessionInformation = session.next();
						LoginUser user = (LoginUser) sessionInformation.getPrincipal();
						if (user.getUsername().equals(username)) {
							if (connCnt <= loginCnt) {
								error = true;
								break;
							}
							loginCnt++;
						}
					}
				}
			} else {
				//사용자 정보 불일치
				MessageAlertUtils.alertRedirectMsg(response, "아이디 또는 비밀번호를 다시 확인하세요.", "/auth/login?error");
				return;
			}
		} catch (UsernameNotFoundException e) {
			MessageAlertUtils.alertRedirectMsg(response, "아이디 또는 비밀번호를 다시 확인하세요.", "/auth/login?error");
			//error = false;
		}
		log.debug("error==" + error);
		if (!error) {
			chain.doFilter(req, res);
			return;
		}
		MessageAlertUtils.alertRedirectMsg(response, "접속 가능한 인원수가 초과 되었습니다.", "/auth/login?error");
		return;
	}
}
728x90

위는 중복체크 filter 전체 소스예요

실제 중복 처리하는 부분은 아래 소스예요

Iterator<SessionInformation> session = sessionInfo.iterator();
while (session.hasNext()) {
	SessionInformation sessionInformation = session.next();
	LoginUser user = (LoginUser) sessionInformation.getPrincipal();
		
	//현재 로그인 된 사용자가 있으면 기존사용자 세션만료
	if (user.getUsername().equals(username)) {
		sessionInformation.expireNow();
		break;
	}
}

//해당계정 설정된 유저수가 넘으면 로그인 차단
Iterator<SessionInformation> session = sessionInfo.iterator();
int loginCnt = 1;
while (session.hasNext()) {
	SessionInformation sessionInformation = session.next();
	LoginUser user = (LoginUser) sessionInformation.getPrincipal();
	if (user.getUsername().equals(username)) {
		if (connCnt <= loginCnt) {
			error = true;
			break;
		}
		loginCnt++;
	}
}

각 요구사항에 맞게 처리한 로직이에요

 

직접 적용한 소스 기반으로 작성해봤어요

반응형

'개발' 카테고리의 다른 글

이클립스에서 한글이 깨질경우  (3) 2020.12.20
톰켓 Symbolic link(Windows)  (0) 2020.10.27
티스토리 소스코드 넣기  (0) 2020.08.13
반응형

카카오 API를 통해서 카카오 로그인을 남겨보려고 해요

 

https://developers.kakao.com/먼저 로그인을 하시고 내 애플리케이션 가서 만드시면 화면처럼 키가 발급이 되었을 거예요

 

 

 

 

저는 카카오 Rest Api를 사용해서 로그인을 할 거예요Rest방식은 Spring RestTemplate를 이용해서 가져올 거예요

 

 

 

 

카카오 인증절차예요.

카카오개발자(https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api) 사이트에서 가져왔어요

 

먼저 https://kauth.kakao.com/oauth/authorize?client_id={apiKey}&redirect_uri={returnUrl}&response_type=code

 

restApi키와 등록한 returnUrl을 넘기면

 

정상적으로 호출이 되면 동의 화면이 나올 거예요  황목설정은 "동의 항목"에서 설정이 가능해요

 

이제 "동의하고 계속하기"클릭 시 설정한 returnUrl로 redirect가 되면서 code이 넘어와요.

 

private static final String kakaoReturnUrl = "returnUrl";	
	private static final String kakaoLoginUrl = "https://kauth.kakao.com/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code";	

	private static final String kakaoKey = "apiKey";	
	private static final String kakaoToken = "https://kauth.kakao.com/oauth/token";
	private static final String kakaoProfile = "https://kapi.kakao.com/v2/user/me";
    
	private OverlapLogin loginManager = OverlapLogin.getInstance();
	
	@RequestMapping(value ="/openId/kakao", method=RequestMethod.GET)
	public String kakaoLogin(HttpServletRequest request, Model model, HttpSession session ) {
	
		return "redirect:" + String.format(kakaoLoginUrl, kakaoKey, (getServerName(request, kakaoReturnUrl)));
	
	}
	
	@RequestMapping(value ="/openId/kakaoLogin")
	public String kakaoLoginCallback(EnnacoreMap enna, Model model, HttpSession session,HttpServletRequest request, HttpServletResponse response ) throws Exception {

      //로그인 후 받는 code값
      String code = request.getParameter("code");

      MultiValueMap<String, Object> mmap = new LinkedMultiValueMap<String, Object>();

      mmap.add("grant_type", "authorization_code"); //필수 고정값
      mmap.add("client_id", kakaoKey); //카카오 rest_key
      mmap.add("redirect_url", getServerName(request, kakaoReturnUrl)); //응답받은 리턴URL
      mmap.add("code", code); //카카오 로그인 후

      HttpHeaders headers = new HttpHeaders();

      headers.add("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=utf-8"); //헤더지정
      HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<MultiValueMap<String,Object>>(mmap, headers);

      RestTemplate restTemplate = new RestTemplate();
      FormHttpMessageConverter converter = new FormHttpMessageConverter();
      converter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_FORM_URLENCODED));
      restTemplate.getMessageConverters().add(converter);

      //code를 이용해 로그인 사용자 token값 가벼오기
      ResponseEntity<AccessTokenRequestResponse> tokenResponse = restTemplate.postForEntity(kakaoToken, httpEntity, AccessTokenRequestResponse.class);
      headers.add("Authorization", "Bearer " + tokenResponse.getBody().getAccess_token());
      mmap.clear();
      httpEntity = new HttpEntity<MultiValueMap<String,Object>>(mmap, headers);

      //해당 토큰값으로 사용자 정보 가져오기
      ResponseEntity<UserProfileViewResponse> profileResponse = restTemplate.postForEntity(kakaoProfile, httpEntity, UserProfileViewResponse.class);

      // logger.info("loginRecd::::"+profileResponse.getBody().getKakao_account().getAge_range());
      // logger.info("loginRecd::::"+profileResponse.getBody().getKakao_account().getBirthday());
      // logger.info("loginRecd::::"+profileResponse.getBody().getKakao_account().getEmail());
      // logger.info("loginRecd::::"+profileResponse.getBody().getProperties().getNickname());

	}

    public String getServerName(HttpServletRequest req, String returnUrl) {

      StringBuffer serverName = new StringBuffer("");
      serverName.append("http://");
      serverName.append(req.getServerName());
      serverName.append(returnUrl);
      return serverName.toString();
    }

VO class들은 파일로 올렸습니다


AccessTokenRequest.java
0.00MB
AccessTokenRequestResponse.java
0.00MB
UserProfileViewKakaoAccountResponse.java
0.00MB
UserProfileViewPropertiesResponse.java
0.00MB
UserProfileViewResponse.java
0.00MB

반응형

+ Recent posts