반응형

사이트나 서버를 HTTPS로 호스팅하기위해서는 SSL 인증서가 필요하다.

인증서의 종류나 인증서를 발급해주는기관 또 인증서별로 지원을 해주는기능들이다르다.  

SSL인증서가 무엇인지 알아보자 

 

 

보안인증서

  • 웹 서버와 사용자 pc간에 전송되는 모든 정보를 암호화하여 안전하게 전송하는 디지털 인증서
  • 스니핑으로 인한 개인정보 유출 방지를 위해서 필수적인 보안수단
  • 데이터 변조를 방지

 

인증서중에는 서브도메인이나 멀티도메인 강제암호화를 지원하는 상품이있다.

 

서브도메인 인증서 - 와일드카드 SSL인증서

  • 하나의 인증서로 서브도메인에서도 사용이가능한 인증서 
  • ex) *.example.com 형식으로 모든 서브도메인을 커버한다.

멀티도메인 인증서 - 다중 도메인 인증서

  • 도메인 주소가 서로 다른경우 인증서 하나로 적용할수있는 멀티도메인 인증서.
  • 다중도메인 인증서는 SAN(Subject AlternativeName)필드를 사용하여 다양한 도메인을 지정할수있다.

강제 암호화 - SGC (Server Gated Cryptography)

  • 클라이언트측의 낮은 암호화를 강제적으로 높은bit의 암호화로 올린다
  • 낮은 버전의 브라우저도 상관없이 높은 수준의 암호화를 요구하는 페이지에 접근이 가능하다.
  • 브라우저 종류에 상관없이 접근이 가능해야한다면 해당 상품을 이용해야한다.

보안인증서 인증 절차

  1. csr 생성 Certificate Signing Request : 도메인 이름.회사정보 , 공개키 등을 포함한 CSR파일을 생성
  2. 인증 기관 선택 CA - Certificate Authority
  3. csr제출 : 생성한 CSR파일을 선택한 인증 기관에 제출합니다.
  4. 도메인 소유권 확인  - 이메일인증, 파일인증, dns인증
  5. 인증서 발급 : 인증 기관이 소유권 확인을 완료하면 SSL인증서를 발급합니다.
  6. 인증서 설치 : 웹서버의 설정파일을 수정하여 발급된 인증서를 설치하고, HTTPS 를 활성화한다. 주로 443번포트를 사용합니다.

* https로 도메인에접근하면 포트를 별도로 명시하지않아도 자동으로 서버의 443번포트에 연결된다.

 

인증서가 가진 정보

  • 주체 - 발급된 도메인이름
  • 발급자 - CA정보: 발급기관
  • 서버 공개키
  • TLS 버전 - 암호화 프로토콜
  • 세션정보
  • 검증결과

 

반응형

'공부 임시 저장소' 카테고리의 다른 글

도메인 레코드란? DNS Record  (1) 2024.10.13
도메인이란? DNS  (1) 2024.10.13
스프링 부트 - spring-security-jwt  (0) 2024.06.25
스프링부트 - aws s3파일저장  (0) 2024.06.25
스프링부트 - 파일 저장  (0) 2024.06.24
반응형

DNS Record

도메인 이름에 대한 다양한 정보를 저장하며 클라이언트가 도메인을 입력할때 

이 정보를 받아서 적절한 처리를 한다.

 

일반적으로 사용되는 DNS 레코드에는

A 및 AAAA 레코드,

CNAME,

DNAME 및

ALIAS 레코드,

CAA 레코드,

CERT 레코드,

MX 레코드,

SOA 레코드,

NS 레코드,

PTR 레코드,

SPF 레코드,

SRV 레코드 및 TXT 레코드가 포함됩니다.

이러한 각 레코드는 고유한 기능을 가지고 있으며 각각의 레코드를 이해하는 것은 DNS 시스템이 제대로 작동하는 데 중요한 부분입니다. 

 

A 레코드

  • A레코드는 도메인이름을 직접적으로 서버 아이피 주소와 연결해주는 레코드이다.
  • A레코드에 입력한 도메인주소로 접근하면 IP주소로 연결된다.

 

CNAME - Canonical name

  • 도메인을 별명과 연결한다 . 
  • IP 주소가 유동적으로 변하는 서버의 경우 A 레코드로 작성하면 IP가 변경될때마다 A레코드를 다시 작성해주어야한다.
  • CNAME으로 A레코드의 도메인 주소값을 입력하면 IP값이 변경되어도 레코드를 수정해주지 않아도 접근이 가능하다.

 

예를 들어 CNAME으로 www.soolae-server.shop을 을 작성하고

a레코드는 soolae-server.shop이고 

a레코드에서 가리키는 IP는 유동적으로 변경된다고 했을때 

 

사용자가 www.soolae-server.shop  인 CNAME으로 접근하고

CNAME은 A레코드값과 연결되고

A레코드가 가진 IP값을 가지고 온다.

 

MX레코드 - Mail Exchange Record

  • 여러개의 MX 레코드를 생성할수있고 우선순위를 설정할수있다. Priority
  • 이메일 서버는 우선순위가 낮은 레코드를 사용하려고 시도한다. ex) 우선순위가  1 , 2, 3 인경우 1
  • 메일 전송시 해당 도메인의 mx 레코드로 전송된다.

 

ex)

A레코드에 메일을받을 서버의 IP를 연결해서 생성  / mail.도메인주소

MX레코드를 생성 / 도메인주소 10 mail.도메인주소

 

이렇게 입력하면 해당도메인주소로 이메일이 전송될때 donghyeon@도메인이름

mx레코드를 요청하고 네임서버는 우선순위가높은 mx레코드를 보내주고

연결된 도메인인 mail.도메인 으로 메일이 전송된다. 

그럼 이메일을 mail.도메인 에 연결된 ip주소를 가진 서버에서 처리하게된다.

 

TXT레코드 

  • 텍스트 레코드는 텍스트 문자열을 넣을수있는 레코드 이다.
  • 도메인이나 하위 도메인과 관련된 텍스트 정보를 저장한다.
  • SPF 레코드와 이메일 확인 레코드를 저장할 수 있다. 

 

SPF레코드 - Sender Policy Framework

  • 도메인 에서 이메일을 보낼수있도록 승인된 서버를 나열하는 텍스트 레코드
  • SPF레코드는 TXT레코드를 작성하는 한 방법이며 이메일 인증을 위해서 사용하는 레코드이다.

 

ex)

도메인 / TXT / "v=spf1 mx include:mailplug.com ~all"​

v=spf1 : SPF레코드의 버전

mx : 도메인의 메일 서버를 허용

include:mailplug.com : mailplug.com 에서 메일을 보낼수있는 서버를 포함 / mailplug.com의 spf 정책을 이용한다

~all : 메일전송정책 해당 SPF정책을 따르지않는 이메일을 유사스팸으로 간주하고 수신서버에서 판단하여 스팸으로 분류.

-all : 엄격한거부정책 SPF를 따르지않는 이메일을 모두 거부.

 

SRV레코드

_admin._tcp.soolae-server.shop. 300 IN SRV 1 10 8081 soolae-server.shop.

 인스턴트 메시징 등의 특정 서비스에 대한 포트를 지정

해당 IP주소의 포트 정보도 포함

일부 인터넷 프로토콜은 SRV레코드를 작성해야 동작할수있다.
서비스 지정: SRV 레코드는 특정 서비스(예: SIP, XMPP 등)가 사용하는 포트를 지정합니다.
프로토콜: SRV 레코드는 어떤 프로토콜을 사용하는지를 명시합니다. 예를 들어, `_sip._tcp.example.com`은 SIP 프로토콜을 TCP를 통해 사용하는 서비스임을 나타냅니다.
우선순위 및 가중치: SRV 레코드는 우선순위와 가중치를 설정할 수 있어, 여러 대의 서버가 있을 경우 어떤 서버로 연결할지를 결정하는 데 도움을 줍니다.

 

연결과정

  1. 도메인 쿼리: 클라이언트가 특정 도메인에 대해 SRV 레코드를 조회합니다.
  2. SRV 레코드 응답: SRV 레코드는 해당 서비스에 대한 포트와 IP 주소를 제공합니다.
  3. A 레코드 조회: 만약 SRV 레코드가 도메인 이름을 포함하고 있다면, 클라이언트는 해당 도메인에 대한 A 레코드를 조회하여 실제 IP 주소를 확인합니다.
  4. 서비스 연결: 클라이언트는 SRV 레코드에서 제공된 포트와 A 레코드에서 제공된 IP 주소를 사용하여 해당 서비스에 연결합니다.

DNS SOA레코드 

도메인에 관련된 설정정보 레코드

  • 도메인 영역에대한 중요한 정보를 저장
  • 권한과 관리정보를 포함
  • 네임서버: 해당 도메인의 주요 네임서버 정보를 포함
  • 관리자이메일: 네임서버 관리자 이메일 주소를 나타낸다
  • 시리얼너버: 도메인 설ㅈ엉의 버전을 나타내며 , 업데이트시 증가
  • 리프레시 주기: 주 서버가 보조 서버로 데이터를 갱신하는 주기를 나타낸다.
  • 재시도 주기 : 주서버와 연결 실패시 보조 서버가 재시도하는 주기
  • 만료시간 : 캐시된 정보를 폐기하기 전에 유지하는 시간.
  • 최소 TTL값: 캐시된 DNS레코드의 기본 유효 시간을 설정

TTL - Time To Live

패킷이 라우터에 의해 폐기될때 까지 네트워크 내부에 존재하도록 설정된 시간 또는 홉(Hop) 의 양을 나타낸다.

- 홉(Hop) : 패킷이 네트워크의 장비를 지나갈때 마다 홉이 하나씩 증가, 홉을 통과할때마다 패킷의 생존 시간이 감소

 

DNS TTL

  1. dns레코드가 생성될때 TTL값을 설정한다.
  2. 클라이언트가 dns서버에 도메인이름을 조회할때 해당 dns서버는 요청된 레코드를 찾는다.
  3. dns서버는 레코드를 반환할때 TTL값을 함께 제공한다. 이 TTL값은 클라이언트나 다른 dns서버가 해당 레코드를 얼마나 오랫동안 저장할지를 결정한다.

 

 

 

 

 

반응형

'공부 임시 저장소' 카테고리의 다른 글

SSL 보안인증서란?  (0) 2024.10.13
도메인이란? DNS  (1) 2024.10.13
스프링 부트 - spring-security-jwt  (0) 2024.06.25
스프링부트 - aws s3파일저장  (0) 2024.06.25
스프링부트 - 파일 저장  (0) 2024.06.24
반응형

 

도메인

도메인은 영문이나 한글로된 인터넷주소이다. 

네이버나 구글 같은 웹사이트에 접근하기위해서는 아이피 주소를 알아야하는데. 
사용자가 이 아이피주소를 모두 기억할수없기때문에 문자로된 도메인주소를 연결해서 
도메인주소로접근했을때 해당하는 아이피주소를 보내줘서 웹서버에 접근이 가능해진다.

 

도메인 형식은 이름, 확장자, 서브도메인 으로 구성된다 

예를 들어 내가 구매한도메인인 soolae-server.shop 은 

soolae-server 가 이름이고 

.shop이 확장자이다 확장자는 TLD 라고도 부르며 TopLevelDomain의 약어이다.

 

 

서브도메인

이렇게 하나의 도메인이있으면 도메인의 앞에 호스트네임을 붙여서 용도별로 도메인을 사용할수있다

soolae-server.shop 이라는 도메인으로 메일서버를 호스팅한다면

mail.soolae-server.shop

블로그를 호스팅한다면 

blog.soolae-server.shop 으로

하나의 도메인으로 역할에 따라 다양하게 사용할수있다.

하나의 도메인값에 여러 하위도메인이 속해있다.

 

DNS

인터넷 에서 도메인 이름과 IP 주소를 변환해주는 시스템

사용자가 웹사이트의 주소를 입력하면 DNS 가 IP 주소로 변환하여 해당 서버에 접근하게 해준다.

 

네임서버

네임서버는 DNS시스템의 일부로 특정도메인의 DNS레코드를 저장하고 관리하는 서버

네임서버는 도메인 이름을 IP주소로 변환하는 작업을 담당한다.

 

반응형

'공부 임시 저장소' 카테고리의 다른 글

SSL 보안인증서란?  (0) 2024.10.13
도메인 레코드란? DNS Record  (1) 2024.10.13
스프링 부트 - spring-security-jwt  (0) 2024.06.25
스프링부트 - aws s3파일저장  (0) 2024.06.25
스프링부트 - 파일 저장  (0) 2024.06.24
반응형

 

Spring Security 

스프링 시큐리티는 간단하게 인증과 인가, 즉 로그인할수있는 기능을 제공해주는 라이브러리이다.

스프링 시큐리티를 이용해 만들어져있는기능으로 로그인, 권한에 따른 접근 을 간편하게 구현할수있다.

 

https://velog.io/@hope0206/Spring-Security-%EA%B5%AC%EC%A1%B0-%ED%9D%90%EB%A6%84-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%97%AD%ED%95%A0-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0

스프링 시큐리티의 동작에대해 잘 설명되어있는 글이다. 여기에 더해서 jwt토큰을 더해 클라이언트에서 사용할 로그인 기능을 구현할 생각이다. 

 

 JWT(json web token)

jwt토큰을 이용하면 필요한 정보를 암호화해서 보관하고 가져와서 사용할수있다.

 

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
    implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
    implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
}

 

jpa , security , swagger , jwt 를 사용하기위한 라이브러리들을 불러와서 사용한다.

 

SecurityConfig

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

    http
            // CORS(Cross-Origin Resource Sharing) 설정
            .cors((cors) -> cors.configurationSource(new CorsConfigurationSource() {
                @Override
                public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                    CorsConfiguration corsConfiguration = new CorsConfiguration();
                    corsConfiguration.setAllowedOrigins(Collections.singletonList("*")); // 모든 출처 허용
                    corsConfiguration.setAllowedMethods(Collections.singletonList("*")); // 모든 HTTP 메서드 허용
                    corsConfiguration.setAllowCredentials(true); // 자격 증명 허용
                    corsConfiguration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type")); // 허용된 헤더
                    corsConfiguration.setExposedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type")); // 노출된 헤더
                    return corsConfiguration;
                }
            }))
            // CSRF 토큰 비활성화
            .csrf((auth) -> auth.disable())
            // 폼 로그인 비활성화
            .formLogin((auth) -> auth.disable())
            // HTTP 기본 인증 비활성화
            .httpBasic((auth) -> auth.disable())
            // 권한 설정
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/login", "/", "/join").permitAll() // 로그인, 홈, 회원가입 페이지 접근 허용
                    .requestMatchers("/imgs/**").permitAll() // 이미지 파일 접근 허용
                    .requestMatchers("/admin/**","/swagger-ui/**").hasRole("ADMIN") // 관리자 권한 필요
                    .requestMatchers("/user/**").hasRole("USER") // 사용자 권한 필요
                    .anyRequest().authenticated() // 나머지 요청은 인증 필요
            )
            // 세션 관리 설정
            .sessionManagement((session) -> session
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 무상태 세션 정책
            )
            // JWT 필터 추가
            .addFilterBefore(new JwtFilter(jwtUtil), LoginFilter.class)
            // 로그인 필터 추가
            .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil),
                    UsernamePasswordAuthenticationFilter.class)
    ;

    return http.build();
}

 

jwt 인증방식의 로그인을 사용하기위해서 httpBasic ,sessionManagement 는 비활성화 해주어야한다.

기존 http인증방식이아닌 jwt로 인증을 해주어야하기 때문이다. 

 

CustomUserDetails

@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {

    private final UserEntity userEntity;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(
                new GrantedAuthority() {
                    @Override
                    public String getAuthority() {
                        return String.valueOf(userEntity.getRole());
                    }
                }
        );
        return collection;
    }

    @Override
    public String getPassword() {
        return userEntity.getPassword();
    }

    @Override
    public String getUsername() {
        return userEntity.getEmail();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

CustomUserDetailService

@Service
@RequiredArgsConstructor
public static class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity byEmail = userRepository.findByEmail(username).orElseThrow(IllegalArgumentException::new);
        return new CustomUserDetails(byEmail);
    }
}

 

사용자가 로그인폼에 아이디와 비밀번호를 입력하고 로그인을하게되면

  1. UsernamePasswordAuthenticationToken 객체가 생성
  2. AuthenticationManager가 이 토큰을 받아 인증을 시도
  3. AuthenticationManager는 내부적으로 AuthenticationProvider를 호출하여 인증 로직을 수행
  4. AuthenticationProvider는 CustomUserDetailsService의 loadUserByUsername 메서드를 호출하여 사용자 정보를 조회
  5. loadUserByUsername메서드 에서는 사용자 이메일로 UserEntity를 찾아 CustomUserDetails 객체를 생성합니다.
  6. CustomUserDetails 객체에는 사용자의 이메일, 비밀번호, 권한 등의 정보가 포함
  7. AuthenticationProvider는 CustomUserDetails 객체에서 가져온 비밀번호와 사용자가 입력한 비밀번호를 비교하여 인증 여부를 판단 인증이 성공하면 인증된 Authentication객체를 반환. 실패하면 Exception을 던진다.

이제 인증을 한후 인증정보를 jwt토큰으로 만들어 클라이언트에 보내주어야한다.

 

JwtUtil

@Component
public class JwtUtil {
    private SecretKey secretKey;
    private String EMAIL = "email";
    private String ROLE = "role";

    public JwtUtil(@Value("${spring.jwt.secret}")String secret) {
        this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), SIG.HS256.key().build().getAlgorithm());
    }

    public String getUsername(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get(EMAIL, String.class);
    }

    public String getRole(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get(ROLE, String.class);
    }

    public Boolean isExpired(String token) {
        JwtParser parser = Jwts.parser().verifyWith(secretKey).build();
        Jws<Claims> claimsJws = parser.parseSignedClaims(token);
        Claims payload = claimsJws.getPayload();
        Date expiration = payload.getExpiration();
        boolean before = expiration.before(new Date());
        return before;
    }

    public String createJwt(String username, String role, Long expiredMs) {
        long now = System.currentTimeMillis();
        Date expiration = new Date(now + expiredMs);
        return Jwts.builder()
                .claim(EMAIL, username)
                .claim(ROLE, role)
                .issuedAt(new Date(now))
                .expiration(expiration)
                .signWith(secretKey)
                .compact();
    }
}

jwt 토큰을 만들고 읽을 수있는 util 클래스를 우선생성해준다.

 

LoginFilter - 로그인 시 동작할 필터

@RequiredArgsConstructor
@Slf4j
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;
    private final JwtUtil jwtUtil;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        // 1. 클라이언트가 보낸 username과 password를 추출합니다.
        String userName = obtainUsername(request);
        String password = obtainPassword(request);

        // 2. 추출한 username과 password로 UsernamePasswordAuthenticationToken을 생성합니다.
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, password, null);

        // 3. AuthenticationManager에게 인증을 요청합니다.
        return authenticationManager.authenticate(authenticationToken);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        // 4. 인증이 성공하면 CustomUserDetails 객체를 가져옵니다.
        CustomUserDetails customUserDetails = (CustomUserDetails) authResult.getPrincipal();

        // 5. CustomUserDetails에서 username과 role을 추출합니다.
        String username = customUserDetails.getUsername();
        String role = getRole(customUserDetails);

        // 6. JwtUtil을 사용하여 JWT 토큰을 생성합니다.
        String token = jwtUtil.createJwt(username, role, 60 * 60 * 1000L);

        // 7. 생성한 JWT 토큰을 응답 헤더에 추가합니다.
        response.addHeader("Authorization", "Bearer " + token);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException failed) throws IOException, ServletException {
        // 8. 인증이 실패하면 응답 상태 코드를 401(Unauthorized)로 설정합니다.
        response.setStatus(401);
    }

    private String getRole(CustomUserDetails customUserDetails) {
        // 9. CustomUserDetails에서 사용자의 권한(role)을 추출합니다.
        return customUserDetails.getAuthorities().iterator().next().getAuthority();
    }
}

 

로그인 할경우 해당 필터를 통과해 attemptAuthentication메서드로 인증을 시도하고 인증을 성공하는경우 

successfulAuthentication메서드를 실행하고 jwt토큰을 발급해서 응답헤더에 토큰을 붙여서 응답을 보내준다.

실패하는경우에는 unsuccessfulAuthentication을 실행시키고 응답에 401에러를 보내주게된다.

 

이제 이렇게하면 로그인을 하고 응답으로 jwt토큰을 받아서 클라이언트에 저장할수있다.

 

이제 다시 서버에서 jwt토큰을 읽어서 해당 유저의 인증된 정보를 받아서 보관하는 필터를 만들어줘야한다.

 

JwtFilter

@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        // 1. 요청 헤더에서 Authorization 헤더를 가져옵니다.
        String authorization = request.getHeader("Authorization");

        // 2. Authorization 헤더가 없거나 "Bearer" 로 시작하지 않으면 토큰이 없는 것으로 간주하고 다음 필터로 넘어갑니다.
        if (authorization == null || !authorization.startsWith("Bearer")) {
            log.error("token null");
            filterChain.doFilter(request, response);
            return;
        }

        // 3. Authorization 헤더에서 실제 토큰 값을 추출합니다.
        String token = authorization.split(" ")[1];

        // 4. 토큰의 만료 시간을 확인합니다. 만료된 경우 다음 필터로 넘어갑니다.
        if (jwtUtil.isExpired(token)) {
            log.error("token expired");
            filterChain.doFilter(request, response);
            return;
        }

        // 5. 토큰이 유효한 경우 setAuthentication 메서드를 호출하여 인증 정보를 SecurityContextHolder에 설정합니다.
        setAuthentication(token);

        // 6. 다음 필터로 요청을 전달합니다.
        filterChain.doFilter(request, response);
    }

    private void setAuthentication(String token) {
        // 7. 토큰에서 사용자 이름과 역할 정보를 추출합니다.
        String username = jwtUtil.getUsername(token);
        String role = jwtUtil.getRole(token);

        // 8. 추출한 정보로 UserEntity 객체를 생성합니다.
        UserEntity user = UserEntity.builder()
                .email(username)
                .password("")
                .role(UserRole.valueOf(role))
                .build();

        // 9. UserEntity 객체를 이용하여 CustomUserDetails 객체를 생성합니다.
        CustomUserDetails customUserDetails = new CustomUserDetails(user);

        // 10. CustomUserDetails 객체와 권한 정보를 이용하여 UsernamePasswordAuthenticationToken 객체를 생성합니다.
        Authentication authenticationToken = new UsernamePasswordAuthenticationToken(
                customUserDetails, null, customUserDetails.getAuthorities());

        // 11. 생성한 인증 토큰을 SecurityContextHolder에 설정합니다.
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }
}

 

서버에 api요청을 하는경우 이 필터를 통과해서 토큰이 있는경우 setAuthentication으로 SecurityContextHolder에 인증된 값을 세팅해준다. 이러면 컨트롤러에서도 인증된 유저의 정보를 사용할수도있고 인증되지않은 유저, 토큰을 가지지않은 유저가 api에 접근할때에도 통과시키지않을수있다.

 

보통 클라이언트와 서버를 나눠서 사용할때는 jwt토큰방식을 사용하고 

서버에서 프론트까지 모두 만드는경우에는 세션방식을 많이 이용한다고 한다.

 

jwt토큰방식은 stateless하다는것이 장점이라고하는데 토큰을 보내주고난다음에는 서버에서는 토큰을 보관하고있지않고 요청이 올때마다 토큰을 읽어서 인증하기때문에 서버에 부담이 적다고 한다.

 

공부를 더 해서 잘 이해하게되면 나중에 이 글도 버전업으로 더 잘써보겠다.

반응형

'공부 임시 저장소' 카테고리의 다른 글

도메인 레코드란? DNS Record  (1) 2024.10.13
도메인이란? DNS  (1) 2024.10.13
스프링부트 - aws s3파일저장  (0) 2024.06.25
스프링부트 - 파일 저장  (0) 2024.06.24
채팅앱 - 채팅 미전송 메시지 처리  (0) 2024.06.24
반응형

기존에 로컬에 이미지를 저장하던 형식에서 클라우드인 aws 의 s3에 올리는 방법으로 변경했다.

s3는 aws에서 지원해주는 파일을 저장해주는 서비스인데 간단하게 네이버박스나 구글드라이브 정도를 생각하면 될것같다. 

 

s3 버켓 생성 

aws의 s3를 이용하려면 s3버켓을 생성해주어야한다.  

aws에 접속해서 s3를 검색하고 페이지에서 버킷 만들기를 선택한다.

여기서 버켓의 이름을 만들고 옵션은 모두 기본으로 설정해주었다. 

버켓의 이름은 유일해야하는 조건이 있어서 흔한 mybucket같은 이름은 지정할수 없었다.

 

버켓을 생성하고나면 버켓에 접근할 권한이 필요하다.

IAM으로 들어가서 s3에 접근할 사용자를 생성해준다.

 

사용자 이름을 설정하고 권한에 AmazonS3FullAccess를 설정해준다.

이후 해당 사용자로 들어가서 액세스키를 만들면 s3에 접근할 키 생성도 끝난다. 

액세스 키를 만들고나서는 추후에 다시 볼수없기때문에  파일로 저장하는걸 추천한다.

 

이제 스프링 부트에서 s3를 사용하기위한 설정을 해보자.

 

build.gradle 에 라이브러리 추가

implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

 

 

application.yml

cloud:
  aws:
    s3:
      bucket: "버킷이름"
    stack.auto: false
    region.static: ap-northeast-2
    credentials:
      accessKey: "${S3_ACCESS_KEY}"
      secretKey: "${S3_SECRET_KEY}"

 

만약 버킷안에 폴더를 만들고 그안에 저장하고싶다면 버킷이름/폴더이름 으로 접근하면 된다.

 

S3Config

@Configuration
public class S3Config {
    @Value("${cloud.aws.credentials.access-key}")
    private String accessKey;
    @Value("${cloud.aws.credentials.secret-key}")
    private String secretKey;
    @Value("${cloud.aws.region.static}")
    private String region;

    @Bean
    public AmazonS3Client amazonS3Client(){
        return (AmazonS3Client) AmazonS3ClientBuilder.standard()
                .withCredentials(
                        new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey,secretKey))
                )
                .withRegion(region)
                .build();
    }
}

 이렇게 S3Config까지 설정해주면 AmazonS3 클래스로 s3에 접근이 가능해진다.

 

S3UploadService

@Service
@RequiredArgsConstructor
public class S3UploadService {

    private final AmazonS3 amazonS3;
    @Value("${cloud.aws.s3.bucket}")
    private String bucket;

    public void saveFile(MultipartFile multipartFile, String userName) throws IOException {
        String fileName = createFileName(multipartFile);
        ObjectMetadata metadata = getObjectMetadata(multipartFile);
        amazonS3.putObject(bucket, fileName, multipartFile.getInputStream(), metadata);
    }

    public void deleteFile(String fileName) {
        amazonS3.deleteObject(bucket, fileName);
    }

    public FileEntity updateFile(String oldFileName, MultipartFile newFile, String uploader) throws IOException {
        //기존이미지를 삭제
        deleteFile(oldFileName);
        //새로운 이미지를 추가
        return saveFile(newFile, uploader);
    }
    
    public String getPath(String fileName){
    	return amazonS3.getUrl(bucket, fileName).toString();
    }

    private static ObjectMetadata getObjectMetadata(MultipartFile multipartFile) {
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentLength(multipartFile.getSize());
        objectMetadata.setContentType(multipartFile.getContentType());
        return objectMetadata;
    }
}

 

파일저장

파일을 저장하기위해서는 버켓이름,파일의이름,파일의 인풋스트림,메타데이터가 필요하다.

메타데이터는 파일의 크기와 컨텐츠의 타입이 들어가게된다. 

putObject로 파일을 저장하게되면 설정한 이름으로 파일이잘 저장되는걸 확인할수있다. 

 

파일삭제

삭제는 간단하게 어떤 버켓의 어떤 파일을 삭제할지에 대한 정보만 있으면된다. 

deleteObject에 버켓과 파일의 이름을 파라미터로 넣으면 해당하는 파일이 삭제된다.

 

이미지에 접근

파일을 저장하면 해당 파일이나 이미지에 접근을 해야한다.

나의 경우는 이미지를 올리고 페이지에서 띄워줘야하는데 어떤 경로로 접근해야할지 알수없었다.

getUrl 을 사용하면 해당 파일에 접근할수있는경로를 리턴해준다. 

이 경로로 바로 접근하려고하면 에러 페이지를 띄워준다.

이를 해결하기 위해서는 cloudFront로 s3이미지를 배포해서 접근하는 방법이 필요하다고 한다.

cloudFront를 생성할때 domain을 클릭하면 내가만든 s3버켓이 나온다 해당 s3버켓을 선택하고 

보안보호만 비활성화로 선택해준다음 나머지 옵션은 모두기본으로 두었다. 그후에 배포를 생성해준다.

 

이후에도  access denied가 뜰텐데 s3bucket의 정책이 접근권한이 aws사용자에게만 열려있기때문이다. 

접근하려는 s3 bucket으로 다시 이동하여 권한으로 들어가면 버킷정책이있다. 여기에서 옵션을 지정해주면된다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AddPerm",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::버켓이름/*"
        }
    ]
}

 

나는 해당 버켓의 모든 폴더에 접근가능하도록 열었는데 특정폴더만 열어놓을수도있다.

이렇게 하면 getUrl로 얻어지는 주소로 저장한 파일에 접근이 가능하다.

 

https://s3.ap-northeast-2.amazonaws.com/cokeholics-bucket/coctail_library_imgs/18153d0d-63be-4bec-834b-8a54f4566dfb_martini.jpg

 

반응형
반응형

프로젝트를 만들다가 이미지를 저장하고 불러와서 사용할일이생겼다. 

처음에는 resource폴더에 이미지를 저장했는데 프로젝트의 resources 폴더는 static한 폴더라서 저장을하고 다시 빌드를 해야 해당이미지를 불러올수있었다. 그래서 프로젝트에 이미지 저장용 폴더를 만들고 저장을 하고 불러오려고하니 에러가 발생했다. 그래서 찾아보니 파일의 위치를 받아서 엔드포인트로 접근할수있게 해주는 config 설정이 있었다. 

 

@Configuration
@Slf4j
public class WebConfig implements WebMvcConfigurer {
public static String UPLOAD_PATH = "file:///C:/Users/kimad/Desktop/JavaPortpolio/chatApp/chat_app_imgs/";
	
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/imgs/**")
                .addResourceLocations(UPLOAD_PATH);
    }
}

 

이렇게 작성해주면 해당 경로에있는 파일을 imgs/ 파일이름 으로 접근이 가능해진다.

 

@Service
@RequiredArgsConstructor
@Slf4j
public class UploadService {

    //외부폴더
    public static String UPLOAD_PATH = "file:///C:/Users/kimad/Desktop/JavaPortpolio/chatApp/chat_app_imgs/";
    private String uploadPath = "C:/Users/kimad/Desktop/JavaPortpolio/chatApp/chat_app_imgs/";

    public static String createFileName(MultipartFile file){
        return UUID.randomUUID() + "_" + file.getOriginalFilename();
    }

    public void saveFile(MultipartFile file,String fileName) {
        try {
            file.transferTo(new File(uploadPath, fileName));
        } catch (IOException e) {
            log.error("Error occurred while saving file: {}", e.getMessage(), e);
        }
    }

    // 파일 삭제 메서드
    public void deleteFile(String fileName) {
        try {
            File file = new File(uploadPath, fileName);
            if (file.delete()) {
                log.info("{} 파일 삭제 성공", fileName);
            } else {
                log.warn("{} 파일 삭제 실패", fileName);
            }
        } catch (Exception e) {
            log.error("Error occurred while deleting file: {}", e.getMessage(), e);
        }
    }

    public void updateFile(String oldFileName,MultipartFile newFile,String newFileName){
        if(!newFile.isEmpty()){
            //새로운 이미지가 업데이트 되는경우

            //기존이미지를 삭제
            deleteFile(oldFileName);
            //새로운 이미지를 추가
            saveFile(newFile,newFileName);
        }
    }
}
반응형
반응형

이전 단계에서 모든 처리를 마치고 이제 메시지만 전송해주면된다. 

그런데 메시지를 전송할때 수신자가 현재 웹소켓에 연결되어있지않을때에 대한 처리를 해주어야했다.

 

getOnlineSession

private List<WebSocketSession> getOnlineSession(List<UserEntity> userEntities) {
    return userEntities.stream()
            .map(receiver -> userSessionMap.get(receiver.getEmail()))
            .filter(Objects::nonNull)
            .toList();
}

 

 

getOfflineUsers

private List<UserEntity> getOfflineUsers(List<UserEntity> userEntities) {
    List<UserEntity> offlineReceivers = userEntities.stream()
            .filter(receiver -> userSessionMap.get(receiver.getEmail()) == null)
            .toList();
    return offlineReceivers;
}

 

위 메서드를 이용해서 온라인인 유저는 세션으로 받아오고, 오프라인인 유저도 찾았다.

private void sendMessageAndkeep(List<UserEntity> userEntities, ChatMessageDto chatMessageDto, UserEntity sender) {
    //연결중인 세션만 찾아서 문자를전송
    getOnlineSession(userEntities).forEach(
            receiverSession -> sendMessage(receiverSession, chatMessageDto.getMessage()));

    //연결중이지않은 유저는 해당 기록을 저장.
    saveOfflineChat(userEntities, chatMessageDto, sender);
}

 

연결되어있는세션은  바로 chatMessegeDto에있는 message를 전송해주고 

연결중이지않은 유저는 유저정보와 메시지를 저장해준다.

 

saveOfflineChat

private void saveOfflineChat(List<UserEntity> userEntities, ChatMessageDto chatMessageDto, UserEntity sender) {
    List<UserEntity> offlineReceivers = getOfflineUsers(userEntities);

    offlineReceivers.forEach(offlineUser -> {
        OfflineChatHistoryEntity offlineChat = OfflineChatHistoryEntity.builder()
                .senderId(sender.getId())
                .receiverId(offlineUser.getId())
                .content(chatMessageDto.getMessage())
                .createdAt(LocalDateTime.now())
                .build();
        offlineChatHistoryRepository.save(offlineChat);
    });
}

 

이렇게 전송되지못한메시지는 저장해두었다가 이후에 해당유저가 웹소켓에 연결될때 다시 전송해준다.

웹소켓에 연결될때 실행되는 afterConnectionEstablished 메서드에 해당 기능을 추가해준다.

@Override
@Transactional
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    //유저의 이메일을 얻어온다.
    String userId = getUserId(session);

    UserEntity userEntity = userRepository.findByEmail(userId)
            .orElseThrow(() -> new ApiException(ErrorCode.BAD_REQUEST, "없는 유저입니다."));


    // 오프라인 유저 리스트에 해당 유저가 있는지 확인
    if (offlineChatHistoryRepository.existsByReceiverId(userEntity.getId())) {
        // chatRepository에서 해당 유저의 오프라인 메시지 정보 조회
        List<OfflineChatHistoryEntity> offlineMessages = offlineChatHistoryRepository.findByReceiverId(
                userEntity.getId());

        // 조회한 메시지 정보를 새로 연결된 세션을 통해 전송
        for (OfflineChatHistoryEntity message : offlineMessages) {
            sendMessage(session, message.getContent());
        }

        // 오프라인 유저 리스트에서 해당 유저 제거
        offlineChatHistoryRepository.deleteAllByReceiverId(userEntity.getId());
    }

    //유저의 이메일과 세션을 매핑해서 서버에서 관리.
    userSessionMap.put(userId, session);
    log.info("{} 환영합니다", userId);
}

 

이렇게 해주면 웹소켓에 연결될때 받지못한 메시지를 받을수있다.

반응형

'공부 임시 저장소' 카테고리의 다른 글

스프링 부트 - spring-security-jwt  (0) 2024.06.25
스프링부트 - aws s3파일저장  (0) 2024.06.25
스프링부트 - 파일 저장  (0) 2024.06.24
사이드 프로젝트) 채팅앱  (0) 2024.06.24
스프링 웹소켓  (1) 2024.06.03
반응형

이전에 웹소켓을 사용해봤던 경험으로 이번에 사이드프로젝트로 프론트한분과 같이 채팅앱을 구현해보기로했다.

간단히 웹소켓을 만들고 채팅을 연결만 했던것에서 발전시켜서 채팅방을 만들고 채팅 기록을 남길수있도록 처리해주었다.

 

크게 만들어야할 기능들을 정리해보겠다.

1. 채팅기능 

2. 채팅방 

3. 채팅내용기록

4. 회원가입

 

이를 위해서 데이터베이스도 따로 erd로 정리를 해보았다.

 

 

회원가입 친구등록 그외 필요한 기능들을 위해서 db를 정의해 놓았다. 

 

라이브러리

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
    implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
    implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
}

 

jpa, spring-security , validation , web, websocket , mysql을 사용하고있다.

 

이제 본격적으로 코드를 하나씩 작성해보자.

반응형

+ Recent posts