빅데이터 Java 개발자 교육 [Spring - (SecurityConfig, JWUtil)]

Jun_Gyu·2023년 5월 16일
0
post-thumbnail

오늘은 먼저 기존에 사용했던 SecurityConfig를 수정하여

Student2 테이블의 정보로 로그인할때도 로그인과 로그아웃 이후의 페이지로 연결함과 동시에

세션에 사용자정보를 저장하는 기능을 사용할 수 있도록 해보겠다.

먼저 SecurityServiceImpl1 부터 생성해보도록 하겠다.

SecurityServiceImpl1

// 로그인에서 로그인버튼 => loadUserByusername으로 이메일 정보를 넘김
// student2 테이블과 연동되는 서비스
@Service
@Slf4j
@RequiredArgsConstructor
public class SecurityServiceImpl1 implements UserDetailsService {
    final String format = "SecurityServiceImpl => {}";

    final Student2Repository s2Repository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 이메일을 이용해서 student2 테이블에서 정보를 꺼낸 후 User타입으로 변환해서 리턴하면
        // 시큐리티가 비교후에 로그인 처리를 자동으로 수행함.
        log.info(format, username);
        Student2 obj = s2Repository.findById(username).orElse(null);
        if (obj != null) { // 이메일이 있는경우
            return User.builder()
                    .username(obj.getEmail()) // 아이디
                    .password(obj.getPassword()) // 암호
                    .roles("STUDENT2").build(); // 권한

        }
        // 아이디가 없는 경우는 User로 처리 (User는 정보 null 상태)
        return User.builder()
                .username("_")
                .password("_")
                .roles("_")
                .build();
    }

}

UserDetailsService 클래스에서 상속을 받아 @Override를 통해 로그인시 세션에 아이디와 암호, 권한이 저장될 수 있도록 직접 커스터마이징 해줬다.

이를 이용하여 로그인, 로그아웃을 수행하도록 SecurityConfig의 코드를 작성해보도록 하겠다.

SecurityConfig

final SecurityServiceImpl1 student2TableService; // student2테이블과 연동되는 서비스

@Bean   // 객체를 생성함. (자동으로 호출됨.)
    @Order(value = 1) // 순서를 먼저 설정
    public SecurityFilterChain filterChain1(HttpSecurity http) throws Exception {
        log.info("SecurityConfig => {}", "start filter chain2");

        // 127.0.0.1:9090/ROOT/student2/login.do
        // 127.0.0.1:9090/ROOT/student2/logout.do
        // 127.0.0.1:9090/ROOT/student2/loginaction.do
        // 위의 주소들만 필터함.
        http.antMatcher("/student2/login.do")
            .antMatcher("/student2/loginaction.do")
            .antMatcher("/student2/logout.do")
            .authorizeRequests().anyRequest().authenticated().and();
       
        // 로그인 처리
        http.formLogin()
            .loginPage("/student2/login.do")
            .loginProcessingUrl("/student2/loginaction.do")
            .usernameParameter("id") 
            .passwordParameter("password")
            .defaultSuccessUrl("/student2/home.do")
            .permitAll();

        // 로그아웃 처리        
        http.logout()
            .logoutUrl("/student2/logout.do")
            .logoutSuccessUrl("/home.do")
            .invalidateHttpSession(true)
            .clearAuthentication(true)
            .permitAll();

        http.userDetailsService(student2TableService);
        return http.build();
    }

로그인을 하면 /student2/loginaction.do 를 통해 홈화면이 뜨도록 처리함과 동시에 세션에 유저정보를 저장할 수 있도록 하였고,

로그아웃 이후에도 홈화면으로 가도록 설정을 해두었다.

(컨트롤러와 html은 기존의 방식대로 반복하는관계로 생략하도록 하겠다.)


JWUtil

로그인을 하고나면 회원 한명당 개인의 정보를 직접 전송하게되면 보안상 문제가 발생할 수 있다.

이를 해결하고자 아이디, 이름 비밀번호등의 정보들을 '토큰'으로 한번에 관리를 하게 되며, 이 토큰에서 각 페이지별 필요한 정보들을 추출하여 사용할 수 있다.

또한 토큰의 만료시간을 지정하여 일정시간 이후에 새로 토큰정보를 갱신하도록 하여 보안성을 강화할 수도 있다.

오늘 수업에서는 JWUtil을 생성하여 두가지의 메소드 기능을 실습해보고자 한다.

  • 토큰생성 메소드
  • 생성된 토큰을 검증하는 메소드
package com.example.restcontroller;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

import org.springframework.stereotype.Component;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

// 컨트롤러X, 서비스X, 엔티티X ... 
@Component
public class JwtUtil2 {

    private final String BASEKEY = "thereisnocowlevelthegatheringshowmethemoneypoweroverwhelming";

    // 토큰생성하는 메소드
    public String createJwt( String id, String name ) throws Exception {
        // 1. header정보
        Map<String, Object> headerMap = new HashMap<>();
        headerMap.put("typ","JWT"); //타입
        headerMap.put("alg", "HS256"); // hash알고리즘

        // 2. 토큰에 포함시킬 사용자 정보들..
        Map<String, Object> claimsMap = new HashMap<>();
        claimsMap.put("id", id);   // 아이디
        claimsMap.put("name", name); // 이름

        // 3. 토큰의 만료시간 ex) 2시간 => 현재시간 + 1000 * 60 * 60 * 2
        Date expiredTime = new Date();
        expiredTime.setTime( expiredTime.getTime() + 1000 * 60 * 60 * 8 );

        // 4. 키 발행
        byte[] keyBytes = DatatypeConverter.parseBase64Binary(BASEKEY);
        Key signKey = new SecretKeySpec(keyBytes, SignatureAlgorithm.HS256.getJcaName());

        // 1 ~ 4의 정보를 이용해서 토큰 생성
        JwtBuilder builder = Jwts.builder()
            .setHeader(headerMap)
            .setClaims(claimsMap)
            .setSubject("TEST")
            .setExpiration(expiredTime)
            .signWith(signKey, SignatureAlgorithm.HS256);
        
        // 토큰을 String 타입으로 변환
        return builder.compact();
    }
    
    // 토큰에 대해서 검증하고 데이터를 추출하는 메소드
    public boolean checkJwt(String token) throws Exception{
        try {
            // 1. key 준비
            byte[] keyBytes = DatatypeConverter.parseBase64Binary(BASEKEY);

            Claims claims = Jwts.parserBuilder()
                .setSigningKey(keyBytes)
                .build()
                .parseClaimsJws(token)
                .getBody();
            System.out.println( "추출한 아이디 => " + claims.get("id"));    
            System.out.println( "추출한 이름   => " + claims.get("name"));    
            return true;
        } 
        catch(ExpiredJwtException e1) {
            System.err.println("만료시간 종료" + e1.getMessage());
            return false;
        }
        catch(JwtException e2) {
            System.err.println("토큰오류" + e2.getMessage());
            return false;
        }
        catch(Exception e) {
            System.out.println("e1과 e2 오류 아닌 모든 오류" + e.getMessage());
            return false;
        }
    }
}

이를 활용하여 Rest방식으로 로그인해보도록 하자.

RestStudent2Controller

final JwtUtil2 jwtUtil2; // 컴포넌트 객체 생성
    final Student2Repository s2Repository; // 저장소 객체 생성
    BCryptPasswordEncoder bcpe = new BCryptPasswordEncoder(); // 암호화 객체
    
    //127.0.0.1:9090/ROOT/api/student2/login.json
    @PostMapping(value="/login.json")
    public Map<String, Object> loginPOST(@RequestBody Student2 student2) {
    Map<String, Object> retMap = new HashMap<>();
        try {
            // 1. 이메일, 암호 전송 확인
            log.info("{}", student2.toString());

            // 2. 이메일을 이용해서 정보를 가져옴.
            Student2 retStudent2 = s2Repository.findById(student2.getEmail()).orElse(null);

            // 3. 실패시 전송할 데이터
            retMap.put( "status", 0 );

            // 4. 암호가 일치하는지 확인 => 전송된 hash되지 않은 암호와 DB에 해시된 암호 일치 확인
            if(  bcpe.matches( student2.getPassword(), retStudent2.getPassword()) ) {
                retMap.put( "status", 200 );
                retMap.put( "token", jwtUtil2.createJwt(retStudent2.getEmail(), retStudent2.getName() ) );
            }
        } catch (Exception e) {
            e.printStackTrace(); 
            retMap.put( "status", -1 );
            retMap.put( "error", e.getMessage() );
        }
        return retMap;
    }

postman에서 위의 json 주소를 통해 기존 정보로 로그인해서 토큰이 잘 생성되었는지 확인해보도록 하겠다.

위와같이 아이디와 비밀번호를 입력하면

토큰이 정상적으로 발행되었음을 확인할 수 있다.

회원정보 수정 기능에 토큰을 이용할 수도 있다.

RestStudent2Controller

// 회원탈퇴, 비번변경, 회원정보수정 ... 로그인이 되어야 되는 모든것.
// 회언정보수정 => 토큰을 주세요. 검증해서 성공하면 정보수정을 진행하는 방식.
    @PostMapping(value = "/update.json")
    public Map<String, Object> updatePOST(@RequestHeader(name = "token") String token) {
        Map<String, Object> retMap = new HashMap<>();
        try {

            // 1. 토큰을 받아서 출력
            log.info("{}", token);

            // 2. 실패시 전달값
            retMap.put("status", 0);

            // 3. 토큰을 검증
            if ( jwtUtil2.checkJwt(token) == true ) {
                // 3, 정보를 수정함.
                retMap.put("status", 200);
            }

        } catch (Exception e) {
            e.printStackTrace();
            retMap.put("status", -1);
            retMap.put("error", e.getMessage());
        }
        return retMap;
    }

postman의 Headers에서 key값을token, Value값을 앞서 발행한 토큰값을 넣고

변경하고자 하는 계정의 아이디값과 비밀번호값을 넣으면

성공적으로 접근했음을 확인할 수 있다. 이후에 기능을 추가하여 수정하고자 하는 정보들을 입력받아 정보들을 수정할수도 있다.

profile
시작은 미약하지만, 그 끝은 창대하리라

0개의 댓글