Backend/String

RESTAPI

AIHYEONJI 2025. 6. 19. 12:07

build.gradle

BCrypt

BCrypt는 패스워드를 안전하게 암호화하기 위해 널리 사용되는 해시 함수 알고리즘입니다. org.mindrot.jbcrypt.BCrypt는 이 알고리즘을 자바에서 쉽게 사용할 수 있도록 제공하는 라이브러리로, 비밀번호를 해시하여 저장하고, 사용자가 로그인할 때 입력한 비밀번호가 기존 해시와 일치하는지 안전하게 비교할 수 있도록 hashpw()와 checkpw() 메서드를 제공합니다. 이 방식은 단방향 암호화로 복호화가 불가능하고, salt을 자동으로 적용하여 동일한 비밀번호라도 해시값이 달라지기 때문에 무차별 대입 공격(Brute-force)과 레인보우 테이블 공격에 강한 보안성을 갖습니다.

...
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // lombok
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // mysql
    runtimeOnly 'com.mysql:mysql-connector-j'

    // mybatis
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'

    // bcrypt
    implementation 'org.mindrot:jbcrypt:0.4'

    // JSON ,JWT
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

}
...

 

application.properties

spring.application.name=restapi

spring.datasource.url=jdbc:mysql://localhost:3306/spring?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.koreait.restful.dto

 

MemberDTO.java

package com.koreait.restapi.dto;

import lombok.Data;

@Data
public class MemberDTO {
    private int id;
    private String username;
    private String password;
    private String name;
}

 

MemberMapper.java

package com.koreait.restapi.mapper;

import com.koreait.restful.dto.MemberDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface MemberMapper {
    MemberDTO findByUsername(@Param("username") String username);
    void save(MemberDTO member);
    void update(MemberDTO member);
}

 

MemberMapper.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.koreait.restapi.mapper.MemberMapper">

    <select id="findByUsername" resultType="com.koreait.restful.dto.MemberDTO">
        SELECT * FROM member WHERE username = #{username}
    </select>

    <insert id="save" parameterType="com.koreait.restful.dto.MemberDTO">
        INSERT INTO member (username, password, name)
        VALUES (#{username}, #{password}, #{name})
    </insert>

    <update id="update" parameterType="com.koreait.restful.dto.MemberDTO">
        UPDATE member
        SET
        <if test="password != null and password != ''">
            password = #{password},
        </if>
        name = #{name}
        WHERE id = #{id}
    </update>

</mapper>

 

MemberController.java

package com.koreait.restapi.controller;

import com.koreait.restapi.dto.MemberDTO;
import com.koreait.restapi.jwt.JwtUtil;
import com.koreait.restapi.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@CrossOrigin(origins = "http://localhost:5173", allowCredentials = "true") // 프론트 주소
@RestController
@RequestMapping("/api/member")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService service;
    private final JwtUtil jwtUtil;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody MemberDTO loginRequest) {
        String token = service.login(loginRequest.getUsername(), loginRequest.getPassword());
        if (token != null) {
            return ResponseEntity.ok().body(token);
        } else {
            return ResponseEntity.status(401).body("로그인 실패");
        }
    }

    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody MemberDTO member) {
        service.register(member);
        return ResponseEntity.ok("회원가입 성공");
    }

    @GetMapping("/info")
    public ResponseEntity<?> getUserInfo(@RequestHeader("Authorization") String token) {
        MemberDTO member = service.getUserInfoFromToken(token);
        if (member != null) {
            return ResponseEntity.ok(member);
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("사용자를 찾을 수 없습니다.");
        }
    }

    @PutMapping("/update")
    public ResponseEntity<?> update(@RequestHeader("Authorization") String token,
                                    @RequestBody MemberDTO member) {
        service.update(token, member);
        return ResponseEntity.ok("회원정보 수정 성공");
    }

    @PostMapping("/logout")
    public ResponseEntity<?> logout(@RequestHeader("Authorization") String token) {
        service.logout(token);
        return ResponseEntity.ok("로그아웃 성공 (클라이언트에서 토큰 삭제)");
    }
}

 

JwtAuthFilter.java

package com.koreait.restapi.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    public JwtAuthFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

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

        response.setHeader("Access-Control-Allow-Origin", "http://localhost:5173");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
        response.setHeader("Access-Control-Allow-Credentials", "true");

        // 프리플라이트 OPTIONS 요청일 경우 바로 응답
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }

        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            if (!jwtUtil.isTokenValid(token)) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }
        } else if (!request.getRequestURI().contains("login") && !request.getRequestURI().contains("register")) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        filterChain.doFilter(request, response);
    }
}

 

JwtUtil.java

package com.koreait.restful.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

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

@Component
public class JwtUtil {

    private final String SECRET = "MySuperSecretKeyForJWTTokenWhichIsVerySecure12345";
    private final long EXPIRATION = 1000 * 60 * 60; // 1시간

    private final Key key = Keys.hmacShaKeyFor(SECRET.getBytes());

    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    public String getUsernameFromToken(String token) {
        return parseClaims(token).getSubject();
    }

    public boolean isTokenValid(String token) {
        try {
            parseClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    private Claims parseClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

 

MemberService.java

package com.koreait.restful.service;

import com.koreait.restful.dto.MemberDTO;

public interface MemberService {
    String login(String username, String password);
    void register(MemberDTO member);
    void update(String token, MemberDTO updatedMember);
    void logout(String token); // optional
    MemberDTO getUserInfoFromToken(String token);
}

 

MemberServiceImpl.java

package com.koreait.restful.service;

import com.koreait.restful.dto.MemberDTO;
import com.koreait.restful.jwt.JwtUtil;
import com.koreait.restful.mapper.MemberMapper;
import lombok.RequiredArgsConstructor;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {

    private final MemberMapper mapper;
    private final JwtUtil jwtUtil;

    @Override
    public String login(String username, String password) {
        MemberDTO member = mapper.findByUsername(username);
        if (member != null && BCrypt.checkpw(password, member.getPassword())) {
            return jwtUtil.generateToken(member.getUsername());
        }
        return null;
    }

    @Override
    public void register(MemberDTO member) {
        String hashed = BCrypt.hashpw(member.getPassword(), BCrypt.gensalt());
        member.setPassword(hashed);
        mapper.save(member);
    }

    @Override
    public void update(String token, MemberDTO updatedMember) {
        String jwt = token.replace("Bearer ", "");
        String username = jwtUtil.getUsernameFromToken(jwt);

        MemberDTO original = mapper.findByUsername(username);
        if (original != null) {
            if (StringUtils.hasText(updatedMember.getPassword())) {
                String hashed = BCrypt.hashpw(updatedMember.getPassword(), BCrypt.gensalt());
                original.setPassword(hashed);
            }
            if (StringUtils.hasText(updatedMember.getName())) {
                original.setName(updatedMember.getName());
            }
            mapper.update(original);
        }
    }

    @Override
    public MemberDTO getUserInfoFromToken(String token) {
        String jwt = token.replace("Bearer ", "");
        String username = jwtUtil.getUsernameFromToken(jwt);
        MemberDTO member = mapper.findByUsername(username);
        if (member != null) {
            member.setPassword(null);
        }
        return member;
    }

    @Override
    public void logout(String token) {
        // Stateless 방식이라 클라이언트에서 토큰 제거하면 됨
    }
}

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

JPA  (0) 2025.06.30
Thymeleaf  (0) 2025.06.18
스프링 vs 스프링부트  (0) 2025.06.18