로그인한 회원의  안전한 정보변경을 위해 비밀번호를 재인증받아보자!! 

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
* 암호화된 비밀번호 일치 여부
* 사용예) 
* 비번 : text123
* a: $qweqsd$awe.113212wqedasdqweqdqwdadwqwe ( DB에서 가져온 암호화된 비밀번호) 
* b: text123 
* @param a : DB에서 가져온 암호화된 비밀번호 
* @param b : 사용자가 입력한 비밀번호 (암호화 전) 
* @return 일치여부 
*/
public static boolean cryptMatch(String a, String b) {
	BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
	return encoder.matches(a, b);
}

 

BCryptPasswordEncoder 는 
BCrypt 라는 해시 함수를 사용한 구현체이다. 단순히 해시를 하는것 뿐만 아니라 Salt 를 넣는 작업까지 하므로, 입력값이 같음에도 불구하고 매번 다른 encoded된 값을 return 해주게 된다.

그러므로 문자열 비교에 사용되는 equals() 함수가아닌 matches 함수를 사용하는것이 적절하다. 

 

주의!! 

코드에 적힌바와 같이 파라메터 두개 모두 암호화된 상태가 아닌 "b" 는 사용자가 입력한 데이터 그대로 

입력해야한다.  

 

사전에 Spring Security 설정은 되어있어야 진행된다. 

SecurityContext sc = SecurityContextHolder.getContext();

//ID, PW, 권한을 설정합니다. PW는 null 이어도 상관없음
sc.setAuthentication(new UsernamePasswordAuthenticationToken(id, null, role)); // (id, pw, 권한)

//Spring security 세션에 등록
httpSession.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, sc);

 

Apple로 로그인 기능을 써보았다. 

타 서비스는 oAuth2 로 연결하였으나 애플의 경우 직접 처리해본다!!! 

 

순서는 이러하다. 

1. front  로그인 버튼을 클릭하면 apple 로그인 화면이 뜬다. 

2. 사용자가 로그인을 완료하면 개발자 센터에 등록해두었던 redirect url로 애플에서 데이터를 쏴준다. 

3. Controller에서 애플에서 넘겨준 아래와 같은 데이터를 받는다. 

{ 
state: 1111111
code: casq31253s211w35321sd15w3132e116w534s23
user: {"name":{"firstName":"Genie","lastName":"Park"},"email":"myemail@naver.com"}
}

4. 사용자가 최초로 로그인을 하였거나, 본인의 기기에서 애플로 로그인 기능을 지웠다가 다시 로그인하는 경우엔 user 데이터가 들어있다. 

5.  code 유효성 검사를 진행한다. 유효한 code라면 애플에서 아래와 같은 데이터를 넘겨준다. 

{
"access_token": "qwe4q5w3e48qwe46q5we4q6w5e4qw.qw5e4q6we54qw",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "we4q5w3e48qwe46q5we4q6w5e4qw.qw5e4q6we54qwasd.asdasdasd",
  "id_token": "asdasdasdasdasdasdasd3qwe4qw2.e1qwe654qw6e54q6w8e4q9w8ea.eqw5e6q4w8dq48q6wd4"
}

 

6.  넘겨준 id_token를 디코드하면 아래와 같은 정보를 얻을 수 있다. 

{
  "at_hash": "asd21a3sd8asd3554as3d",
  "aud": "com.genie.service",
  "sub": "00asd00.14qw0eqwe55.qwwwe410000",
  "nonce_supported": true,
  "email_verified": "true",
  "auth_time": 1231231231,
  "iss": "https://appleid.apple.com",
  "exp": 123123123123,
  "iat": 13123123123,
  "email": "myemail@naver.com"
  }

 

이렇게 받은 데이터를 가지고 내가 원하는 기능을  검증히고 구현하면 된다!! 

 

 

 

코드로 봅시다!!! 

 

1. 애플 개발자 센터 가입 후 아래의 키들을 발급받는다. 

public static final String TEAM_ID = "팀아이디";
public static final String REDIRECT_URL = "애플에 등록한 리다이렉트 url"
public static final String CLIENT_ID = "Service ID 등록시 작성했던 Identifier";
public static final String KEY_ID = "MYKEY "; // .p8 파일을 다운받았다면 파일명에 적혀있다.
public static final String AUTH_URL = "https://appleid.apple.com";
public static final String KEY_PATH = "static/AuthKey_MYKEY.p8";

 

2. Front에 버튼을 생성한다. 

<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <title>Sign in with Apple </title>
  
  <meta name="appleid-signin-client-id" content="서비스아이디 발급시 사용한 Identifier">
  <meta name="appleid-signin-scope" content="name email"> <!-- 이름과 이메일을 받는다-->
  <meta name="appleid-signin-redirect-uri" content="개발자센터에 등록한 리다이렉트 url">
  <meta name="appleid-signin-state" content="임의의 문자열">
  <meta name="appleid-signin-nonce" content="임의의 문자열">
  <meta name="appleid-signin-use-popup" content="true"> <!-- or false defaults to false -->
  
</head>
<body>

   <div id="appleid-signin" data-color="black" data-border="true" data-type="sign in"></div>
   <script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
 
</html>

 

3. Controller 에서 redirect 부분을 작업해본다. 

import java.net.URISyntaxException;
import java.text.ParseException;
import java.time.LocalDateTime;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;

import com.me.genie.util.AppleUtils;
import com.me.genie.util.HttpClientUtils;


@PostMapping("/auth/apple")
public CommonResponse<UserAuthForUserDto> authByApple( 
HttpServletRequest request
, @RequestParam(value = "code", required= false) String code
, @RequestParam(value = "user", required= false) String user
, HttpServletResponse response) throws ParseException, JsonMappingException, JsonProcessingException, URISyntaxException {


/*********************************************************************
* 애플 로그인 취소시 null 리턴  
* ******************************************************************/
if(code == null) return null;


/*********************************************************************
* code 유효성 검증 
* 유효한 토큰이라면 id_token 이 포함된 데이터를 리턴해준다.
* ******************************************************************/
String apiResponse = AppleUtils.validateAuthorizationGrantCode(code);
if(apiResponse == null) { 
logger.info("CODE 유효성 검사 실패");
return null;
}


/*********************************************************************
* id-token 유효성 검증 
* ******************************************************************/
JSONObject tokenResponse = (JSONObject) JSONValue.parse(apiResponse);
if(!AppleUtils.verifyIdentityToken((String) tokenResponse.get("id_token"))) {
logger.info("ID-TOKEN 유효성 검사 실패");
return null;
}


/*********************************************************************
* id_token 토큰을 파싱하여 데이터를 가져온다.
* ******************************************************************/
JSONObject appleInfo = AppleUtils.decodeFromIdToken((String) tokenResponse.get("id_token"));
logger.debug(tokenResponse.toJSONString());
logger.debug(appleInfo.toJSONString());


/*********************************************************************
* user 정보는 최초 1회만 제공 받을 수 있다.
* 단, 고객이 본인의 아이디 관리에서 로그인을 해지한 경우엔 다시 받을 수 있다.
* ******************************************************************/
String username = "", email = (String) appleInfo.get("email");
User target = null;



if(user != null) { // 가입한적이 없거나, 애플로 로그인 기능을 삭제 후 다시 접근하는 경우.
    
    // 사용자 정보를 추출한다. 
	JSONObject u = (JSONObject) JSONValue.parse(user);
	username = (String) u.get("lastName") + (String) u.get("firstName"); 
	email = (String) u.get("email");
    
    // TODO: 여기서부터 자유롭게 로직을 진행하면 된다. 
    // 예) username과 email을 이용해 가입이력 조회 
    // 없다면 가입, 있다면 로그인 처리
    
} else { 
	// 기존에 가입한적이 있는 경우 여기로 들어온다. 
	if(email == null) {
      return null;
	}
 	
    // 기존에 가입한적이 있는 경우 refresh_token  유효성 검사를 진행한다. 
    if(AppleUtils.validateAnExistingRefreshToken((String) tokenResponse.get("refresh_token")) == null) {
      logger.info("refresh_token 유효성 검사 실패");
      return null;
    }
			
	if(email == null) {
		return null;
	}
    
    /*********************************************************************
    * 로그인 화면에서 사용자가 이메일을 공개로 로그인한 경우 이메일 값이 들어온다.
    * 진짜 이메일이 들어오고 test111@naver.com
    * 나의 이메일 숨기기를 선택했다면 email":"7ct4qm6gdn@privaterelay.appleid.com 와 같은 이메일이 들어온다.
    * ******************************************************************/
  	email = (String) appleInfo.get("email");
  
    // TODO: 여기서부터 자유롭게 로직을 진행하면 된다. 
    // 예) email or refesh_token을 통해 사용자 가입이력을 조회한뒤 처리한다. 
	// (refesh_token은 기한이 없음.)
}

 

 

4. Controller에서 사용한 Utils

package com.unli.cogba.util;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;

import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.springframework.core.io.ClassPathResource;

import java.io.Reader;
import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;


public class AppleUtils {
	public static final String TEAM_ID = "team id";
    public static final String REDIRECT_URL = "애플에 등록한 리다이렉트 url"
    public static final String CLIENT_ID = "Service ID 등록시 작성했던 Identifier";
    public static final String KEY_ID = "MYKEY "; // .p8 파일을 다운받았다면 파일명에 적혀있다.
    public static final String AUTH_URL = "https://appleid.apple.com";
    public static final String KEY_PATH = "static/AuthKey_MYKEY.p8";
	
	
	/**
	 * createClientSecret 생성
	 * @return
	 */
    public static String createClientSecret() {
    	PrivateKey pKey = readPrivateKey(KEY_PATH);
        String token = Jwts.builder()
                .setHeaderParam(JwsHeader.KEY_ID, KEY_ID)
                .setIssuer(TEAM_ID)
                .setAudience("https://appleid.apple.com")
                .setSubject(CLIENT_ID)
                .setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 5)))
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .signWith(pKey, SignatureAlgorithm.ES256)
                .compact();
        return token;
    }   
    
    /**
     * 파일에서 private key 획득
     * @return Private Key
     */
    private static PrivateKey readPrivateKey(String keyPath) {
    	try { 
	    	ClassPathResource resource = new ClassPathResource(keyPath);
	        String privateKey = new String(Files.readAllBytes(Paths.get(resource.getURI())));
	        Reader pemReader = new StringReader(privateKey);
	        PEMParser pemParser = new PEMParser(pemReader);
	        JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
	        PrivateKeyInfo object = (PrivateKeyInfo) pemParser.readObject();
	        return converter.getPrivateKey(object);
    	} catch (Exception e) {
			e.printStackTrace();
		}
    	return null;
    }
    
    /**
     * id_token 검증
     * @param id_token
     * @return
     */
    public static boolean verifyIdentityToken(String id_token) {
    	try {
    		SignedJWT signedJWT = SignedJWT.parse(id_token);
    		JWTClaimsSet payload = signedJWT.getJWTClaimsSet();

			// EXP
			Date currentTime = new Date(System.currentTimeMillis());
		    if (!currentTime.before(payload.getExpirationTime())) {
		    	return false;
		    }

		    
		    if (!"none에 입력했던값".equals(payload.getClaim("nonce")) || !AUTH_URL.equals(payload.getIssuer()) || !CLIENT_ID.equals(payload.getAudience().get(0))) {
		    	return false;
		    }

	    // RSA
	    if (verifyPublicKey(signedJWT)) {
	    	return true;
	    }
	  } catch (Exception e) {
	  	e.printStackTrace();
	  }

	  return false;
	}
    
    
    /**
     * 공개키 검증
     * @param signedJWT
     * @return
     */
    private static boolean verifyPublicKey(SignedJWT signedJWT) {
        try {
            String publicKeys = HttpClientUtils.doGet("https://appleid.apple.com/auth/keys");
            ObjectMapper objectMapper = new ObjectMapper();
            Keys keys = objectMapper.readValue(publicKeys, Keys.class);
            for (Key key : keys.getKeys()) {
                RSAKey rsaKey = (RSAKey) JWK.parse(objectMapper.writeValueAsString(key));
                RSAPublicKey publicKey = rsaKey.toRSAPublicKey();
                JWSVerifier verifier = new RSASSAVerifier(publicKey);

                if (signedJWT.verify(verifier)) {
                    return true;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
    
    
    /**
     * refresh_token 유효성 검증
     * @param client_secret
     * @param refresh_token
     * @return
     */
    public static String validateAnExistingRefreshToken(String refresh_token) {
    	String reqUrl = AUTH_URL + "/auth/token";
        Map<String, String> tokenRequest = new HashMap<>();
        tokenRequest.put("client_id", CLIENT_ID);
        tokenRequest.put("client_secret", createClientSecret());
        tokenRequest.put("grant_type", "refresh_token");
        tokenRequest.put("refresh_token", refresh_token);

        return HttpClientUtils.doPost(reqUrl, tokenRequest);
    }
    
    /**
     * code 유효성 검증
     * @param code
     * @return
     */
    public static String validateAuthorizationGrantCode(String code) {
    	try {
	        String reqUrl = AUTH_URL + "/auth/token";
		    
	        Map<String, String> tokenRequest = new HashMap<>();
		    tokenRequest.put("client_id", CLIENT_ID);
		    tokenRequest.put("client_secret", createClientSecret());
		    tokenRequest.put("code", code);
		    tokenRequest.put("grant_type", "authorization_code");
		    String apiResponse = HttpClientUtils.doPost(reqUrl, tokenRequest);
		    
	        return apiResponse;
    	} catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
    
    
    /**
     * id_token decode
     * @param id_token
     * @return
     */
    public static JSONObject decodeFromIdToken(String id_token) {
        try {
            SignedJWT signedJWT = SignedJWT.parse(id_token);
            JWTClaimsSet getPayload = signedJWT.getJWTClaimsSet();
    	    String appleInfo = getPayload.toJSONObject().toJSONString();
    	    JSONObject payload = (JSONObject) JSONValue.parse(appleInfo);

            if (payload != null) {
                return payload;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

}

 

5. 통신을 위한 util

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class HttpClientUtils {

    private static Logger logger = LoggerFactory.getLogger(HttpClientUtils.class);
    private static ObjectMapper objectMapper = new ObjectMapper();

    public static String doGet(String url) {
        String result = null;
        CloseableHttpClient httpclient = null;
        CloseableHttpResponse response = null;
        Integer statusCode = null;
        String reasonPhrase = null;

        try {
            httpclient = HttpClients.createDefault();
            HttpGet get = new HttpGet(url);
            response = httpclient.execute(get);
            statusCode = response.getStatusLine().getStatusCode();
            reasonPhrase = response.getStatusLine().getReasonPhrase();
            HttpEntity entity = response.getEntity();
            result = EntityUtils.toString(entity, "UTF-8");
            EntityUtils.consume(entity);

            if (statusCode != 200) {
                logger.error(String.format("[doGet]http get url(%s) failed. status code:%s. reason:%s. result:%s", url, statusCode, reasonPhrase, result));
            }
        } catch (Throwable t) {
            logger.error(String.format("[doGet]http get url(%s) failed. status code:%s. reason:%s.", url, statusCode, reasonPhrase), t);
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
                if (httpclient != null) {
                    httpclient.close();
                }
            } catch (IOException e) {
                logger.error(String.format("[doGet]release http get resource failed. url(%s). reason:%s.", url, e.getMessage()));
            }
        }

        return result;
    }

    public static String doPost(String url, Map<String, String> param) {
        String result = null;
        CloseableHttpClient httpclient = null;
        CloseableHttpResponse response = null;
        Integer statusCode = null;
        String reasonPhrase = null;
        try {
            httpclient = HttpClients.createDefault();
            HttpPost httpPost = new HttpPost(url);
            httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded");
            List<NameValuePair> nvps = new ArrayList<>();
            Set<Entry<String, String>> entrySet = param.entrySet();
            for (Entry<String, String> entry : entrySet) {
                String fieldName = entry.getKey();
                String fieldValue = entry.getValue();
                nvps.add(new BasicNameValuePair(fieldName, fieldValue));
            }
            UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nvps);
            httpPost.setEntity(formEntity);
            response = httpclient.execute(httpPost);
            statusCode = response.getStatusLine().getStatusCode();
            reasonPhrase = response.getStatusLine().getReasonPhrase();
            HttpEntity entity = response.getEntity();
            result = EntityUtils.toString(entity, "UTF-8");

            if (statusCode != 200) {
                logger.error(String.format("[doPost]post url(%s) failed. status code:%s. reason:%s. param:%s. result:%s", url, statusCode, reasonPhrase, objectMapper.writeValueAsString(param), result));
            }
            EntityUtils.consume(entity);
        } catch (Throwable t) {
            try {
                logger.error(String.format("[doPost]post url(%s) failed. status code:%s. reason:%s. param:%s.", url, statusCode, reasonPhrase, objectMapper.writeValueAsString(param)), t);
            } catch (JsonProcessingException e) {
            }
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
                if (httpclient != null) {
                    httpclient.close();
                }
            } catch (IOException e) {
                try {
                    logger.error(String.format("[doPost]release http post resource failed. url(%s). reason:%s, param:%s.", url, e.getMessage(), objectMapper.writeValueAsString(param)));
                } catch (JsonProcessingException ex) {
                }
            }
        }
        return result;
    }

}

 

공개키 검증을 위한 utils

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class Key {
    private String kty;
    private String kid;
    private String use;
    private String alg;
    private String n;
    private String e;
}
import java.util.List;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class Keys {
    private List<Key> keys;

}

 

 

끝!!!!!!!!!!!!!!!!!!!! 

 

 

 

SpringBoot Session Timeout 설정을 해보겠습니다~ 

 

1. bean을 정의해둔 config 파일에 SessionListener 등록! 

@Bean
public HttpSessionListener httpSessionListener(){
	return new SessionListener();
}

 

2. SessionListener 파일 생성! 

import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;


public class SessionListener  implements HttpSessionListener{

	@Override
	public void sessionCreated(HttpSessionEvent session) {
		// 초단위로 설정된다. 테스트로 300(5분)으로 설정
        session.getSession().setMaxInactiveInterval(60*5);
	}

	@Override
	public void sessionDestroyed(HttpSessionEvent arg0) {
	}

}

 

3. 시간이 잘 설정되었는지 확인하기! Controller에 세션을 확장할 메소드를 하나 생성한다. 

 

@PostMapping("/extendSession")
ResponseBody
public boolean  extendSession() {
	logger.info("로그인 세션 연장");
	logger.info(request.getSession().getMaxInactiveInterval()); 
	logger.info("==============================");
      
    // 리턴값으론 로그인 여부를 리턴해주고있다. 본인코드에 맞춰 수정하기 
	return 로그인여부(true or false) ; 
}

 

이 메소드를 호출하면 아래 이미지처럼 설정된 시간이 초단위로 출력된다. 

* 설정한 값으로 변경이 안된다면 브라우저 캐시 지우고 다시 실행하자!! 

 

 

Session 만료를 사용자에게 알리기 위해 script 작업을 추가했다. 

main_layout.html 에 스크립트 추가~ ( 전역에서 사용되는 공간에 추가하면 됩니다. )

 

//<![CDATA[
        $(function () {
        	
            // 세션을 5분으로 설정해둬서 5로 설정함! 자기 시간에 맞춰 설정하기
            // 숫자 5는 상수값으로 처리해서 사용하면 좋겠다! 
         	var sessionTimeChecker = 5;
            
            // 1분에 한번씩 인터발을 이용해 남은 시간을 계산한다. 
            var sessionInterval = setInterval(checkSession, 60000);
            
            // 비동기 요청을 캐치해 시간을 리셋해준다. 
            $(document).ajaxStart(function () {
            	if(loginck) { // 로그인 여부 확인 후 세션 리셋 
	            	sessionTimeChecker = 5;
            	} else { 
                	// 로그인이 안된 상태라면 인터발 종료 
            		clearInterval(sessionInterval);	
            	}
             });
            
            
            
            // 1분에 한번씩 호출되는 함수로 1분이 남았을때 alert를 출력해준다. 
            function checkSession() { 
		sessionTimeChecker -= 1; // 1분씩 마이너스해서 남은 시간을 계산한다. 
		console.log("인터발 동작 " + sessionTimeChecker +"  " +  new Date() );
            	
                // 1분이 남은 경우 알림창 띄운다. 
                // 확인버튼을 눌렀으나 이미 로그아웃된 경우는 만료알림 문구를 출력하고 페이지를 새로고침한다.
            	if(sessionTimeChecker == 1) { 
            		if(confirm("장시간 사용되지 않아 1분뒤 자동으로 로그아웃됩니다. \n로그인 시간을 연장하시려면 확인 버튼을 클릭해주세요.")) { 
            			$.post("/extendSession").done(function(data) {
            				if(!data) {
            					alert("유효시간 만료로 로그아웃 되었습니다.");
            					location.reload(); // 리로드되면 로그인화면으로 자동 변경됨 
            				}
            			});
            		}
            	}	
            }
            
            
        });
        
        //]]>

 

* 여러번 반복되는 숫자들은 상수로 처리해서 하나만 수정하면 전부 수정되도록 코딩하는게 좋다.

아래 코드는 샘플로 값을 바로 확인하기 위해 그대로 입력해둠!  


Session을 이용한 로그인 처리~~~!! 

내부에서만 사용하거나 단순한 사이트로 굳이 Security 까지 추가할 필요가 없는 경우 사용하면 좋겠다. 

 

프로세스는 ~~~ 

모든 요청을 감시하는 Interceptor 생성! 

로그인에 성공하면 Session값을 설정하고 

요청이 올때마다 Interceptor가 Session 값이 있는지 확인 후 없다면 로그인페이지로 리턴시키걸로~ 

 

즈아~~ 시작! 

 

1.  Interceptor 생성 
요청이 들어오면 Session 지정해둔 Session 값을 확인하고 없다면 로그인페이지로 리다이렉트 처리한다. 

public class LoginInterceptor extends HandlerInterceptorAdapter {
	private static final Logger logger = LoggerFactory.getLogger(LoginInterceptor.class);
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		
		if( request.getSession().getAttribute(Consts.AUTHORIZED) == null  ) {
			response.sendRedirect(request.getContextPath() +"/login");
			return false;
		}
		return true;
	}
}

 

2. Interceptor 등록! 
모든 요청을 검사하도록  .addPathPatterns("/**") 로 설정 후 
Js, css 파일 등이 들어있는 resource 디렉토리 하위는 무시하도록 exclude 처리한다. 
로그인을 위한 요청도 exclude 처리~~!! 

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter  {
	
	 @Override
	    public void addInterceptors(InterceptorRegistry registry) {
	        registry.addInterceptor(new LoginInterceptor())
	                .addPathPatterns("/**")
	                .excludePathPatterns("/resources/**")
	                .excludePathPatterns("/login/**"); 
	    }
	 
}

 

 

3. Login/out 세션작업~~!! 

(위치는 각자 로그인/아웃 파일에 작성!)

// Login 
// 비밀번호, 잠금여부 등등 모든 조건이 맞아 로그인에 성공하게되면 Session 값을 추가해준다. 
request.getSession().setAttribute(Consts.AUTHORIZED, Consts.AUTHORIZED_VALUE);

// Logout
// 로그아웃시 Session 값을 제거해준다. 
request.getSession().removeAttribute(Consts.AUTHORIZED); 

 

4. Session 시간 설정~ 

초단위 결과를 안쓰고 굳이 (60 * 60 * 24)로 쓴 이유는~ 보고 바로 알수있게 하기 위함~~

 시간 같은건 이렇게 해주면 같이 일하는 사람 편하겠죠?  ( 암산이 빠르면 필요없겠지만.. 나는 좋음.. ^^! ) 

public class SessionListener  implements HttpSessionListener{

	@Override
	public void sessionCreated(HttpSessionEvent session) {
		session.getSession().setMaxInactiveInterval(60 * 60 * 24);// 24 시간으로 설정
	}
	@Override
	public void sessionDestroyed(HttpSessionEvent arg0) {
	}

}

 

 

Session 만료되면 자동으로 로그인페이지로 보내볼까??? 

시간 설정할때 보니 아래 있는 sessionDestroyed가 Session이 만료되면 호출된다고한다. 

요고요고 ~ ↓ 


	@Override
	public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
		 HttpSession session = httpSessionEvent.getSession();
		
	}


음~~

그럼 여기서 로그인 페이지로 redirect해주면 되겠군! 하고 생각할 수 있다. 

자~! 여기서 생각해봐야할건 

sessionDestroyed 메소드는 사용자의 요청으로 호출되는게 아니라

Session이 만료되었을때 호출 된다는 것이다. 

즉 ~ R e q u e s t 가 없 다 는 뜻 ! !  

요청(request)이 없으니 redirect 할곳도 없습니다~ ~!

혹시~~~~ redirect 처리한다고 시간낭비하지 않기로 ..  ^^ 

 

보통 Session 만료 검사를 Interceptor에서 필터링으로 처리하는데 

우린 이미 interceptor 처리하고있으니~ 추가안해도 될듯

 

그래도~~~!! 

Session 만료되기 전 뭔가 알림을  설정하고자 한다면 

스크립트단에서 

setInterval 같은 함수를 이용해 10분 간격으로 체크!~

이벤트가 생기면 체크값을 초기화~ 이벤드가 없어서 5번째까지 온다면 

"세션이 10분 남았음"  표기를 해주는것도 방법일 듯 하다. ~ 

+ Recent posts