config
JasyptConfig
package com.sparta.example.config;
import lombok.RequiredArgsConstructor;
import org.jasypt.encryption.StringEncryptor;
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@RequiredArgsConstructor
@Configuration
public class JasyptConfig {
@Value("${jasypt.encryptor.password}")
private String encryptKey;
@Bean("jasyptStringEncryptor")
public StringEncryptor stringEncryptor() {
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword(encryptKey);
config.setAlgorithm("PBEWithMD5AndDES");
config.setKeyObtentionIterations("1000");
config.setPoolSize("1");
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
config.setIvGeneratorClassName("org.jasypt.iv.NoIvGenerator");
config.setStringOutputType("base64");
encryptor.setConfig(config);
return encryptor;
}
}
JpaConfig
package com.sparta.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@Configuration
public class JpaConfig {
}
controller
AuthController
package com.sparta.example.controller;
import com.sparta.example.dto.user.UserDto;
import com.sparta.example.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthService authService;
@PostMapping("/signup")
public ResponseEntity<?> signup(@RequestBody @Valid final UserDto.SignupReqDto reqDto){
authService.signup(reqDto);
return ResponseEntity.ok(null);
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody final UserDto.LoginReqDto reqDto){
String accessToken = authService.login(reqDto);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.AUTHORIZATION, accessToken);
return new ResponseEntity<>(null, httpHeaders, HttpStatus.OK);
}
}
PostController
package com.sparta.example.controller;
import com.sparta.example.dto.post.PostDto;
import com.sparta.example.service.PostService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RequestMapping("/api/posts")
@RequiredArgsConstructor
@RestController
public class PostController {
private final PostService postService;
@PostMapping
public ResponseEntity<PostDto.DetailResDto> create(@Valid @RequestBody final PostDto.CreateReqDto dto,
@AuthenticationPrincipal final UserDetails userDetails) {
PostDto.DetailResDto resDto = postService.create(Long.parseLong(userDetails.getUsername()), dto);
return ResponseEntity.ok(resDto);
}
@GetMapping
public ResponseEntity<PostDto.AllResDto> readAll() {
PostDto.AllResDto resDtos = postService.readAll();
return ResponseEntity.ok(resDtos);
}
@GetMapping("/{id}")
public ResponseEntity<PostDto.DetailResDto> readOneById(@PathVariable("id") final Long id) {
PostDto.DetailResDto resDto = postService.readOneById(id);
return ResponseEntity.ok(resDto);
}
@PutMapping("/{id}")
public ResponseEntity<?> update(@PathVariable("id") final Long id,
@Valid @RequestBody final PostDto.UpdateReqDto dto,
@AuthenticationPrincipal final UserDetails userDetails) {
postService.update(Long.parseLong(userDetails.getUsername()), id, dto);
return ResponseEntity.ok(null);
}
@DeleteMapping("/{id}")
public ResponseEntity<?> delete(@PathVariable("id") final Long id,
@AuthenticationPrincipal final UserDetails userDetails) {
postService.delete(Long.parseLong(userDetails.getUsername()), id);
return ResponseEntity.ok(null);
}
}
dto
common
ErrorResponse
package com.sparta.example.dto.common;
public record ErrorResponse(String msg) {
}
post
PostDto
package com.sparta.example.dto.post;
import com.sparta.example.entity.Post;
import com.sparta.example.entity.User;
import javax.validation.constraints.Size;
import java.util.List;
public class PostDto {
public record CreateReqDto(@Size(min = 2, max = 20, message = "제목은 2자 이상, 20자 이하로 입력해 주세요") String title,
@Size(min = 1, max = 255, message = "제목은 1자 이상, 255자 이하로 입력해 주세요") String content) {
public Post toEntity(User user) {
return new Post(user, this.title, this.content);
}
}
public record UpdateReqDto(@Size(min = 2, max = 20, message = "제목은 2자 이상, 20자 이하로 입력해 주세요") String title,
@Size(min = 1, max = 255, message = "제목은 1자 이상, 255자 이하로 입력해 주세요")String content) {
}
public record DetailResDto(Long id, String title, String content, String nickname) {
public DetailResDto(Post post){
this(post.getId(), post.getTitle(), post.getContents(), post.getUser().getNickname());
}
}
public record AllResDto(List<SimpleResDto> postList) {
}
public record SimpleResDto(Long id, String title, String nickname) {
public SimpleResDto(Post post) {
this(post.getId(), post.getTitle(), post.getUser().getNickname());
}
}
}
user
UserDto
package com.sparta.example.dto.user;
import com.sparta.example.entity.User;
import com.sparta.example.validation.ValidSignup;
import javax.validation.constraints.NotBlank;
public class UserDto {
@ValidSignup
public record SignupReqDto(String nickname, String password){
public User toEntity(){
return new User(this.nickname, this.password);
}
}
public record LoginReqDto(@NotBlank(message = "닉네임을 입력해 주세요") String nickname,
@NotBlank(message = "비밀번호를 입력해 주세요") String password){
}
}
entity
Post
package com.sparta.example.entity;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Post extends TimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
@Column(nullable = false, columnDefinition = "VARCHAR(20)")
private String title;
@Column(nullable = false, columnDefinition = "VARCHAR(255)")
private String contents;
@ManyToOne(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
private User user;
public Post(User user, String title, String contents){
this.title = title;
this.contents = contents;
this.user = user;
}
public void update(String title, String contents){
this.title = title;
this.contents = contents;
}
public boolean checkMemberByMemberId(Long memberId) {
return this.user.getId().equals(memberId);
}
}
TimeEntity
package com.sparta.example.entity;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.sql.Timestamp;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimeEntity {
@CreatedDate
private Timestamp createdAt;
@LastModifiedDate
private Timestamp updatedAt;
}
User
package com.sparta.example.entity;
import com.sparta.example.util.converter.PasswordEncConverter;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "USERS")
public class User extends TimeEntity{
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
@Column(unique = true, nullable = false, columnDefinition = "VARCHAR(20)")
private String nickname;
@Convert(converter = PasswordEncConverter.class)
@Column(nullable = false, columnDefinition = "VARCHAR(80)")
private String password;
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
private UserRole role;
public User(String nickname, String password){
this.nickname = nickname;
this.password = password;
this.role = UserRole.USER;
}
}
UserRole
package com.sparta.example.entity;
import lombok.Getter;
@Getter
public enum UserRole {
USER(Authority.USER);
private final String authority;
UserRole(String authority) {
this.authority = authority;
}
public static class Authority {
private static final String USER = "ROLE_USER";
}
}
exception
handler
RestControllerExceptionHandler
package com.sparta.example.exception.handler;
import com.sparta.example.dto.common.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.authentication.rememberme.InvalidCookieException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
import java.util.List;
@RestControllerAdvice
public class RestControllerExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public ErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {
List<ObjectError> allErrors = e.getAllErrors();
ObjectError objectError = allErrors.get(0);
String message;
if(objectError.getClass().equals(FieldError.class)){
FieldError fieldError = (FieldError) objectError;
message = fieldError.getField() + " : " + fieldError.getDefaultMessage();
}else{
message = objectError.getDefaultMessage();
}
return new ErrorResponse(message);
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse constraintViolationException(ConstraintViolationException e) {
return new ErrorResponse(e.getMessage());
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse notExistException(IllegalArgumentException e) {
return new ErrorResponse(e.getMessage());
}
}
repository
PostRepository
package com.sparta.example.repository;
import com.sparta.example.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostRepository extends JpaRepository<Post, Long> {
}
UserRepository
package com.sparta.example.repository;
import com.sparta.example.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByNickname(String nickname);
boolean existsByNickname(String nickname);
}
security
config
JwtConfig
package com.sparta.example.security.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.example.security.filter.JwtFilter;
import com.sparta.example.security.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
@RequiredArgsConstructor
public class JwtConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final JwtUtil jwtUtil;
private final ObjectMapper om;
@Override
public void configure(HttpSecurity http) {
JwtFilter jwtFilter = new JwtFilter(jwtUtil, om);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
}
SecurityConfig
package com.sparta.example.security.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.example.security.exceptionhandler.CustomAccessDeniedHandler;
import com.sparta.example.security.exceptionhandler.CustomAuthenticationEntryPoint;
import com.sparta.example.security.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtUtil jwtUtil;
private final ObjectMapper om;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf()
.disable();
http
.authorizeRequests(auth -> auth
.antMatchers("/h2-console/**").permitAll()
.antMatchers("/api/auth/**").permitAll()
.antMatchers(HttpMethod.GET, "/api/posts").permitAll()
.antMatchers(HttpMethod.GET,"/api/posts/{\\d+}").permitAll()
.anyRequest().authenticated());
http
.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint())
.accessDeniedHandler(customAccessDeniedHandler())
.and()
.headers()
.frameOptions()
.disable()
.and()
.cors()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.apply(new JwtConfig(jwtUtil, om));
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://charleybucket.s3-website.ap-northeast-2.amazonaws.com");
config.addExposedHeader(JwtUtil.AUTHORIZATION_HEADER);
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
config.validateAllowCredentials();
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Bean
public CustomAccessDeniedHandler customAccessDeniedHandler(){
return new CustomAccessDeniedHandler(om);
}
@Bean
public CustomAuthenticationEntryPoint customAuthenticationEntryPoint(){
return new CustomAuthenticationEntryPoint(om);
}
@Bean
public PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
exceptionhandler
CustomAccessDeniedHandler
package com.sparta.example.security.exceptionhandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.example.dto.common.ErrorResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RequiredArgsConstructor
public class CustomAccessDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler {
private final ObjectMapper om;
private static final String CUSTOM_DEFAULT_ERROR_MSG = "권한이 없습니다.";
private static final String DEFAULT_ERROR_MSG = "접근이 거부되었습니다.";
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
String errorMsg = accessDeniedException.getMessage().equals(DEFAULT_ERROR_MSG)
? CUSTOM_DEFAULT_ERROR_MSG
: accessDeniedException.getMessage();
ErrorResponse errorResponse = new ErrorResponse(errorMsg);
String result = om.writeValueAsString(errorResponse);
response.getWriter().write(result);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
}
CustomAuthenticationEntryPoint
package com.sparta.example.security.exceptionhandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.example.dto.common.ErrorResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RequiredArgsConstructor
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper om;
private static final String DEFAULT_ERROR_MSG = "Full authentication is required to access this resource";
private static final String CUSTOM_DEFAULT_ERROR_MSG = "로그인이 필요합니다";
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
String errorMsg = authException.getMessage();
if(errorMsg.equals(DEFAULT_ERROR_MSG)){
errorMsg = CUSTOM_DEFAULT_ERROR_MSG;
}
ErrorResponse errorResponse = new ErrorResponse(errorMsg);
String result = om.writeValueAsString(errorResponse);
response.getWriter().write(result);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
filter
JwtFilter
package com.sparta.example.security.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.example.dto.common.ErrorResponse;
import com.sparta.example.security.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.http.HttpMethod;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.rememberme.InvalidCookieException;
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;
@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final ObjectMapper om;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = jwtUtil.extractToken(request.getHeader(JwtUtil.AUTHORIZATION_HEADER));
if (StringUtils.hasText(token)) {
try {
jwtUtil.validateToken(token);
setAuthentication(token);
filterChain.doFilter(request, response);
} catch (InvalidCookieException e) {
sendErrorMsg(e, response);
}
} else {
filterChain.doFilter(request, response);
}
}
private void setAuthentication(String token) {
Authentication authentication = jwtUtil.getAuthentication(token);
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
}
private void sendErrorMsg(Exception e, HttpServletResponse response) {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ErrorResponse errorResponse = new ErrorResponse(e.getMessage());
try {
String result = om.writeValueAsString(errorResponse);
response.getWriter().write(result);
} catch (IOException ex) {
log.error(ex.getMessage(), ex);
}
}
}
jwt
JwtUtil
package com.sparta.example.security.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.rememberme.InvalidCookieException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.security.Key;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Component
@Slf4j
public class JwtUtil {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
private static final String AUTHORITY_KEY = "auth";
private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30;
private final Key key;
public JwtUtil(@Value("${jwt.secret}") String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
public String extractToken(String bearerToken){
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(JwtUtil.TOKEN_PREFIX)){
return bearerToken.substring(7);
}
return null;
}
public String generateToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining());
Date accessTokenExpiresIn = new Date(new Date().getTime() + ACCESS_TOKEN_EXPIRE_TIME);
String accessToken = Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITY_KEY, authorities)
.setExpiration(accessTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS512)
.compact();
return TOKEN_PREFIX + accessToken;
}
public Authentication getAuthentication(String accessToken) {
Claims claims = parseClaims(accessToken);
if (claims.get(AUTHORITY_KEY) == null) throw new InvalidCookieException("로그인 정보가 잘못되었습니다. 다시 로그인해주세요.");
List<SimpleGrantedAuthority> authorities = Arrays.stream(claims.get(AUTHORITY_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
UserDetails principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
public void validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
} catch (SecurityException | MalformedJwtException e) {
log.error("잘못된 JWT 서명입니다.", e);
throw new InvalidCookieException("로그인 정보가 잘못되었습니다. 다시 로그인해주세요.");
} catch (ExpiredJwtException e) {
log.error("만료된 JWT 토큰입니다.", e);
throw new InvalidCookieException("로그인 시간이 만료되었습니다. 다시 로그인해주세요.");
} catch (UnsupportedJwtException e) {
log.error("지원되지 않는 JWT 토큰입니다.", e);
throw new InvalidCookieException("로그인 정보가 잘못되었습니다. 다시 로그인해주세요.");
} catch (IllegalArgumentException e) {
log.error("JWT 토큰이 잘못되었습니다.");
throw new InvalidCookieException("로그인 정보가 잘못되었습니다. 다시 로그인해주세요.");
}
}
private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
service
UserDetailsServiceImpl
package com.sparta.example.security.service;
import com.sparta.example.entity.User;
import com.sparta.example.repository.UserRepository;
import lombok.RequiredArgsConstructor;
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.Component;
import java.util.Collections;
@RequiredArgsConstructor
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByNickname(username)
.map(this::createUserDetails)
.orElse(null);
}
private UserDetails createUserDetails(User user){
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(user.getRole().toString());
return new org.springframework.security.core.userdetails.User(
String.valueOf(user.getId()),
user.getPassword(),
Collections.singleton(grantedAuthority));
}
}
CustomAuthenticationProvider
package com.sparta.example.security;
import com.sparta.example.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if(userDetails == null){
throw new UsernameNotFoundException("입력하신 회원 닉네임 정보가 없습니다");
} else if (!passwordEncoder.matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("비밀번호가 일치하지 않습니다");
}
return new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
service
AuthService
package com.sparta.example.service;
import com.sparta.example.dto.user.UserDto;
import com.sparta.example.repository.UserRepository;
import com.sparta.example.security.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Transactional
@Service
public class AuthService {
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
public void signup(final UserDto.SignupReqDto dto){
userRepository.save(dto.toEntity());
}
public String login(UserDto.LoginReqDto dto){
UsernamePasswordAuthenticationToken beforeAuthentication = new UsernamePasswordAuthenticationToken(dto.nickname(), dto.password());
Authentication afterAuthentication = authenticationManagerBuilder.getObject().authenticate(beforeAuthentication);
return jwtUtil.generateToken(afterAuthentication);
}
}
PostService
package com.sparta.example.service;
import com.sparta.example.dto.post.PostDto;
import com.sparta.example.entity.Post;
import com.sparta.example.entity.User;
import com.sparta.example.repository.PostRepository;
import com.sparta.example.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Transactional
@Service
public class PostService {
private final UserRepository userRepository;
private final PostRepository postRepository;
public PostDto.DetailResDto create(final Long userId, final PostDto.CreateReqDto dto) {
User user = getUserByIdIfExists(userId);
Post post = dto.toEntity(user);
return new PostDto.DetailResDto(postRepository.save(post));
}
@Transactional(readOnly = true)
public PostDto.AllResDto readAll() {
List<PostDto.SimpleResDto> simpleResDtos = postRepository.findAll().stream().map(
PostDto.SimpleResDto::new).collect(Collectors.toList());
return new PostDto.AllResDto(simpleResDtos);
}
@Transactional(readOnly = true)
public PostDto.DetailResDto readOneById(final Long postId) {
Post post = getPostByIdIfExists(postId);
return new PostDto.DetailResDto(post);
}
public void update(final Long userId, final Long postId, final PostDto.UpdateReqDto dto) {
Post post = getPostByIdIfExists(postId);
throwExceptionIfNotOwner(post, userId);
post.update(dto.title(), dto.content());
}
public void delete(final Long userId, final Long postId) {
Post post = getPostByIdIfExists(postId);
throwExceptionIfNotOwner(post, userId);
postRepository.deleteById(postId);
}
private void throwExceptionIfNotOwner(final Post post, final Long memberId) {
if (!post.checkMemberByMemberId(memberId)) {
throw new AccessDeniedException("회원님이 작성한 글이 아닙니다.");
}
}
private Post getPostByIdIfExists(final Long postId) {
return postRepository.findById(postId).orElseThrow(
() -> new IllegalArgumentException("존재하지 않는 게시글입니다")
);
}
private User getUserByIdIfExists(Long userId) {
return userRepository.findById(userId).orElseThrow(
() -> new IllegalArgumentException("존재하지 않는 회원입니다")
);
}
}
util
converter
PasswordEncConverter
package com.sparta.example.util.converter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
@Converter
@Component
@RequiredArgsConstructor
public class PasswordEncConverter implements AttributeConverter<String, String> {
private final PasswordEncoder passwordEncoder;
@Override
public String convertToDatabaseColumn(String attribute) {
return passwordEncoder.encode(attribute);
}
@Override
public String convertToEntityAttribute(String dbData) {
return dbData;
}
}
validation
SignupValidator
package com.sparta.example.validation;
import com.sparta.example.dto.user.UserDto;
import com.sparta.example.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
@Component
@RequiredArgsConstructor
public class SignupValidator implements ConstraintValidator<ValidSignup, UserDto.SignupReqDto> {
private final UserRepository userRepository;
@Override
public void initialize(ValidSignup constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(UserDto.SignupReqDto value, ConstraintValidatorContext context) {
String nickname = value.nickname();
String password = value.password();
if(!StringUtils.hasText(nickname)){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("닉네임을 입력해 주세요").addConstraintViolation();
return false;
}else if(!Pattern.matches("(?=.*[a-z])(?=.*\\d)[a-z\\d]{4,10}$" ,nickname)){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("닉네임은 소문자, 숫자를 필수로 포함한 4-10자로 입력할 수 있습니다").addConstraintViolation();
return false;
}else if(userRepository.existsByNickname(nickname)){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("이미 존재하는 닉네임입니다").addConstraintViolation();
return false;
}
if(!StringUtils.hasText(password)){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("비밀번호를 입력해 주세요").addConstraintViolation();
return false;
}else if(!Pattern.matches("(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*])(?=.*\\d)[a-zA-Z0-9!@#$%^&*]{8,15}$" ,password)){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("비밀번호는 소문자, 대문자, 숫자, 특수문자(!@#$%^&*)를 필수로 포함한 8-15자로 입력할 수 있습니다").addConstraintViolation();
return false;
}
return true;
}
}
ValidSignup
package com.sparta.example.validation;
import javax.validation.Constraint;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = SignupValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidSignup {
String message() default "";
Class[] groups() default {};
Class[] payload() default {};
}
application.yml
# 분리된 properties 파일을 그룹화 및 실행할 profile 지정
spring:
profiles:
active:
- local
group:
local:
- db-local
- common-local
dev:
- db-dev
- common-dev
include:
- db
- common
# yml 파일을 둘로 나눈 이유?
# 1. 개발 환경과 배포 환경이 다른데, 개발 할때마다 매번 yml 을 작성할 수도 없는 노릇. 그래서 미리 분리를 시켜놓고 관리.
# 2. 하나의 yml 파일에 모든 기능을 넣으면 찾기 어려움. 그래서 쉽게 찾아서 관리 할 수 있도록 분리시킴
application-common.yml
# 공통 적용 부분
# 깃허브에 올릴 때, 보안 문제 때문에 jwt 가 암호화되도록 만듦
jwt:
secret: ENC(evMIiXSPQ5odTUYlKFZhBSpXFmwAKAeYx10sX/gfE5vB88tMf7WyYeu4Mx9YC16lpu300wBzW7jW3VJQ3Bo4cWAyWy7qIeNDTslZ73VE5EyJzuGrs2xPYq5TNZP9ap4DsRpltDH11IY=)
---
# local 환경에서 적용
spring:
config:
activate:
on-profile: "common-local"
#HTTP 요청 로그 - 참고 https://soobindeveloper8.tistory.com/765
# 이걸 넣으면, http method, url, host, header, ... 등 어떤 요청이 들어왔는지 확인 가능하도록 출력된다.
logging:
level:
org.apache.coyote.http11: debug
---
# dev 환경에서 적용
spring:
config:
activate:
on-profile: "common-dev"
logging:
level:
org.apache.coyote.http11: debug
application-db.yml
# 공통 적용
spring:
jpa:
open-in-view: false
properties:
hibernate:
jdbc.batch_size: 100
default_fetch_size: 100
order_inserts: true
order_updates: true
---
# local 환경에서 적용
spring:
config:
activate:
on-profile: "db-local"
h2:
console:
enabled: true
path: /h2-console
jpa:
show-sql: true
database: H2
hibernate:
ddl-auto: create-drop
properties:
hibernate:
format_sql: true
datasource:
hikari:
driver-class-name: org.h2.Driver
jdbc-url: jdbc:h2:mem://localhost/~/board;MODE=MySQL;
username: sa
password:
---
# dev 환경에서 적용
spring:
config:
activate:
on-profile: "db-dev"
jpa:
show-sql: true
database: MYSQL
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
datasource:
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: ENC() # 내부에 여러분의 암호화된 jdbc-url을 넣어주시면 됩니다!
username: ENC() # 내부에 여러분의 암호화된 username을 넣어주시면 됩니다!
password: ENC() # 내부에 여러분의 암호화된 password를 넣어주시면 됩니다!
max-lifetime: 60000
connection-timeout: 3000
maximum-pool-size: 10
build.gradle
plugins {
id 'org.springframework.boot' version '2.7.5'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
id 'java'
}
group = 'com.sparta'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
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 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4'
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'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
tasks.named('test') {
exclude '**/*'
useJUnitPlatform()
}