+ 00 00 0000

Have any Questions?

04_Simple Coding – SI-MSA – 로그인 실전예제 4

04_Simple Coding – SI-MSA – 로그인 실전예제 4

쉬운 목차

📃 요약

네이버 카페 등의 로그인 기본 기능을 제작하는 예제입니다.
JSP 와 달리 인증은 웹토큰 인증으로 진행하며 주로 서비스 업체의 MSA 프로젝트에서 많이 사용됩니다.
웹토큰 인증은 스프링 시큐리티에서 제공하지 않으므로 관련 클래스를 직접 작성하고,
스프링 시큐리티 설정에 넣어 자동 웹토큰 인증이 이루어지게 설정해 보겠습니다.

기본적인 DB CRUD 를 사용해 제작합니다.
DB 프레임워크는 JPA 를 사용해 자동화기능을 강화합니다.

요소 기술 및 테이블 설계는 아래와 같습니다.

요소 기술 :

– 프론트엔드 : Vue

– 벡엔드 : 스프링부트 & JPA & Oracle 18xe(Oracle Cloud 19c) & 스프링 시큐리티

결과 화면 :

프로젝트 탐색기 :

Rest API :

메소드URL설명
POST/login로그인 진행 : 스프링에서 인증 웹토큰 보냄, 로컬스토리지에 웹토큰 저장, 만료시간 지나면 사용못함(웹토큰)
POST/register회원가입 진행 :

📃 기술 구현

스펙 :

- jdk 17
- spring boot 3.x
- gradle

테이블 설계

-- TODO: 인증관련 테이블 정의

-- 공통코드 테이블은 시퀀스는 사용하지 않음
-- 공통코드 테이블의 등록된 코드는 향후에 않쓰이더라도 삭제/수정하지 않음 : 데이터가 많지않아 오버헤드가 없음
DROP TABLE TB_CODE_CATEGORY CASCADE CONSTRAINT;
DROP TABLE TB_CODE CASCADE CONSTRAINT;

-- 코드성 테이블 : 공통 코드 유형 테이블
CREATE TABLE TB_CODE_CATEGORY
(
    CATEGORY_ID   NUMBER NOT NULL PRIMARY KEY,
    CATEGORY_NAME VARCHAR2(255)
);

-- 코드성 테이블 : 공통 코드 테이블
CREATE TABLE TB_CODE
(
    CODE_ID     NUMBER NOT NULL PRIMARY KEY,
    CODE_NAME   VARCHAR2(255),
    CATEGORY_ID NUMBER NOT NULL
        CONSTRAINT FK_CODE_CATEGORY_CODE REFERENCES TB_CODE_CATEGORY (CATEGORY_ID),
    USE_YN      VARCHAR(1) DEFAULT 'Y'
);

-- TODO: 인증관련 테이블 정의
-- 유저 테이블
-- login table ddl
DROP TABLE TB_MEMBER CASCADE CONSTRAINTS;

CREATE TABLE TB_MEMBER
(
    EMAIL       VARCHAR2(1000) NOT NULL PRIMARY KEY, -- id (email)
    PASSWORD    VARCHAR2(1000),                                         -- 암호
    NAME        VARCHAR2(1000),                                         -- 유저명
    CODE_NAME   VARCHAR2(1000),                                         -- 권한코드명(ROLE_USER, ROLE_ADMIN)
    DELETE_YN   VARCHAR2(1) DEFAULT 'N',
    INSERT_TIME VARCHAR2(255),
    UPDATE_TIME VARCHAR2(255),
    DELETE_TIME VARCHAR2(255)
);

로그인 구현을 위한 테이블 설계입니다.

데이터베이스를 오라클을 사용하여 구현해 보겠습니다.

로그인 로직

로그인은 인증(사용자 확인), 인가(권한관리)로 나눔
1) 인증(authority) : 로그인시 적절한 사용자인지 확인하는 절차
2) 인가(authentication) : 로그인은 했지만 일정 권한을 가진 사람만 특정 화면을 볼 수 있는 권리, 권한관리라고도 함

스프링 시큐리티 인증 객체들

  • AuthenticationManagerBuilder : 스프링 시큐리티 인증/인가 관리클래스, 다양한 인증방법 함수를 가지고 있음
  • Authentication : 인증 객체 클래스, MemberDto 와 함께 추가 인증에 대한 환경정보를 가지고 있음
  • SecurityContextHolder : 인증 객체들을 넣어두는 필통(홀더), 스프링 시큐리티는 인증 객체들을 보관하는 공간이 있음
  • jwtUtils : 개발자가 직접 작성한 웹토큰 인증 유틸리티 클래스-웹토큰 생성/삭제/검증 등이 있음
  • AuthTokenFilter : 개발자가 직접 작성한 웹토큰 자동 인증 클래스, 스프링 시큐리티 환경 설정함수에 끼워넣어 자동인증함
    (프론트에서 결과 요청할때마다 웹토큰 자동인증이 되어 인증한 유저인지 아닌지 자동 인증됨)

인증 방식 :

1) 쿠키/세션 인증 : 전통적인 인증방식으로 주로 JSP 등을 이용하는 프로그램에서 사용하며, 관련 해킹공격이 있으므로 방어에 유의해야함
    (csrf 해킹 공격)
    - 쿠키 : 웹브라우저에서 기본적으로 제공하는 저장공간 : 주로 민감 보안외의 정보를 넣어서 관리
    - 세션 : 서버에서 정상적인 유저로 확인되면 세션이라는 공간에 인증 정보를 저장하고 세션ID 를 발급해서 쿠키에 넣어 유저에게 전송함
           유저는 로그인 후 다른 화면을 검색할때 정상적인 유저임을 확인하기 위해 항상 쿠키안의 세션id 를 확인함 (쿠키는 서버로 자동전송됨)
    가. 절차 
            1) 로그인 버튼을 클릭하면 유저/암호 정보를 spring 서버로 전송
            2) spring 서버에서 정상적인 유저인지 DB 에서 확인 : 스프링 시큐리티 인증
            3) 확인되면 세션ID 를 발급해서 쿠키에 넣어 유저의 웹브라우저로 전송
            4) 유저는 이제 세션ID 가 있는 쿠키를 가지고 해당 웹사이트의 화면을 볼수 있음
            => 상기의 모든 절차는 spring security 에서 몇가지 설정에 의해 자동으로 모두 이루어지며 기본으로 작동되어 코딩은 필요없음
            => 단, 쿠키/세션에 대한 csrf 해킹공격이 있으므로 방어를 위해 csrf 토큰인증을 추가로 진행해야함
2) 웹토큰 인증(*) : 현재 MSA 환경에서 사용되는 인증방식으로 정상적인 웹토큰(디지털서명인증)을 지닌 사람만이 화면을 볼 수 있게하는 방식,
     주로 vue/react/angular 와 spring 연결 프로그램 제작시 사용하는 인증 방법
     (쿠키/세션 방식이 아니므로 csrf 해킹은 할수 없음 : csrf 해킹방어 불필요)
    가. 절차
            1) 로그인 버튼을 클릭하면 유저/암호 정보를 spring 서버로 전송
            2) spring 서버에서 정상적인 유저인지 DB 에서 확인 : 스프링 시큐리티 인증
            3) 확인되면 웹토큰 발급해서 유저의 웹브라우저로 전송
            4) 웹브라우저의 저장공간 로컬스토리지 또는 쿠키에 웹토큰 저장
            5) 유저는 이제 웹토큰을 가지고 해당 웹사이트의 화면을 볼수 있음
            => 스프링 시큐리티에서 웹토큰 인증방식을 아직 지원하지 않으므로 웹토큰 클래스 로직을 직접 코딩해야하며 
              , 스프링 시큐리티에 수동으로 적용해야함
            => 로컬스토리지에 저장시 탈취될 위험도 존재하므로 리프레쉬 웹토큰 인증을 적용할 수도 있음(인증 강화)
            => 리프레쉬 토큰전략은 웹토큰 만료시간을 아주 짧게 유지하여 탈취되더라도 만료시간으로 인해 위험도를 낮추는 방식임
              , 정상적인 유저는 웹토큰이 만료되면 리프레쉬 토큰을 spring 에 보내 웹토큰을 재발급 받을 수 있음
                (웹토큰 , 리프레쉬 토큰 모두 탈취되면 방법이 없음 : 
                      최고의 기본보안은 pc, 노트북 등에 잠금장치를 해서 탈취를 방지하고, 암호등이 노출안되게 주의해야함 )

프론트 : Vue

  • 쿠키/세션을 사용하지 않지 않으므로 csrf 해킹 공격을 할 수 없음 : csrf 보안 토큰 필요 없음

벡엔드

– 스프링 시큐리티로 환경 설정 : WebSecurityConfig 클래스 – 권한 관리 가능
 스프링 시큐리티 : 필터 기반 인증을 사용 각각의 인증 필터를 통과해야 전체 인증이 됨 ( 디자인패턴 : 책임연쇄 패턴(chain of responsibility)
예) 인증필터 종류 : 웹토큰 필터, id/pwd 필터 등
– 웹토큰 인증은 vue/spring 방식때 사용함

로그인 절차 :

1) 로그인 버튼을 클릭하면 유저/암호 정보를 spring 서버로 전송

  • login 컨트롤러 함수 실행
    2) spring 서버에서 정상적인 유저인지 DB 에서 확인 : 스프링 시큐리티 인증

  • 다양한 인증 등을 하는 인증 관리클래스 준비 : authenticationManagerBuilder
  • 유저/패스워드 인증을 위한 토큰 클래스(DB 인증됨) : UsernamePasswordAuthenticationToken
  • 인증 완료된 유저는 인증 필통(홀더)에 보관 클래스 : SecurityContextHolder
    3) 확인되면 웹토큰 발급해서 유저의 웹브라우저로 전송

  • 웹 토큰 생성 후 유저객체에 넣어 프론트로 전송 클래스 : jwtUtils
    4) 웹브라우저의 저장공간 로컬스토리지에 웹토큰 저장 + 뷰 공유저장소 공유속성에 웹토큰 저장(모든 페이지 접근 가능)

  • 프론트 로컬스토리지에 웹토큰 저장 : localstorage
  • 로컬스토리지의 웹토큰을 공유저장소에서 복사 : 모든 페이지 컴포넌트가 접근 가능
  • 공유저장소 : 엡토큰과 로그인 상태 공유변수를 가지고 있음

5) 유저는 이제 웹토큰을 가지고 해당 웹사이트의 화면을 볼수 있음

    - 다른 페이지의 결과를 조회 요청 할때마다 http 헤더에 웹토큰을 함께 넣어 벡엔드 인증을 통과해야 결과를 볼 수 있음
    - 모든 컨트롤러 함수에 웹토큰 인증 검증 로직이 들어가야함
      코딩 효율성을 위해 시프링 시큐리티 설정에 추가해서 자동 웹토큰 인증을 받도록 구성함
      단, 웹토큰 인증은 시프링 시큐리티가 지원하지 않으므로 개발자가 직접 인증 로직을 구현한 클래스를 작성해야함

벡엔드 시프링 시큐리티 작동 :

스프링 시큐리티 설정 중 로그인 부분 :

WebSecurityConfig.java 의 규칙정의 함수 : filterChain

    //  스프링 시큐리티 룰을 무시하게 하는 Url 규칙(여기 등록하면 규칙 적용하지 않음)
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors();           // cors 사용
        http.csrf().disable(); // csrf 보안 비활성화 (쿠키/세션 사용않함)

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 세션안쓰고(stateful) JWT 사용 예정
                .formLogin().disable()     // form 태그를 이용한 로그인 사용않함 -> vue : axios 로 통신함
                .authorizeRequests().antMatchers("/api/auth/**").permitAll() // 이 url 은 모든 사용자 접근 허용
                .antMatchers("/api/admin/**").hasRole("ADMIN") // admin 메뉴는 ROLE_ADMIN 만 가능
                .antMatchers("/api/basic/dept/**").permitAll() // 이 url 은 모든 사용자 접근 허용
                .anyRequest().authenticated(); // 그외 url은 모든 사용자, 모든 접속에 대해서 인증이 필요하다는 걸 의미

//    UsernamePasswordAuthenticationFilter → username, password를 쓰는 form기반 인증을 처리하는 필터.
//        위의 인증 전에 새로 만든 웹토큰 인증 필터를 끼워 넣어 프론트에서 결과 조회 요청을 할때마다 웹토큰 인증을 자동으로 받게함
//            AuthenticationManager를 통한 인증 실행
        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); // JWT 토큰 필터 적용

        return http.build();
    }

로그아웃 절차

1) 프론트에서 로그아웃 버튼 클릭 :

        (1) 로그아웃 시 로컬스토리지에 존재하는 웹토큰 삭제함
        (2) 웹토큰은 만료시간이 있어 시간이 지나면 쓸 수 없음
           벡엔드 조회시 403 인증 에러 발생함
        (3) 웹토큰의 만료시간을 지정가능함

모델 : 엔티티

공통 모델

package com.example.jpaexam.model.common;

import lombok.Getter;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * packageName : com.example.jpaexam.model
 * fileName : BaseTimeEntity
 * author : GGG
 * date : 2023-10-16
 * description : JPA 에서 자동으로 생성일자/수정일자를 만들어 주는 클래스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * —————————————————————————————
 * 2023-10-16         GGG          최초 생성
 */
@Getter
// todo: 자동으로 생성일자/수정일자 컬럼을 sql 문에 추가시키는 어노테이션 2개
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
//    todo: 공통속성 : yyyy-MM-dd HH:mm:ss 아니고 기본 패턴으로 보임
    private String insertTime;

    private String updateTime;

////    todo: 해당 테이블에 데이터가 만들어 질때(insert 문) 실행되는 이벤트 함수
    @PrePersist
    void OnPrePersist() {
        this.insertTime
                = LocalDateTime.now()
                    .format(DateTimeFormatter
                            .ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
//
////    todo: 해당 테이블에 데이터가 수정 질때(update 문) 실행되는 이벤트 함수
    @PreUpdate
    void OnPreUpdate() {
        this.updateTime
                = LocalDateTime.now()
                .format(DateTimeFormatter
                        .ofPattern("yyyy-MM-dd HH:mm:ss"));
        this.insertTime = this.updateTime; // 생성일시 == 수정일시 동일하게 처리
    }
}

Member.java

package com.example.simpledms.model.entity.auth;

import com.example.simpledms.model.common.BaseTimeEntity;
import com.example.simpledms.model.common.BaseTimeEntity2;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * packageName : com.example.simplelogin.model.entity.auth
 * fileName : User
 * author : kangtaegyung
 * date : 2023/08/06
 * description :
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2023/08/06         kangtaegyung          최초 생성
 */
@Entity
@Table(name="TB_MEMBER")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Member extends BaseTimeEntity2 {

  @Id
  private String email;

  private String password;
//  로그인 ID 임
  private String name;     // 유저명

  private String codeName; // 권한명 ( ROLE_USER, ROLE_ADMIN )

}

로그인 레포지토리

package com.example.simpledms.repository.auth;

import com.example.simpledms.model.entity.auth.Member;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MemberRepository extends JpaRepository<Member, String> {

  Page<Member> findAllByNameContaining(String name, Pageable pageable);
}

select : @Query 이용한 오라클 기반 쿼리(nativeQuery = true) 임

스프링 시큐리티 관련 클래스

- WebSecurityConfig.java : 스프링 시큐리티 설정 클래스 
- 웹토큰 인증 필터를 끼워넣어 프론트에서 결과 요청 할때마다 자동 웹토큰 인증 진행됨 
- MemberDto, UserDetailsServiceImpl 클래스를 이용해서 DB 에 사용자가 있는지 확인하고 새로운 권한을 부여함 : 추가 코딩이 필요

WebSecurityConfig : 스프링 시큐리티 설정 클래스

- 다양한 인증/인가 설정 가능 : 예) 특정 웹페이지만 접속, 특정 권한만 접속 등
package com.example.simpledms.config;

import com.example.simpledms.security.jwt.AuthTokenFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * packageName : com.example.modelexam.controller
 * fileName : FileDbController
 * author : kangtaegyung
 * date : 2022/10/12
 * description : 부서 컨트롤러
 * 요약 :
 * // spring security 라이브러리를 설치하면
 * // 기본적으로 모든 url 에 대해 인증을 진행함
 * // 내부적으로 사용하고 있는 로그인페이지로 자동 리다이렉트함
 * // 기본 user id : user , pwd: 콘솔에 보임
 * // application.properties 파일에 user/pwd 설정 가능
 * // 아래 클래스에서 인증/접근권한을 설정할 수 있음
 * <p>
 * // securedEnabled, prePostEnabled, jsr250Enabled 3개의 옵션이 존재(활성화 @)
 * // 1.securedEnabled
 * //  @Secured 애노테이션을 사용하여 인가 처리를 하고 싶을때 사용하는 옵션이다.
 * //  단순 권한체크, spring 에서만 가능
 * //  기본값은 false
 * // 2.prePostEnabled
 * //  @PreAuthorize, @PostAuthorize 애노테이션을 사용하여 인가 처리를 하고 싶을때 사용하는 옵션이다.
 * //  다양하고 유연하게 권한체크 가능, 유연한 권한체크를 위한 el 언어 제공 : 예) 권한문자열이 140 이상일때만 통과 등
 * //  기본값은 false
 * // 3.jsr250Enabled
 * //  @RolesAllowed 애노테이션을 사용하여 인가 처리를 하고 싶을때 사용하는 옵션이다.
 * //  단순 권한체크, java 사용하는 곳은 모두 가능
 * //  기본값은 false
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2022/10/12         kangtaegyung          최초 생성
 */

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    //  패스워드 암호화 함수
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //  JWT 토큰 필터 객체 생성
    @Bean
    public AuthTokenFilter authenticationJwtTokenFilter() {
        return new AuthTokenFilter();
    }

    /*
     * 스프링 시큐리티 룰을 무시하게 하는 Url 규칙(여기 등록하면 규칙 적용하지 않음)
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().antMatchers("/js/**", "/images/**", "/css/**");
    }

    //  스프링 시큐리티 룰을 무시하게 하는 Url 규칙(여기 등록하면 규칙 적용하지 않음)
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors();           // cors 사용
        http.csrf().disable(); // csrf 보안 비활성화 (쿠키/세션 사용않함)

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 세션안쓰고(stateful) JWT 사용 예정
                .formLogin().disable()     // form 태그를 이용한 로그인 사용않함 -> vue : axios 로 통신함
                .authorizeRequests().antMatchers("/api/auth/**").permitAll() // 이 url 은 모든 사용자 접근 허용
                .antMatchers("/api/admin/**").hasRole("ADMIN") // admin 메뉴는 ROLE_ADMIN 만 가능
                .antMatchers("/api/basic/dept/**").permitAll() // 이 url 은 모든 사용자 접근 허용
                .anyRequest().authenticated(); // 그외 url은 모든 사용자, 모든 접속에 대해서 인증이 필요하다는 걸 의미

//    UsernamePasswordAuthenticationFilter → username, password를 쓰는 form기반 인증을 처리하는 필터.
//        위의 인증 전에 새로 만든 웹토큰 인증 필터를 끼워 넣어 프론트에서 결과 조회 요청을 할때마다 웹토큰 인증을 자동으로 받게함
//            AuthenticationManager를 통한 인증 실행
        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); // JWT 토큰 필터 적용

        return http.build();
    }
}

MemberDto : User 상속(스프링 시큐리티 제공)

package com.example.simpledms.security.dto;

import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

/**
 * packageName : com.example.jpaexam.security.dto
 * fileName : UserDto
 * author : kangtaegyung
 * date : 1/14/24
 * description : UserDetailsService 에서 사용할 스프링 시큐리티 유저 DTO
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 1/14/24         kangtaegyung          최초 생성
 */
@Getter
@Setter
@ToString
public class MemberDto extends User {

    private String email; // 개발자 추가 속성, 스프링시큐리티의 username 과 같음(로그인 ID 로 사용할 변수)

    public MemberDto(String email, String password,
                     Collection<? extends GrantedAuthority> authorities) {
        super(email, password, authorities);
        this.email = email;
    }

}

UserDetailsServiceImpl : UserDetailsService 상속(스프링 시큐리티 제공)

package com.example.simpledms.security.services;

import com.example.simpledms.model.entity.auth.Member;
import com.example.simpledms.repository.auth.MemberRepository;
import com.example.simpledms.security.dto.MemberDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.HashSet;
import java.util.Set;

/**
 * packageName : com.example.simpledms.controller
 * fileName : UserController
 * author : kangtaegyung
 * date : 2023/07/13
 * description : DB 에 ID(email) 로 조회해서 있는지 확인하는 클래스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2023/07/13         kangtaegyung          최초 생성
 */
// 유저 인증을 위한 클래스
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
  @Autowired
  MemberRepository memberRepository;

  //  유저 인증을 위한 함수
//  DB에 있는 지 확인해서 있으면 UserDto 객체 생성
  @Override
  public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    //  유저 인증을 위한 함수 ( DB 확인 ) : 기본키 (이메일)
    Member member = memberRepository.findById(email)
            .orElseThrow(() -> new UsernameNotFoundException("User Not Found with email: " + email));

//    권한 정보 생성 :
    GrantedAuthority authority = new SimpleGrantedAuthority(member.getCodeName());

//    권한이 여러개 저장될 수 있으므로 set로 생성해서 전달되어야 함 : (스프링 시큐리티에서 set 을 사용함)
    Set<GrantedAuthority> authoritis = new HashSet<>();
    authoritis.add(authority);

    //  DB에 있는 지 확인해서 있으면 UserDto 로 User 객체 생성
    return new MemberDto(member.getEmail(), member.getPassword(), authoritis);
  }
}

AuthTokenFilter : OncePerRequestFilter(스프링 시큐리티 제공 템플릿 인증 필터 상속)

웹토큰 자동 인증 클래스 : 프론트에서 결과 요청할때마다 자동실행되어 웹토큰 검증 및 DB 인증을 진행함

스프링시큐리티 설정 클래스에 등록 : 등록해야만 자동 웹토큰 검증이 진행됨

스프링 시큐리티에서 웹토큰은 아직 지원하지 않으므로 개발자가 직접 코딩해야함

package com.example.simpledms.security.jwt;

import com.example.simpledms.security.services.UserDetailsServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * packageName : com.example.simpledms.controller
 * fileName : UserController
 * author : kangtaegyung
 * date : 2023/07/13
 * description : 웹 토큰 인증 필터 - 로그인 이후 인증 : 웹토큰을 프론트의 헤더에서 전송했을 경우 자동 인증하는 클래스
 * 요약 :
 *    역할 : 로그인 이후 화면을 서핑할때 헤더에 넣어 웹토큰을 전송해 주는데 매번 컨트롤러에서 인증하는 것은 번거로운 일이므로
 *        자동 인증이 될수 있도록 스프링 시큐리티 인증 필터로 등록해서 사용함
 *        스프링시큐리티가 자동으로 웹토큰을 인증함
 *        (1) 웹토큰 유효성 체크해서
 *        (2) 유효하면 DB에서 유저 있는 지 조회
 *        (3) 조회된 유저를 인증된 유저로 해서(authentication) 홀더에(SecurityContextHolder) 넣음 => 시큐리티에서 관리함
 *           가. UserDto + 추가 속성들 => authentication 객체에 등록
 *               => UserDto == UserDetails == User(id, password) =< Authentication(id(==principal), password(==credential), ip, 인증여부 등) : 3개는 거의 비슷한 인증 객체임
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2023/07/13         kangtaegyung          최초 생성
 */
// JWT 토큰 인증을 위한 필터 , 스프링에 기본적으로 없으므로 만들어 주어야함 ( 기본 : 세션 필터 )
// OncePerRequestFilter : 요청 당 반드시 한번만 인증/인가 로직 실행하게 보장함
// 예) 요청에 대해 인증/권한체크 후(서브 요청 1) 특정 url로 리다이렉트할때(서브 요청 2) 보통은 인증/권한체크가 2번 일어남 이때
//     마지막은 불필요하므로 한번만 인증/권한체크가 일어나게 보장하게 만들어주는 인터페이스
@Slf4j
public class AuthTokenFilter extends OncePerRequestFilter {
  @Autowired
  private JwtUtils jwtUtils;

  @Autowired
  private UserDetailsServiceImpl userDetailsService;

//  Json Web Token 필터 만들어 SecurityContextHolder 에 새로운 JWT 필터 저장
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
    try {
//      웹토큰 받아서 문자열로 변환
      String jwt = parseJwt(request);
//      1. 웹토큰 유효성 체크해서
//      2. 유효하면 DB에서 유저 있는 지 조회
//      3. 조회된 유저를 인증된 유저로 해서 홀더에 넣음
      if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
//        웹토큰에서 유저 id 꺼냄
        String email = jwtUtils.getUserNameFromJwtToken(jwt);

//        유저 id로 db 조회해서 userDetails에 넣음
        UserDetails userDetails = userDetailsService.loadUserByUsername(email);
//      인증된 객체로 반환 : new UsernamePasswordAuthenticationToken() 매개변수 3개짜리 생성자 효출하면 강제 인증 성공 authenticated = true 로 설정됨
        UsernamePasswordAuthenticationToken authentication =
            new UsernamePasswordAuthenticationToken(
                userDetails,
                null,
                userDetails.getAuthorities());
//        인증 추가 정보 : id/ password 외에 추가정보가 있으면 더 넣는 코딩 : 추가 인증키들을 프론트에서 보내주면 저장할 수 있음
//        WebAuthenticationDetailsSource : 인증 추가정보를 관리하는 클래스
        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

//        인증된 authentication 객체를 홀더에 넣어둠
        SecurityContextHolder.getContext().setAuthentication(authentication);
      }
    } catch (Exception e) {
      logger.error("Cannot set user authentication: {}", e);
    }

//    필터체인으로 연결하여 줍니다.
//    체인에 필터를 실행하고 체인의 가장 마지막에는 클라이언트가 요청한 최종 자원이 위치
    filterChain.doFilter(request, response);  // 필터 실행
  }

//  네트웍으로 전송된 헤더 데이터에 "Bearer" 있고
//  "Authorization" 다음 문자열이 있으면 7부터 헤더의 길이만큼 잘라서 리턴함
  private String parseJwt(HttpServletRequest request) {
    String headerAuth = request.getHeader("Authorization");

    if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
      return headerAuth.substring(7, headerAuth.length());
    }

    return null;
  }
}

JwtUtils : 웹토큰 인증 유틸리티 클래스(웹토큰 생성, 검증, 삭제함수 제공)

스프링 시큐리티에서 웹토큰은 아직 지원하지 않으므로 개발자가 직접 코딩해야함

package com.example.simpledms.security.jwt;

import com.example.simpledms.security.dto.MemberDto;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * packageName : com.example.simpledms.controller
 * fileName : UserController
 * author : kangtaegyung
 * date : 2023/07/13
 * description : 웹 토큰 유틸리티 클래스 (토큰 만들기, 토큰 유효성 체크, 토큰에서 유저 꺼내기)
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2023/07/13         kangtaegyung          최초 생성
 */
@Slf4j
@Component
public class JwtUtils {
//  private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);

  @Value("${simpleDms.app.jwtSecret}")
  private String jwtSecret;

  @Value("${simpleDms.app.jwtExpirationMs}")
  private int jwtExpirationMs;

//  JWT 토큰 만들기
  public String generateJwtToken(Authentication authentication) {

    MemberDto memberDto = (MemberDto) authentication.getPrincipal();

//    Json Web Token 구조 : 헤더(header).내용(payload).서명(signature)
//    헤더 : 토큰타입, 알고리즘
//    내용 : 데이터(subject(주체(이름))), 토큰발급대상(issuedAt), 만료기간(expiration), 토큰수령자
//    서명 : Jwts.builder().signWith(암호화알고리즘, 비밀키값)
//    생성 : Jwts.builder().compact()
    return Jwts.builder()
        .setSubject((memberDto.getEmail()))
        .setIssuedAt(new Date())
//            만료일자 적용
        .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
        .signWith(SignatureAlgorithm.HS512, jwtSecret) // 암호화 적용 서명
        .compact(); // 토큰 생성
  }

//  JWT 토큰에서 유저명 꺼내기
  public String getUserNameFromJwtToken(String token) {
//    웹토큰의 비밀키 + 토큰명을 적용해 body 안의 subject(주체(이름))에 접근해서 꺼냄
    return Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
  }

  //  JWT 토큰에서 만료시간 꺼내기
  public Date getExpirationFromJwtToken(String token) {
//    웹토큰의 비밀키 + 토큰명을 적용해 body 안의 subject(주체(이름))에 접근해서 꺼냄
    return Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody()
            .getExpiration();
  }

//  JWT 웹토큰 유효성 체크
//  디지털 서명이 위조 또는 훼손되었는지 확인하는 함수
  public boolean validateJwtToken(String authToken) {
    try {
//      setSigningKey(jwtSecret) : 비밀키를 넣어 웹토큰 디코딩하기(해석)
//      parseClaimsJws : 웹토큰을 분리하여 유효성 점검하는 함수
      Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
      return true;
    } catch (SignatureException e) {
      log.error("Invalid JWT signature: {}", e.getMessage());
    } catch (MalformedJwtException e) {
      log.error("Invalid JWT token: {}", e.getMessage());
    } catch (ExpiredJwtException e) {
      log.error("JWT token is expired: {}", e.getMessage());
    } catch (UnsupportedJwtException e) {
      log.error("JWT token is unsupported: {}", e.getMessage());
    } catch (IllegalArgumentException e) {
      log.error("JWT claims string is empty: {}", e.getMessage());
    }

    return false;
  }
}

로그인 서비스

package com.example.simpledms.service.auth;

import com.example.simpledms.model.entity.auth.Member;
import com.example.simpledms.repository.auth.MemberRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.Optional;

/**
 * packageName : com.example.simpledms.service
 * fileName : UserService
 * author : kangtaegyung
 * date : 2023/07/13
 * description : User 서비스
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2023/07/13         kangtaegyung          최초 생성
 */
@Slf4j
@Service
public class MemberService {

    @Autowired
    private MemberRepository memberRepository;

    //    username like 검색
    public Page<Member> findAllByNameContaining(String username, Pageable pageable) {

        //        제목 조회 결과
        Page<Member> page = memberRepository.findAllByNameContaining(username, pageable);

        return page;
    }

    public Optional<Member> findById(String email) {
        Optional<Member> optionalUser = memberRepository.findById(email);

        return optionalUser;
    }

    public boolean existsById(String email) {
        boolean bResult = memberRepository.existsById(email);

        return bResult;
    }

    public Member save(Member user) {
        Member user2 = memberRepository.save(user);

        return user2;
    }

    public boolean removeById(String email) {

        if (memberRepository.existsById(email)) {
            memberRepository.deleteById(email);
            return true;
        }

        return false;
    }

}

로그인 컨트롤러

package com.example.simpledms.controller.auth;

import com.example.simpledms.model.dto.auth.request.UserReq;
import com.example.simpledms.model.dto.auth.response.UserRes;
import com.example.simpledms.model.entity.auth.Member;
import com.example.simpledms.security.jwt.JwtUtils;
import com.example.simpledms.service.admin.CodeService;
import com.example.simpledms.service.auth.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.Date;

/**
 * packageName : com.example.simpledms.controller
 * fileName : UserController
 * author : kangtaegyung
 * date : 2023/07/13
 * description : 부서 컨트롤러
 * 요약 :
 * <p>
 * ===========================================================
 * DATE            AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2023/07/13         kangtaegyung          최초 생성
 */
@RestController
@RequestMapping("/api/auth")
public class AuthController {
  @Autowired
  AuthenticationManagerBuilder authenticationManagerBuilder;; // 인증/권한체크 처리를 위한 객체

  @Autowired
  JwtUtils jwtUtils;  // 웹토큰 객체

  @Autowired
  MemberService userService;

  @Autowired
  CodeService codeService;

  @Autowired
  PasswordEncoder encoder;

  //  로그인 함수 : GET 방식 아닌 POST 방식으로 객체를 body 에 숨겨서 전송됨
  @PostMapping("/login")
  public ResponseEntity<Object> login(@RequestBody UserReq userReq) {

    try {

//    인증 시작
      Authentication authentication = authenticationManagerBuilder.getObject().authenticate(
              // 아이디와 패스워드로, Security 가 알아 볼 수 있는 token 객체로 생성해서 인증처리
              new UsernamePasswordAuthenticationToken(userReq.getEmail(), userReq.getPassword()));

//    인증된 객체를 홀더에 저장
      SecurityContextHolder.getContext().setAuthentication(authentication);

//    JWT 토큰 발행
      String jwt = jwtUtils.generateJwtToken(authentication);

//    권한 가져와서 codeName 에 저장
//      권한은 1st 집합배열에 있음 => 향상된 배열로 변경해서 1st 데이터 꺼내서 문자열로 변환
      String codeName = new ArrayList(authentication.getAuthorities()).get(0).toString();

//            결과를 전송할 유저인증 정보 생성
      UserRes userRes = new UserRes(jwt,
              userReq.getEmail(),
              codeName
      );
//    클라이언트에 인증된 사용자 정보 전송(토큰 + 사용자 정보) :
//         userRes DTO 객체에 정보를 담아 전송
      return new ResponseEntity<>(userRes, HttpStatus.OK);
    } catch (Exception e) {
//            서버 에러
      return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  @PostMapping("/register")
  public ResponseEntity<Object> createUser(@RequestBody UserReq userReq) {

    try {
//    이메일이 DB 에 있는 지 확인
      if (userService.existsById(userReq.getEmail())) {
        return ResponseEntity
                .badRequest()
                .body("Error: Email is already in use!");
      }

      // Create new userDto's account
      Member member = new Member(
              userReq.getEmail(),
              encoder.encode(userReq.getPassword()),
              userReq.getName(),
              userReq.getCodeName()
      );

      userService.save(member);

      return new ResponseEntity<>("User registered successfully!", HttpStatus.OK);

    } catch (Exception e) {
//            DB 에러가 났을경우 : INTERNAL_SERVER_ERROR 프론트엔드로 전송
      return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

}

Vue 페이지

Vue 공통 서비스

인증시 웹토큰을 헤더에 넣는 공통 자바스크립트 파일

벡엔드에 결과 요청시 항상 웹토큰을 전송하면서 요청해야 정상적으로 결과를 볼 수 있음

services/auth/AuthHeader.js

export default function authHeader() {
  // localStorage 의 user 정보를(json 형태 문자열) 객체로 변환  
  let user = JSON.parse(localStorage.getItem('user'));

  // 유저 객체가 있다면 http 헤더에 아래 정보 저장
  // { Authorization: 'Bearer ' + user.accessToken }
  if (user && user.accessToken) {
    return { Authorization: 'Bearer ' + user.accessToken }; // for Spring Boot back-end
    // return { 'x-access-token': user.accessToken };       // for Node.js Express back-end
  } else {
    return {};
  }
}

공통 로그인/로그아웃 서비스 함수

services/auth/AuthService.js

// import axios from 'axios';
import http from '@/utils/http-common';

// const API_URL = 'http://localhost:8080/api/auth/';

class AuthService {

  login(user) {

    // user 객체의 정보를 임시 객체에 저장
    let userData = {
      email: user.email,
      password: user.password
    }

    return http.post('/auth/login', userData)
  }

  logout() {
    // localStorage 값을 삭제
    localStorage.removeItem('user');
  }

  register(user) {

    let userData = {
      email: user.email,
      password: user.password,
      name: user.name,
      codeName: user.codeName
    }

    // return axios.post(API_URL + 'signup', userData);
    return http.post('/auth/register', userData);
  }
}

export default new AuthService();

웹토큰은 공유저장소에 함께 저장하고 로그인상태를 공유저장소에서 관리함

store/index.js

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

//  로컬스토리지에서 저장된 user 객체 가져오기
const user = JSON.parse(localStorage.getItem("user"));

export default new Vuex.Store({
  // 공유 속성
  state: {
    loggedIn: user ? true : false,
    user: user ? user : null,
  },
  // 공유속성에 값을 저장하는 함수 : setter 함수와 비슷
  mutations: {
    loginSuccess(state, user) {
      state.loggedIn = true;
      state.user = user;
    },
    loginFailure(state) {
      state.loggedIn = false;
      state.user = null;
    },
    logout(state) {
      state.loggedIn = false;
      state.user = null;
    },
    registerSuccess(state) {
      state.loggedIn = false;
    },
    registerFailure(state) {
      state.loggedIn = false;
    },
  },
  // 비동기 공유 함수 정의 : 벡엔드 통신 함수들은 여기에 작성함
  // mutations 함수를 호출할 경우 사용법 : commit("뮤테이션함수명", 값);
  actions: {
  },
});

Vue 로그인 페이지

결과 화면 :


Vue 로그인 후 성공 페이지

결과 화면 :

<template>
  <div>
    <div class="row justify-content-center">
      <div class="col-xl-10 col-lg-12 col-md-9">
        <div class="card mt-5">
          <div class="card-body p-0">
            <!-- {/* Nested Row within Card Body */} -->
            <div class="row">
              <div class="col-lg-6 bg-login-image"></div>
              <div class="col-lg-6">
                <div class="p-5">
                  <div class="text-center">
                    <h1 class="h4 mb-4">Welcome Back!</h1>
                  </div>
                  <form class="user" @submit.prevent="handleLogin">
                    <div class="form-group">
                      <input
                        v-model="user.email"
                        v-validate="'required'"
                        type="email"
                        class="form-control form-control-user mb-3"
                        id="exampleInputEmail"
                        aria-describedby="emailHelp"
                        placeholder="Enter Email Address..."
                        name="email"
                      />
                      <div
                        v-if="errors.has('email')"
                        class="alert alert-danger"
                        role="alert"
                      >
                        Email is required!
                      </div>
                    </div>
                    <div class="form-group">
                      <input
                        v-model="user.password"
                        v-validate="'required'"
                        type="password"
                        class="form-control form-control-user mb-3"
                        id="exampleInputPassword"
                        placeholder="Password"
                        name="password"
                      />
                      <div
                        v-if="errors.has('password')"
                        class="alert alert-danger"
                        role="alert"
                      >
                        Password is required!
                      </div>
                    </div>

                    <button class="btn btn-primary btn-user w-100 mb-3">
                      Login
                    </button>
                    <hr />
                    <a href="/" class="btn btn-google btn-user w-100 mb-2">
                      <i class="fab fa-google fa-fw"></i> Login with Google
                    </a>
                    <a href="/" class="btn btn-naver btn-user w-100 mb-2">
                      <i class="fa-solid fa-n"></i> Login with Naver
                    </a>
                    <a href="/" class="btn btn-kakao btn-user w-100 mb-3">
                      <i class="fa-solid fa-k"></i> Login with Kakao
                    </a>
                  </form>
                  <hr />
                  <div class="text-center">
                    <a class="small" href="/forgot-password">
                      Forgot Password?
                    </a>
                  </div>
                  <div class="text-center">
                    <a class="small" href="/register"> Create an Account! </a>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import AuthService from "@/services/auth/AuthService";
export default {
  data() {
    return {
      user: {
        email: "",
        password: "",
        name: "",
        codeName: "",
      },
      message: "",
    };
  },
  created() {
    if (this.$store.state.loggedIn) {
      this.$router.push("/profile");
    }
  },
  methods: {
    handleLogin() {
      // vee-balidator 실행
      this.$validator.validateAll().then(async (isValid) => {
        // 유효성 검사 모두 통과하면 isValid = true, 1개라도 틀리면 false
        if (isValid) {
          try {
            // spring login 요청 -> respnse : 웹토큰 가진 요청가 리턴됨, localstroage에 저장
            let response = await AuthService.login(this.user);
            console.log(response.data);
            // 데이터를 문자열로 바꾸어서 localStorage 저장
            localStorage.setItem("user", JSON.stringify(response.data));
            // 로그인 성공상태 반영 : 공유저장소
            this.$store.commit("loginSuccess", response.data);
            // 홈 페이지로 강제이동
            this.$router.push("/");
          } catch (e) {
            // 로그인 실패상태 반영 : 공유저장소
            this.$store.commit("loginFailure");
            console.log(e);
          }
        }
      });
    },
  },
};
</script>
<style>
@import "@/assets/css/login.css";
</style>

Vue 로그아웃 페이지 : 로그인하면 메뉴가 변경되고, 메뉴 오른쪽에 로그아웃 버튼 있음

결과 화면 :


Vue 회원가입 페이지 :

결과 화면 :

<template>
  <div>
    <div class="card mt-5">
      <div class="card-body p-0">
        <!-- Nested Row within Card Body -->
        <div class="row">
          <div class="col-lg-5 bg-register-image"></div>
          <div class="col-lg-7">
            <div class="p-5">
              <div class="text-center">
                <h1 class="h4 text-gray-900 mb-4">Create an Account!</h1>
              </div>
              <form class="user" @submit.prevent="handleRegister">
                <div class="form-group">
                  <input
                    v-model="user.email"
                    v-validate="'required|email|max:50'"
                    type="email"
                    class="form-control form-control-user mb-3"
                    id="exampleInputEmail"
                    placeholder="Email Address"
                    name="email"
                  />
                  <div
                    v-if="submitted && errors.has('email')"
                    class="alert-danger"
                  >
                    {{ errors.first("email") }}
                  </div>
                </div>
                <div class="form-group row">
                  <div class="col-sm-6 mb-3 mb-sm-0">
                    <input
                      v-model="user.password"
                      v-validate="'required|min:6|max:40'"
                      type="password"
                      class="form-control form-control-user mb-3"
                      id="exampleInputPassword"
                      placeholder="Password"
                      name="password"
                      ref="password"
                    />
                    <div
                      v-if="submitted && errors.has('password')"
                      class="alert-danger"
                    >
                      {{ errors.first("password") }}
                    </div>
                  </div>
                  <div class="col-sm-6">
                    <input
                      v-validate="'required|confirmed:password'"
                      type="password"
                      class="form-control form-control-user mb-3"
                      id="exampleRepeatPassword"
                      placeholder="Repeat Password"
                      name="repassword"
                    />
                    <div
                      v-if="submitted && errors.has('repassword')"
                      class="alert-danger"
                    >
                      {{ errors.first("repassword") }}
                    </div>
                  </div>
                  <div class="form-group">
                    <input
                      v-model="user.name"
                      v-validate="'required|min:3|max:20'"
                      type="text"
                      class="form-control form-control-user mb-3"
                      id="exampleName"
                      placeholder="Full Name"
                      name="name"
                    />
                    <!-- 유효성 체크 후 에러 체크 : errors.hat(체크대상) -->
                    <!-- errors.first(체크대상) : 에러 내용 출력 -->
                    <div
                      v-if="submitted && errors.has('name')"
                      class="alert-danger"
                    >
                      {{ errors.first("name") }}
                    </div>
                  </div>
                  <div class="form-group">
                    <select
                      class="form-select form-control-select form-select-sm mb-3"
                      aria-label="Default select example"
                      name="codeName"
                      v-model="user.codeName"
                    >
                      <option value="ROLE_USER">ROLE_USER</option>
                      <option value="ROLE_ADMIN">ROLE_ADMIN</option>
                    </select>
                  </div>
                </div>
                <button
                  type="submit"
                  class="btn btn-primary btn-user w-100 mb-3"
                >
                  Register Account
                </button>
              </form>
              <p v-if="message" class="alert alert-success" role="alert">
                {{ message }}
              </p>
              <hr />
              <div class="text-center">
                <a href="/forgot-password"> Forgot Password? </a>
              </div>
              <div class="text-center">
                <a href="/login"> Already have an account? Login! </a>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import AuthService from '@/services/auth/AuthService';
export default {
  data() {
    return {
      user: {
        email: "",
        password: "",
        name: "",
        codeName: "ROLE_USER",
      },
      submitted: false,
      message: "",
    };
  },
  mounted() {
    // loggedIn == true : 로그인 상태 -> /profile 페이지로 강제 이동
    if (this.$store.state.loggedIn) {
      this.$router.push("/home");
    }
  },
  methods: {
    // 새 사용자 등록 함수
    handleRegister() {
      this.message = "";
      this.submitted = true;
      // form 유효성 체크 검사
      // this.$validator.validate() : 유효하면 isValid = true , 아니면 isValid = false
      this.$validator.validate().then(async (isValid) => {
        if (isValid) {
          try {
            let response = await AuthService.register(this.user);
            // 공유저장소에 새사용자 성공상태 저장
            this.$store.commit("registerSuccess");
            // 화면에 성공메세지 출력
            this.message = "user registered sucessfull!";
            // 성공상태 
            console.log(response.data);
          } catch (e) {
            // 공유저장소에 새사용자 실패상태 저장
            this.$store.commit("registerFailure");
            this.message = "error found!";
            console.log(e);
          }
        }
      });
    },
  },
};
</script>
<style>
@import "@/assets/css/login.css";
</style>

📃 결론

로그인/로그아웃 샘플 예제를 살펴보았습니다.
스프링 시큐리티 설정을 이용해서 구현했으며 스프링 시큐리티의 자동 실행되는 내부 클래스에 수정해서 사용해봤습니다.
인증 방식은 웹토큰 인증 방식을 적용했으며,
스프링 시큐리티에서 웹토큰 인증을 아직 지원하지 않으므로 몇가지 클래스를 직접 작성하여 로그인 인증,
웹토큰 자동 인증을 구현해 보았습니다.

DB 프레임워크는 JPA 를 이용해서 sql 문을 직접 제작하지 않고 자동화기능을 이용해 구현했습니다.
Mybatis 의 직접 sql 문 제작 기능에 반해서 자주 반복되고 쉬운 기능은 JPA 의 sql 자동생성 기능을 이용하고,
복잡한 sql 문은 @Query 를 이용해 직접 작성할 수 있어 요즘 많이 서비스 업체 기준으로 사용되고 있습니다.

감사합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다