티스토리 뷰

Vue.js 와 SpringBoot를 사용해 게시판 토이프로젝트를 구현하고 있다.

CRUD과정은 전부 끝났고 Vue.js와 씨름하며 시간을 보내다가 드디어 로그인 파트로 넘어왔다.

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-jwt/dashboard

 

[무료] Spring Boot JWT Tutorial - 인프런 | 강의

Spring Boot, Spring Security, JWT를 이용한 튜토리얼을 통해 인증과 인가에 대한 기초 지식을 쉽고 빠르게 학습할 수 있습니다., [사진] 본 강의는 Spring Boot, Spring Security를 이용해서 JWT 인증과 인가를 쉽

www.inflearn.com

위 과정을 참고하여 Jwt를 진행하였다.

확실히 1년 전 부트캠프를 다닐 때 베껴 쓰며 사용한 Jwt와 현재 기본을 배우면서 응용하는 과정은 차이가 큰 것 같다고 느꼈다.

강의에선 Jpa를 사용하여 구현하고 있지만 Mybatis를 사용하고 있는 입장에서 바꿔야 될 부분이 의외로 많았다.

 

기록으로 남겨두어야 혹시 나중에 Mybatis로 Jwt를 구현할 때 편하게 사용할까 싶어 적는다.

 

 


Security + Jwt 설정

UserSchema

# users 테이블
create table users
(
    user_no  bigint auto_increment comment '회원번호(pk)'
        primary key,
    username varchar(50)                      not null comment '유저아이디',
    password varchar(100)                     not null comment '비밀번호',
    role     varchar(100) default 'ROLE_USER' not null comment '회원 role',
    constraint users_pk2
        unique (username)
)
    comment '회원테이블';

강의에선 User와 Authority의 테이블을 다대다로 구현하여

User, Authority, User_Authority 테이블 총 3개가 생긴다.

물론 User테이블과 Authority테이블을 관계설정하면 좋겠지만

나중에 쿼리문을 작성할 때, 귀찮아질 것 같아 users하나의 테이블에 전부 넣어두었다.

 

 


build.gradle

    //Security
    implementation 'org.springframework.boot:spring-boot-starter-security'

    //Jwt
    implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

빌드 그레이들 Security + Jwt 설정 추가

 


application.yml

spring:
  datasource:
    driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
    url: jdbc:log4jdbc:mariadb://localhost:3306/spa_springboot_vu
...

#jwt설정
jwt:
  header: Authorization
  secret: ......
  token-validity-in-seconds: 86400

맨마지막에 jwt설정만 보면 된다.

secret은 hs256을 사용하려고 하니 256bit = 32byte 로 설정하자.


TokenProvider

package com.mogreene.backend.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.List;

/**
 * @name : TokenProvider
 * @author : Mo-Greene
 * @date : 2023/07/05
 * 토큰 생성, 토큰 유효성 검증
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class TokenProvider implements InitializingBean {

    @Value("${jwt.secret}")
    private String secretKey;

    @Value("${jwt.token-validity-in-seconds}")
    private long tokenValidMillisecond;
    private final UserDetailsService userDetailsService;
    private Key key;

    // SecretKey Base64로 인코딩
    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    /**
     * secret key 를 base64 디코드 하기위해 생성
     */
    @Override
    public void afterPropertiesSet() {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

    // JWT 토큰 생성
    public String createToken(Long userId, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(Long.toString(userId));
        claims.put("roles", roles);
        Date now = new Date();

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .signWith(key, SignatureAlgorithm.HS256) // 암호화
                .setExpiration(new Date(now.getTime() + tokenValidMillisecond)) // 토큰 만료일 설정
                .compact();
    }

    // JWT 토큰에서 인증 정보 조회
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserId(token));

        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    // 유저 이름 추출
    public String getUserId(String token) {
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    // JWT 토큰 유효성 체크
    public boolean validateToken(String token) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);

            return !claims.getBody().getExpiration().before(new Date());
        } catch (SecurityException | MalformedJwtException | IllegalArgumentException exception) {
            log.info("잘못된 Jwt 토큰입니다");
        } catch (ExpiredJwtException exception) {
            log.info("만료된 Jwt 토큰입니다");
        } catch (UnsupportedJwtException exception) {
            log.info("지원하지 않는 Jwt 토큰입니다");
        }

        return false;
    }

}

토큰의 생성과 유효성 검증을 담당한다.

 

createToken

클라이언트의 정보가 담겨있는 Claims객체를 만들어 userNo와 role을 받아 생성한다.

 

getAuthentication

Jwt토큰에서 인증정보를 조회한다. 후에 JwtFilter에서 사용한다.

 

validateToken

유효성검증하는 부분이며 나중에 token관련한 에러가 콘솔에 찍힌다.

 


JwtAuthenticationEntryPoint

package com.mogreene.backend.jwt;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

/**
 * @name : JwtAuthenticationEntryPoint
 * @author : Mo-Greene
 * @date : 2023/07/05
 * 401 에러
 */
@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    /**
     * 유효한 자격증명을 제공하지 않고 접근시 401
     * @param request that resulted in an <code>AuthenticationException</code>
     * @param response so that the user agent can begin authentication
     * @param authException that caused the invocation
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {

        log.error("401(Unauthorized) error");

        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}

JwtAccessDeniedHandler

package com.mogreene.backend.jwt;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

/**
 * @name : JwtAccessDeniedHandler
 * @author : Mo-Greene
 * @date : 2023/07/05
 * 403에러
 */
@Slf4j
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    /**
     * 필요한 권한이 없이 접근시 403에러
     * @param request that resulted in an <code>AccessDeniedException</code>
     * @param response so that the user agent can be advised of the failure
     * @param accessDeniedException that caused the invocation
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {

        log.error("403(Access Denied) error");

        response.sendError(HttpServletResponse.SC_FORBIDDEN, "필요한 권한이 없는 상태에서 접근하여 오류가 발생하였습니다.");
    }
}

 

각각 자격증명과 권한에 따라 401, 403에러를 뱉어낸다.

 


SecurityConfig

package com.mogreene.backend.config.security;

import com.mogreene.backend.jwt.JwtAccessDeniedHandler;
import com.mogreene.backend.jwt.JwtAuthenticationEntryPoint;
import com.mogreene.backend.jwt.JwtFilter;
import com.mogreene.backend.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @name : SecurityConfig
 * @author : Mo-Greene
 * @date : 2023/07/05
 * Security Configuration
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final TokenProvider tokenProvider;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

    /**
     * 비밀번호 복호화
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * web,favicon 예외
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) {
        web
                .ignoring()
                .antMatchers(
                        "/favicon.ico"
                );
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //토큰 사용하기에 csrf 사용x
                .csrf().disable()

                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)

                //h2 console 설정
                .and()
                .headers()
                .frameOptions()
                .sameOrigin()

                //세션 사용하지 않기 때문에 STATELESS 지정
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                //토큰이 없는 상태에서 요청하는 api permitAll
                .and()
                .authorizeRequests()
                .antMatchers("/api/login").permitAll()
                .antMatchers("/api/signup").permitAll()
                .antMatchers(
                        HttpMethod.GET, "/**").permitAll()
                //todo 정리해서 블로그 적자 (cors prefilight)
                .antMatchers(
                        HttpMethod.OPTIONS, "/**").permitAll()
                .anyRequest().authenticated()

                //JwtFilter 적용 => Username, Password 인증 전 Jwt filter 실행
                .and()
                .addFilterBefore(new JwtFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);
    }
}

JwtFilter

package com.mogreene.backend.jwt;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

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

/**
 * @name : JwtFilter
 * @author : Mo-Greene
 * @date : 2023/07/05
 * 커스텀 필터
 */
@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends GenericFilterBean {

    public static final String AUTHORIZATION_HEADER = "Authorization";
    private final TokenProvider tokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String jwt = resolveToken(httpServletRequest);
        String requestURI = httpServletRequest.getRequestURI();

        if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            log.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI);
        } else {
            log.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI);
        }

        chain.doFilter(request, response);
    }

    // Request header에서 token 추출
    public String resolveToken(HttpServletRequest request) {
        String token = request.getHeader("Authorization");

        // 가져온 Authorization Header 가 문자열이고, Bearer 로 시작해야 가져옴
        if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
            return token.substring(7);
        }

        return null;
    }
}

기본적인 설정이 끝났다.

 

현재까진 Jwt의 설정과 Security의 설정을 하여 TokenProvider를 통한 토큰생성, 권한정보 검증 후 예외처리까지 처리하였다.

 

 


Repository

 

user-mapper.xml

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mogreene.backend.user.repository.UserRepository">

    <!--회원 이름으로 검색-->
    <select id="findUserByUsername" resultType="UserDTO">
        SELECT
            user_no,
            username,
            password,
            role

        FROM
            users

        WHERE
            username = #{username}
        ;
    </select>

    <!--회원 PK 검색-->
    <select id="findByUserNo" resultType="UserDTO">
        SELECT
            user_no,
            username,
            password,
            role

        FROM
            users

        WHERE
            user_no = #{userNo}
        ;
    </select>

    <!--회원가입-->
    <insert id="signUp" parameterType="UserDTO">
        INSERT INTO
            users (
                   username, password
            )

        VALUES (
                #{username},
                #{password}
        )
        ;
    </insert>
</mapper>

 

UserRepository

package com.mogreene.backend.user.repository;

import com.mogreene.backend.user.dto.UserDTO;
import org.apache.ibatis.annotations.Mapper;

import java.util.Optional;

/**
 * @name : UserRepository
 * @author : Mo-Greene
 * @date : 2023/07/05
 * UserRepository
 */
@Mapper
public interface UserRepository {

    Optional<UserDTO> findUserByUsername(String username);

    Optional<UserDTO> findByUserNo(Long userNo);

    void signUp(UserDTO userDTO);
}

 

UserDTO

package com.mogreene.backend.user.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * @name : UserDTO
 * @author : Mo-Greene
 * @date : 2023/07/05
 * 회원 dto
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO implements UserDetails {

    //회원 번호 (pk)
    private Long userNo;

    //회원 이름
    private String username;

    //비밀번호
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private String password;

    //회원 role
    private String role;


    //아래 UserDetails 구현
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private Collection<? extends GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

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

    @Override
    public String getUsername() {
        return this.username;
    }

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Override
    public boolean isEnabled() {
        return true;
    }
}

@Entity 어노테이션을 사용하지 않고 따로 구현해야 되다보니 UserDTO에 UserDetails에 구현해서 사용한다.

 

LoginDTO

package com.mogreene.backend.user.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @name : LoginDTO
 * @author : Mo-Greene
 * @date : 2023/07/05
 * 로그인 dto
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginDTO {

    //회원 username
    private String username;

    //비밀번호
    private String password;

}

후에 로그인시 사용한다.

 

 


Service

 

UserService

package com.mogreene.backend.user.service;

import com.mogreene.backend.jwt.TokenProvider;
import com.mogreene.backend.user.dto.LoginDTO;
import com.mogreene.backend.user.dto.UserDTO;
import com.mogreene.backend.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.javassist.bytecode.DuplicateMemberException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.security.auth.login.LoginException;
import java.nio.file.attribute.UserPrincipalNotFoundException;
import java.util.Collections;

/**
 * @name : UserService
 * @author : Mo-Greene
 * @date : 2023/07/05
 * 회원 로직
 */
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final TokenProvider tokenProvider;

    /**
     * 회원가입 로직
     * @param userDTO
     * @return
     */
    public UserDTO signUp(UserDTO userDTO) throws DuplicateMemberException {
        if (userRepository.findUserByUsername(userDTO.getUsername()).isPresent()) {
            throw new DuplicateMemberException("이미 가입되어 있는 유저입니다.");
        }

        userDTO.setPassword(passwordEncoder.encode(userDTO.getPassword()));
        userRepository.signUp(userDTO);

        return userRepository.findUserByUsername(userDTO.getUsername()).get();
    }

    /**
     * 로그인 로직
     * @param loginDTO
     * @return
     * @throws LoginException
     */
    public String login(LoginDTO loginDTO) throws LoginException {
        UserDTO userDTO = userRepository.findUserByUsername(loginDTO.getUsername())
                .orElseThrow(() -> new LoginException("잘못된 아이디 입니다."));

        if (!passwordEncoder.matches(loginDTO.getPassword(), userDTO.getPassword())) {
            throw new LoginException("잘못된 비밀번호입니다.");
        }

        return tokenProvider.createToken(userDTO.getUserNo(), Collections.singletonList(userDTO.getRole()));
    }

    /**
     * 유저 조회
     * @param userNo
     * @return
     * @throws UserPrincipalNotFoundException
     */
    public UserDTO findByUserNo(Long userNo) throws UserPrincipalNotFoundException {
        return userRepository.findByUserNo(userNo)
                .orElseThrow(() -> new UserPrincipalNotFoundException("없는 유저입니다."));
    }
}

로그인, 회원가입, 간단한 유저조회 로직을 넣었다.

 

 

CustomUserDetailService

package com.mogreene.backend.user.service;

import com.mogreene.backend.user.dto.UserDTO;
import com.mogreene.backend.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
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 org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * @name : CustomUserDetailService
 * @author : Mo-Greene
 * @date : 2023/07/05
 */
@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    /**
     * pk로 db 검색 후 없을 경우 에러 리턴
     * @param userNo the username identifying the user whose data is required.
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    @Transactional
    public UserDetails loadUserByUsername(String userNo) throws UsernameNotFoundException {
        return userRepository.findByUserNo(Long.valueOf(userNo))
                .map(this::addAuthorities)
                .orElseThrow(() -> new UsernameNotFoundException(userNo + " -> 데이터베이스에서 찾을 수 없습니다."));
    }

    private UserDTO addAuthorities(UserDTO userDTO) {
        userDTO.setAuthorities(List.of(new SimpleGrantedAuthority(userDTO.getRole())));

        return userDTO;
    }
}

 

 

간단한 SpringSecurity 인증과정 아키텍쳐

흐름을 간략히 정리해보자면

  1. 요청이 들어온다.
  2. AuthenticationManager가 권한정보를 확인한다.
  3. 권한정보가 없다면 리턴
  4. 권한정보 확인 후 UserDetailsService를 사용해 User DB검색
  5. 없다면 데이터베이스에서 찾을 수 없다는 예외처리 리턴(나의 경우 CustomUserDetailService에서 loadUserByUsername이 이 역할을 구현한다.)
  6. User테이블에 조회를 끝냈다면 UserDetails를 상속받은 User(UserDto)를 리턴해서 전달해줌

 


Controller

 

package com.mogreene.backend.user.controller;

import com.mogreene.backend.config.responseApi.ApiResponseDTO;
import com.mogreene.backend.jwt.JwtFilter;
import com.mogreene.backend.user.dto.LoginDTO;
import com.mogreene.backend.user.dto.UserDTO;
import com.mogreene.backend.user.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.javassist.bytecode.DuplicateMemberException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

import javax.security.auth.login.LoginException;
import java.nio.file.attribute.UserPrincipalNotFoundException;

/**
 * @name : UserController
 * @author : Mo-Greene
 * @date : 2023/07/05
 * 회원 로직
 */
@Slf4j
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    /**
     * 회원가입 로직
     * @param userDTO
     * @return
     * @throws DuplicateMemberException
     */
    // TODO: 2023/07/05 예외처리 뽑아주자
    @PostMapping("/signup")
    public ResponseEntity<ApiResponseDTO<?>> signUp(@RequestBody UserDTO userDTO) throws DuplicateMemberException {

        UserDTO savedUser = userService.signUp(userDTO);

        ApiResponseDTO<?> apiResponseDTO = ApiResponseDTO.builder()
                .httpStatus(HttpStatus.CREATED)
                .data(savedUser)
                .build();
        return new ResponseEntity<>(apiResponseDTO, HttpStatus.CREATED);
    }

    /**
     * 로그인 token response
     * @param loginDTO
     * @return
     * @throws LoginException
     */
    // TODO: 2023/07/05 예외처리 뽑아주자
    @PostMapping("/login")
    public ResponseEntity<ApiResponseDTO<?>> login(@RequestBody LoginDTO loginDTO) throws LoginException {

        String token = userService.login(loginDTO);

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + token);

        ApiResponseDTO<?> apiResponseDTO = ApiResponseDTO.builder()
                .httpStatus(HttpStatus.OK)
                .data(token)
                .build();
        return ResponseEntity.status(HttpStatus.OK).headers(httpHeaders).body(apiResponseDTO);
    }

    /**
     * 테스트용 유저 조회
     * @param authentication
     * @return
     * @throws UserPrincipalNotFoundException
     */
    // TODO: 2023/07/05 예외처리 뽑아주자
    @GetMapping("/users")
    @PreAuthorize("hasAnyRole('USER')")
    public ResponseEntity<ApiResponseDTO<?>> findUserByUsername(final Authentication authentication) throws UserPrincipalNotFoundException {
        Long userNo = ((UserDTO) authentication.getPrincipal()).getUserNo();

        UserDTO findUser = userService.findByUserNo(userNo);

        ApiResponseDTO<?> apiResponseDTO = ApiResponseDTO.builder()
                .httpStatus(HttpStatus.OK)
                .data(findUser)
                .build();
        return new ResponseEntity<>(apiResponseDTO, HttpStatus.OK);
    }
}

 

이제 테스트를 해보려고 한다.

테스트는 'Postman'으로 진행한다.

 

 

회원가입

 

 

로그인

body에 생성된 토큰이 찍힌다.

 

전역변수 설정

전역변수로 설정하면 후에 쉽게 확인할 수 있다.

 

TestApi

Token값을 확인한 후 어떤 사용자인지 확인하는 테스트이다.

 

이렇게 Mybatis로 jwt를 구현해보았다.

Jpa로 구현하는 블로그들이 많았기에 한번쯤 기록용으로 만들자고 생각했다.

이걸 토대로 프로젝트에 인증권한 파트를 추가해보려고 한다.

'Backend > SpringBoot' 카테고리의 다른 글

네이버 문자(sms) 서비스 구현  (0) 2023.09.12
Swagger Opendoc API  (0) 2023.07.18
SpringBoot 동적인(?) 파일 수정  (0) 2023.05.15
SpringBoot 비동기 댓글  (0) 2023.04.27
SpringBoot 파라미터 달고다니기  (0) 2023.04.26
Comments