코드 그라데이션
[중간점검] 또 여기까지 전체 코드 정리 / 버전 2.5.3 본문
Config
CorsConfig
@Configuration
public class CorsConfig {
// CORS 필터 빈을 정의하는 메서드
@Bean
public CorsFilter corsFilter() {
// URL 기반의 CORS 구성을 관리하는 객체 생성
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// CORS 구성 설정 객체 생성
CorsConfiguration config = new CorsConfiguration();
// 요청에서 자격 증명(자격 증명 쿠키, 인증 등)을 허용
config.setAllowCredentials(true);
// 모든 출처(Origin)를 허용
config.addAllowedOrigin("*");
// 모든 HTTP 헤더를 허용
config.addAllowedHeader("*");
// 모든 HTTP 메서드(GET, POST, PUT, DELETE 등)를 허용
config.addAllowedMethod("*");
// "/api/**" 패턴에 해당하는 엔드포인트에 대해 CORS 구성을 등록
source.registerCorsConfiguration("/api/**", config);
// CORS 필터를 생성하고 반환
return new CorsFilter(source);
}
}
SecurityConfig
@EnableWebSecurity // 기본적인 웹 시큐리티 설정을 활성화하겠다.
// 추가적 설정을 위해서 WebSecurityConfigurer 을 implement 하거나
// WebSecurityConfigureAdapter 를 extends 하는 방법이 있다.
@EnableGlobalMethodSecurity(prePostEnabled = true) // @PreAuthorize 어노테이션을 메서드 단위로 추가하기 위해서 사용
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final TokenProvider tokenProvider;
private final CorsFilter corsFilter;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* h2-console 하위 모든 요청들과 파비콘 관련 요청은
* Spring Security 로직을 수행하지 않고 접근할 수 있게 configure 메서드를 오버라이드해서 내용을 추가해준다.
*/
@Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers(
"/h2-console/**"
,"/favicon.ico"
,"/error"
);
}
/**
*
* @param http the {@link HttpSecurity} to modify
* @throws Exception
*
* 많은 부분들을 추가
* : 토큰을 사용하기 때문에 csrf 설정은 disable
* Exception을 핸들링할 때 만들었던 클래스들을 추가한다.
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// token을 사용하는 방식이기 때문에 csrf를 disable한다.
http.csrf().disable()
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// h2-console을 위한 설정
.and()
.headers()
.frameOptions()
.sameOrigin()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // session을 사용하지 않음.
.and()
.authorizeRequests() // HttpServletRequest를 사용하는 요청들에 대한 접근 제한을 설정하겠다.
.antMatchers("/api/hello").permitAll() // /api/hello 에 대한 요청은 인증 없이 접근을 허용하겠다.
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/api/signup").permitAll()
.anyRequest().authenticated() // 나머지 요청들에 대해서는 인증을 받아야 한다.
.and()
.apply(new JwtSecurityConfig(tokenProvider));
}
}
Controller
AuthController
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class AuthController {
private final TokenProvider tokenProvider;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
@PostMapping("/authenticate")
public ResponseEntity<TokenDto> authorize(@Valid @RequestBody LoginDto loginDto) {
// 사용자가 제공한 로그인 정보(username과 password)를 사용하여
// UsernamePasswordAuthenticationToken을 생성
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());
// AuthenticationManagerBuilder를 사용하여
// 생성된 authenticationToken을 인증
Authentication authentication
= authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// 인증에 성공한 경우, SecurityContextHolder에 현재 인증 정보를 설정
SecurityContextHolder.getContext().setAuthentication(authentication);
// TokenProvider를 사용하여 JWT(JSON Web Token)를 생성
String jwt = tokenProvider.createToken(authentication);
// HTTP 응답 헤더에 JWT 토큰을 추가
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwt);
// JWT 토큰을 포함한 TokenDto를 응답으로 반환
return new ResponseEntity<>(new TokenDto(jwt), httpHeaders, HttpStatus.OK);
}
}
UserController
@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
/**
* 사용자 등록을 처리하는 엔드포인트.
*
* @param userDto 등록할 사용자 정보를 포함한 UserDto 객체
* @return ResponseEntity<UserDto> 등록된 사용자 정보를 포함한 ResponseEntity
*/
@PostMapping("/signup")
public ResponseEntity<UserDto> signup(@Valid @RequestBody UserDto userDto) {
return ResponseEntity.ok(userService.signup(userDto));
}
/**
* 현재 사용자의 정보를 조회하는 엔드포인트.
*
* @param request HttpServletRequest 객체
* @return ResponseEntity<UserDto> 현재 사용자 정보를 포함한 ResponseEntity
*/
@GetMapping("/user")
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public ResponseEntity<UserDto> getMyUserInfo(HttpServletRequest request) {
return ResponseEntity.ok(userService.getMyUserWithAuthorities());
}
/**
* 리디렉션 테스트를 위한 엔드포인트.
*
* @param response HttpServletResponse 객체
* @throws IOException 입출력 예외 발생 시
*/
@PostMapping("/test-redirect")
public void testRedirect(HttpServletResponse response) throws IOException {
response.sendRedirect("/api/user");
}
/**
* 특정 사용자의 정보를 조회하는 엔드포인트.
*
* @param username 조회할 사용자의 이름
* @return ResponseEntity<UserDto> 조회된 사용자 정보를 포함한 ResponseEntity
*/
@GetMapping("/user/{username}")
@PreAuthorize("hasAnyRole('ADMIN')")
public ResponseEntity<UserDto> getUserInfo(@PathVariable String username) {
return ResponseEntity.ok(userService.getUserWithAuthorities(username));
}
}
DTO
AuthorityDto
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthorityDto {
private String authorityName;
}
ErrorDto
// ErrorDTO 클래스는 API 또는 웹 애플리케이션에서 예외 또는 오류 정보를 표현하는 데 사용됩니다.
public class ErrorDto {
// 상태 코드를 나타내는 변수. HTTP 상태 코드와 관련이 있으며, 클라이언트에게 반환됨.
private final int status;
// 클라이언트에게 오류의 원인을 설명하는 데 사용.
private final String message;
// 필드 오류(FieldError) 객체들을 저장하는 리스트.
// 필드 오류는 양식 유효성 검사와 관련이 있으며, 특정 필드에 대한 오류를 저장.
private List<FieldError> fieldErrors = new ArrayList<>();
// ErrorDTO 클래스의 생성자. 상태 코드와 메시지를 전달하여 객체를 초기화.
public ErrorDto(int status, String message) {
this.status = status;
this.message = message;
}
public int getStatus() {
return status;
}
public String getMessage() {
return message;
}
// 필드 오류(FieldError)를 추가하는 메서드입
// 이 메서드를 사용하여 특정 필드에 대한 오류를 ErrorDTO 객체에 추가할 수 있다.
public void addFieldError(String objectName, String path, String message) {
FieldError error = new FieldError(objectName, path, message);
fieldErrors.add(error);
}
public List<FieldError> getFieldErrors() {
return fieldErrors;
}
}
LoginDto
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginDto {
@NotNull
@Size(min = 3, max = 50) // 유효성 검사
private String username;
@NotNull
@Size(min = 3, max = 100)
private String password;
}
TokenDto
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TokenDto {
private String token;
}
UserDto
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserDto {
@NotNull
@Size(min = 3, max = 50)
private String username;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@NotNull
@Size(min = 3, max = 100)
private String password;
@NotNull
@Size(min = 3, max = 50)
private String nickname;
// 추가: UserDto 클래스에 있는 authorityDtoSet 필드를 선언.
private Set<AuthorityDto> authorityDtoSet;
// 정적 메서드인 from(User user)를 정의.
public static UserDto from(User user) {
// 만약 입력으로 주어진 user가 null이면 null을 반환.
if (user == null) return null;
// UserDto 객체를 빌더 패턴을 사용하여 생성.
return UserDto.builder()
// User 객체의 username 값을 UserDto 객체의 username에 설정.
.username(user.getUsername())
// User 객체의 nickname 값을 UserDto 객체의 nickname에 설정.
.nickname(user.getNickname())
// User 객체의 authorities (권한) 집합을 스트림으로 변환한 후,
// 각 권한을 AuthorityDto 객체로 매핑하여 새로운 Set으로 수집.
.authorityDtoSet(user.getAuthorities().stream()
.map(authority -> AuthorityDto.builder().authorityName(authority.getAuthorityName()).build())
.collect(Collectors.toSet()))
// UserDto 객체를 빌드하여 반환합니다.
.build();
}
}
Entity
Authority
@Entity
@Table(name = "authority")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Authority {
@Id
@Column(name = "authority_name", length = 50)
private String authorityName; // 권한 이름
}
User
@Entity
@Table(name = "user")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long userId;
@Column(name = "username", length = 50, unique = true)
private String username;
@Column(name = "password")
private String password;
@Column(name = "nickname", length = 50)
private String nickname;
@Column(name = "activated")
private boolean activated;
@ManyToMany
@JoinTable( // 다대다를 일대다, 다대일 관계로 재정의했다는 뜻.
name = "user_authority",
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "user_id")},
inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "authority_name")})
private Set<Authority> authorities;
}
Exception
DuplicateMemberException
// 사용자 정의 예외 클래스인 DuplicateMemberException 을 정의.
public class DuplicateMemberException extends RuntimeException {
// 기본 생성자
public DuplicateMemberException() {
super();
}
// 메시지와 원인 예외를 받는 생성자
public DuplicateMemberException(String message, Throwable cause) {
super(message, cause);
}
// 메시지를 받는 생성자
public DuplicateMemberException(String message) {
super(message);
}
// 원인 예외를 받는 생성자
public DuplicateMemberException(Throwable cause) {
super(cause);
}
}
Handler
MethodArgumentNotValidExceptionHandler
// 이 어노테이션은 클래스가 예외 처리기로 사용될 때 우선순위를 지정.
// Ordered.HIGHEST_PRECEDENCE는 가장 높은 우선순위를 나타내며,
// 이 클래스가 다른 예외 처리기보다 먼저 실행됨을 의미.
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class MethodArgumentNotValidExceptionHandler {
// @ControllerAdvice 어노테이션이 지정된 클래스는 전역 예외 처리를 위한 클래스임을 알려준다.
@ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
// @ResponseStatus 어노테이션은 해당 메서드가 호출될 때 HTTP 응답 상태 코드를 CONFLICT(409)로 설정하도록 지정한다.
@ResponseStatus(HttpStatus.CONFLICT)
// @ExceptionHandler 어노테이션은 특정 예외 타입(DuplicateMemberException)을 처리하기 위한 메서드를 지정한다.
@ExceptionHandler(value = { DuplicateMemberException.class })
// @ResponseBody 어노테이션은 해당 메서드가 HTTP 응답 본문에 데이터를 직렬화하여 반환함을 나타낸다.
@ResponseBody
// ResponseEntityExceptionHandler 클래스를 확장하여 예외를 처리하는 메서드입니다.
// 이 메서드는 RuntimeException을 받고, 해당 예외와 WebRequest를 사용하여 ErrorDto를 생성하고 반환.
protected ErrorDto badRequest(RuntimeException ex, WebRequest request) {
// ErrorDto 객체를 생성하고 HTTP 응답 상태 코드와 예외 메시지를 설정하여 반환.
return new ErrorDto(HttpStatus.CONFLICT.value(), ex.getMessage());
}
}
}
RestResponseEntityException
// @ControllerAdvice 어노테이션이 지정된 클래스는 전역 예외 처리를 위한 클래스임을 나타냅니다.
@ControllerAdvice
public class RestResponseEntityException extends ResponseEntityExceptionHandler {
// @ResponseStatus 어노테이션은 해당 메서드가 호출될 때 HTTP 응답 상태 코드를 CONFLICT(409)로 설정하도록 지정.
@ResponseStatus(HttpStatus.CONFLICT)
// @ExceptionHandler 어노테이션은 특정 예외 타입(DuplicateMemberException)을 처리하기 위한 메서드를 지정.
@ExceptionHandler(value = { DuplicateMemberException.class })
// @ResponseBody 어노테이션은 해당 메서드가 HTTP 응답 본문에 데이터를 직렬화하여 반환함.
@ResponseBody
// ResponseEntityExceptionHandler 클래스를 확장하여 예외를 처리하는 메서드.
// 이 메서드는 RuntimeException을 받고, 해당 예외와 WebRequest를 사용하여 ErrorDto를 생성하고 반환.
protected ErrorDto badRequest(RuntimeException ex, WebRequest request) {
// ErrorDto 객체를 생성하고 HTTP 응답 상태 코드와 예외 메시지를 설정하여 반환.
return new ErrorDto(HttpStatus.CONFLICT.value(), ex.getMessage());
}
}
jwt
JwtAccessDeniedHandler
// 필요한 권한이 존재하지 않는 경우에 403 에러 리턴하기 위함
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
JwtAuthenticationEntryPoint
// 유효한 자격증명을 제공하지 않고 접근하려고 할 때 401 Unauthorized 에러를 리턴할 클래스
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED); // 401에러
}
}
JwtFilter
public class JwtFilter extends GenericFilterBean {
private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
// Authorization 헤더의 이름
public static final String AUTHORIZATION_HEADER = "Authorization";
private TokenProvider tokenProvider;
public JwtFilter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
/**
*
* @param servletRequest The request to process
* @param servletResponse The response associated with the request
* @param filterChain Provides access to the next filter in the chain for this
* filter to pass the request and response to for further
* processing
*
* @throws IOException
* @throws ServletException
*
* 실제 필터링 로직은 이 메서드 내부에 작성
* 이것은 토큰의 인증정보를 SecurityContext에 저장하는 역할을 수행
*/
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = resolveToken(httpServletRequest);
String requestURI = httpServletRequest.getRequestURI();
// 추출한 JWT 토큰이 유효한지 검증
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
// JWT 토큰을 사용하여 사용자 인증 정보 생성
Authentication authentication = tokenProvider.getAuthentication(jwt);
// 정상 토큰이면 Spring Security의 SecurityContextHolder에 인증 정보 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
// 로깅
logger.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI);
} else {
// 유효한 JWT 토큰이 없을 경우 로깅
logger.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI);
}
// 다음 필터 또는 요청 처리로 전달
filterChain.doFilter(servletRequest, servletResponse);
}
/**
*
* @param request
* @return
* Request Header에서 토큰 정보를 꺼내오기 위한 resolveToken 메서드
*/
// Authorization 헤더에서 JWT 토큰 추출
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
JwtSecurityConfig
/**
* TokenProvider, JwtFilter를 SecurityConfig에 적용할 때 사용할 클래스
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final TokenProvider tokenProvider;
@Override
public void configure(HttpSecurity http) throws Exception {
JwtFilter customFilter = new JwtFilter(tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
TokenProvider
@Component
public class TokenProvider implements InitializingBean {
private final Logger logger = LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY = "auth";
private final String secret;
private final long tokenValidityInMilliseconds;
private Key key;
public TokenProvider(
@Value("${jwt.secret}") String secret,
@Value("${jwt.token-validity-in-seconds}") long tokenValidityInSeconds) {
this.secret = secret;
this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
}
@Override
public void afterPropertiesSet() {
// JWT 서명을 위한 Key 생성
byte[] keyBytes = Decoders.BASE64.decode(secret);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
/**
* InitializingBean을 implements 해서 afterPropertiesSet을 오버라이드 한 이유는
* 빈이 생성 되고 주입을 받은 후에 secret 값을 Base64 Decode 해서 key 변수에 할당하기 위함.
*/
/**
*
* @param authentication
* @return
*
* Authentication 객체의 권한정보를 이용해서 토큰을 생성하는 createToken 메서드 추가
*/
public String createToken(Authentication authentication) {
// 사용자 권한을 JWT에 추가
String authorities = authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
long now = (new Date()).getTime();
Date validity = new Date(now + this.tokenValidityInMilliseconds); // 토큰 만료 시간
// JWT 토큰 생성
return Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.signWith(key, SignatureAlgorithm.HS512)
.setExpiration(validity)
.compact();
}
/**
*
* @param token
* @return
*
* Token에 담겨있는 정보를 이용해 Authentication 객체를 리턴하는 메서드 생성
*/
public Authentication getAuthentication(String token) {
// JWT 토큰 파싱
// 토큰을 파라미터로 받아서 클레임을 만들고
Claims claims = Jwts
.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
// 사용자 권한 파싱 및 Principal 생성
Collection<? extends GrantedAuthority> authorities = // 클레임에서 권한정보들을 빼내서
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 권한정보들을 이용해서 User 객체를 만들어 주고,
User principal = new User(claims.getSubject(), "", authorities);
// 유저객체와 토큰, 권한정보를 이용해서 최종적으로
// Authentication 객체 반환
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
/**
*
* @param token
* @return
* 토큰을 파라미터로 받아서 토큰의 유효성 검사를 수행하는 validateToken 메서드 추가
*/
public boolean validateToken(String token) {
try {
// 토큰을 파싱해보고
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
// 나오는 Exception들을 캐치하고
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
logger.info("잘못된 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
logger.info("만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
logger.info("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
logger.info("JWT 토큰이 잘못되었습니다.");
}
return false;
}
}
Repository
UserRepository(인터페이스)
public interface UserRepository extends JpaRepository<User, Long> {
// 해당 쿼리 수행 될 때 Eager 조회로 authorities 정보를 같이 가져오게 된다.
@EntityGraph(attributePaths = "authorities")
Optional<User> findOneWithAuthoritiesByUsername(String username);
//username을 기준으로 User 정보를 가져올 때 권한 정보도 같이 가져오게 된다.
}
Service
CustomUserDetailsService
@Component("userDetailsService")
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// UserRepository를 사용하여 사용자 정보를 데이터베이스에서 검색
return userRepository.findOneWithAuthoritiesByUsername(username)
.map(user -> createUser(username, user))
.orElseThrow(() -> new UsernameNotFoundException(username + " -> 데이터베이스에서 찾을 수 없습니다."));
}
// 사용자 정보를 기반으로 UserDetails 객체를 생성하는 메서드
private org.springframework.security.core.userdetails.User createUser(String username, User user) {
if (!user.isActivated()) { // 사용자가 활성화되지 않은 경우 예외 던짐
throw new RuntimeException(username + " -> 활성화되어 있지 않습니다.");
}
// 사용자의 권한 정보를 가져와서 SimpleGrantedAuthority 객체로 변환
List<GrantedAuthority> grantedAuthorities = user.getAuthorities()
.stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName()))
.collect(Collectors.toList());
// UserDetails 인터페이스를 구현한 객체를 생성하여 반환
return new org.springframework.security.core.userdetails.User(user.getUsername(),
user.getPassword(), grantedAuthorities);
}
}
UserService
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
/**
*
* @param userDto
* @return
*/
@Transactional
public UserDto signup(UserDto userDto) {
if (userRepository.findOneWithAuthoritiesByUsername(userDto.getUsername()).orElse(null) != null) {
throw new DuplicateMemberException("이미 가입되어 있는 유저입니다."); // 변경
}
// 권한 정보 생성
Authority authority = Authority.builder()
.authorityName("ROLE_USER")
.build();
// 유저 정보 생성
User user = User.builder()
.username(userDto.getUsername())
.password(passwordEncoder.encode(userDto.getPassword()))
.nickname(userDto.getNickname())
.authorities(Collections.singleton(authority))
.activated(true)
.build();
// 저장
return UserDto.from(userRepository.save(user));
}
/**
*
* @param username
* @return
*/
@Transactional(readOnly = true)
public UserDto getUserWithAuthorities(String username) {
return UserDto.from(userRepository.findOneWithAuthoritiesByUsername(username).orElse(null));
}
/**
*
* @return
*/
@Transactional(readOnly = true)
public UserDto getMyUserWithAuthorities() {
return UserDto.from(SecurityUtil.getCurrentUsername().flatMap(userRepository::findOneWithAuthoritiesByUsername).orElse(null));
}
}
Util
SecurityUtil
@NoArgsConstructor
public class SecurityUtil {
private static final Logger logger = LoggerFactory.getLogger(SecurityUtil.class);
/**
* 이 메서드의 역할은 Security Context의 Authentication 객체를 이용해 username을 리턴해주는 간단한 유틸성 메서드
*
* @return 현재 사용자의 username을 Optional<String> 형태로 반환. 인증 정보가 없으면 Optional.empty() 반환.
*/
public static Optional<String> getCurrentUsername() {
// 현재의 Authentication 객체를 가져온다.
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 인증 정보가 없는 경우
if (authentication == null) {
logger.debug("Security Context에 인증 정보가 없습니다.");
return Optional.empty(); // 빈 Optional 반환
}
String username = null;
// Authentication 객체의 주체(principal)를 확인
if (authentication.getPrincipal() instanceof UserDetails) {
UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
username = springSecurityUser.getUsername(); // UserDetails에서 username을 가져옴
}
else if (authentication.getPrincipal() instanceof String) {
username = (String) authentication.getPrincipal(); // 문자열로 된 주체의 경우, username으로 설정
}
return Optional.ofNullable(username); // Optional로 username 반환 (null인 경우에도 처리)
}
}
설정파일
application.yml
spring:
h2:
console:
enabled: true
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
properties:
hibernate:
format_sql: true
show_sql: true
defer-datasource-initialization: true
jwt:
header: Authorization
#HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야 한다.
#echo 'silvernine-tech-spring-boot-jwt-tutorial-secret-silvernine-tech-spring-boot-jwt-tutorial-secret'|base64
secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK
token-validity-in-seconds: 86400
logging:
level:
inflearn.freejwt: DEBUG
data.sql
INSERT INTO USER (USER_ID, USERNAME, PASSWORD, NICKNAME, ACTIVATED) VALUES (1, 'admin', '$2a$08$lDnHPz7eUkSi6ao14Twuau08mzhWrL4kyZGGU5xfiGALO/Vxd5DOi', 'admin', 1);
INSERT INTO USER (USER_ID, USERNAME, PASSWORD, NICKNAME, ACTIVATED) VALUES (2, 'user', '$2a$08$UkVvwpULis18S19S5pZFn.YHPZt3oaqHZnDwqbCW9pft6uFtkXKDC', 'user', 1);
INSERT INTO AUTHORITY (AUTHORITY_NAME) values ('ROLE_USER');
INSERT INTO AUTHORITY (AUTHORITY_NAME) values ('ROLE_ADMIN');
INSERT INTO USER_AUTHORITY (USER_ID, AUTHORITY_NAME) values (1, 'ROLE_USER');
INSERT INTO USER_AUTHORITY (USER_ID, AUTHORITY_NAME) values (1, 'ROLE_ADMIN');
INSERT INTO USER_AUTHORITY (USER_ID, AUTHORITY_NAME) values (2, 'ROLE_USER');
728x90
'Spring > Security' 카테고리의 다른 글
JWT 예제 구현 (12) 3.0.1 버전 업그레이드 (1) | 2023.12.23 |
---|---|
JWT 예제 구현 (11) 버전 2.7.2 업데이트, 그래들 업데이트,RestResponseExceptionHandler 수정 (1) | 2023.12.21 |
JWT 예제 구현 (10) Response 시 DTO를 통해서만 응답하도록 변경 (0) | 2023.12.19 |
JWT 예제 구현 (9) Exception 세분화, 리다이렉트 수정, Response 로직 변경, 리다이렉트 테스트 추가 (1) | 2023.12.18 |
JWT 예제 구현 (8) 예외처리 로직 추가, Exception(핸들러) 추가, 버전 2.53 업그레이드 등 (0) | 2023.12.16 |
Comments