init spotless 적용
This commit is contained in:
@@ -1,22 +1,22 @@
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
public class BCryptSaltGenerator {
|
||||
|
||||
public static String generateSaltWithEmployeeNo(String employeeNo) {
|
||||
|
||||
// bcrypt salt는 16바이트(128비트) 필요
|
||||
byte[] randomBytes = new byte[16];
|
||||
new SecureRandom().nextBytes(randomBytes);
|
||||
|
||||
String base64 = Base64.getEncoder().encodeToString(randomBytes);
|
||||
|
||||
// 사번을 포함 (22자 제한 → 잘라내기)
|
||||
String mixedSalt = (employeeNo + base64).substring(0, 22);
|
||||
|
||||
// bcrypt 포맷에 맞게 구성
|
||||
return "$2a$10$" + mixedSalt;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
public class BCryptSaltGenerator {
|
||||
|
||||
public static String generateSaltWithEmployeeNo(String employeeNo) {
|
||||
|
||||
// bcrypt salt는 16바이트(128비트) 필요
|
||||
byte[] randomBytes = new byte[16];
|
||||
new SecureRandom().nextBytes(randomBytes);
|
||||
|
||||
String base64 = Base64.getEncoder().encodeToString(randomBytes);
|
||||
|
||||
// 사번을 포함 (22자 제한 → 잘라내기)
|
||||
String mixedSalt = (employeeNo + base64).substring(0, 22);
|
||||
|
||||
// bcrypt 포맷에 맞게 구성
|
||||
return "$2a$10$" + mixedSalt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import com.kamco.cd.training.common.enums.StatusType;
|
||||
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
|
||||
import com.kamco.cd.training.common.exception.CustomApiException;
|
||||
import com.kamco.cd.training.postgres.entity.MemberEntity;
|
||||
import com.kamco.cd.training.postgres.repository.members.MembersRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CustomAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final MembersRepository membersRepository;
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String username = authentication.getName();
|
||||
String rawPassword = authentication.getCredentials().toString();
|
||||
|
||||
// 유저 조회
|
||||
MemberEntity member =
|
||||
membersRepository
|
||||
.findByEmployeeNo(username)
|
||||
.orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND));
|
||||
|
||||
// 미사용 상태
|
||||
if (member.getStatus().equals(StatusType.INACTIVE.getId())) {
|
||||
throw new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND);
|
||||
}
|
||||
|
||||
// jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
|
||||
if (!BCrypt.checkpw(rawPassword, member.getPassword())) {
|
||||
// 실패 카운트 저장
|
||||
int cnt = member.getLoginFailCount() + 1;
|
||||
member.setLoginFailCount(cnt);
|
||||
membersRepository.save(member);
|
||||
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
|
||||
}
|
||||
|
||||
// 로그인 실패 체크
|
||||
if (member.getLoginFailCount() >= 5) {
|
||||
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED);
|
||||
}
|
||||
|
||||
// 인증 성공 → UserDetails 생성
|
||||
CustomUserDetails userDetails = new CustomUserDetails(member);
|
||||
|
||||
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import com.kamco.cd.training.common.enums.StatusType;
|
||||
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
|
||||
import com.kamco.cd.training.common.exception.CustomApiException;
|
||||
import com.kamco.cd.training.postgres.entity.MemberEntity;
|
||||
import com.kamco.cd.training.postgres.repository.members.MembersRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CustomAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final MembersRepository membersRepository;
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String username = authentication.getName();
|
||||
String rawPassword = authentication.getCredentials().toString();
|
||||
|
||||
// 유저 조회
|
||||
MemberEntity member =
|
||||
membersRepository
|
||||
.findByEmployeeNo(username)
|
||||
.orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND));
|
||||
|
||||
// 미사용 상태
|
||||
if (member.getStatus().equals(StatusType.INACTIVE.getId())) {
|
||||
throw new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND);
|
||||
}
|
||||
|
||||
// jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
|
||||
if (!BCrypt.checkpw(rawPassword, member.getPassword())) {
|
||||
// 실패 카운트 저장
|
||||
int cnt = member.getLoginFailCount() + 1;
|
||||
member.setLoginFailCount(cnt);
|
||||
membersRepository.save(member);
|
||||
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
|
||||
}
|
||||
|
||||
// 로그인 실패 체크
|
||||
if (member.getLoginFailCount() >= 5) {
|
||||
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED);
|
||||
}
|
||||
|
||||
// 인증 성공 → UserDetails 생성
|
||||
CustomUserDetails userDetails = new CustomUserDetails(member);
|
||||
|
||||
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import com.kamco.cd.training.postgres.entity.MemberEntity;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
public class CustomUserDetails implements UserDetails {
|
||||
|
||||
private final MemberEntity member;
|
||||
|
||||
public CustomUserDetails(MemberEntity member) {
|
||||
this.member = member;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return List.of(new SimpleGrantedAuthority("ROLE_" + member.getUserRole()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return member.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return String.valueOf(member.getUuid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return true; // 추후 상태 필드에 따라 수정 가능
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return "ACTIVE".equals(member.getStatus());
|
||||
}
|
||||
|
||||
public MemberEntity getMember() {
|
||||
return member;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import com.kamco.cd.training.postgres.entity.MemberEntity;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
public class CustomUserDetails implements UserDetails {
|
||||
|
||||
private final MemberEntity member;
|
||||
|
||||
public CustomUserDetails(MemberEntity member) {
|
||||
this.member = member;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return List.of(new SimpleGrantedAuthority("ROLE_" + member.getUserRole()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return member.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return String.valueOf(member.getUuid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return true; // 추후 상태 필드에 따라 수정 가능
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return "ACTIVE".equals(member.getStatus());
|
||||
}
|
||||
|
||||
public MemberEntity getMember() {
|
||||
return member;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,70 @@
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final UserDetailsService userDetailsService;
|
||||
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
|
||||
private static final String[] EXCLUDE_PATHS = {
|
||||
"/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/api/members/*/password"
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String token = resolveToken(request);
|
||||
|
||||
if (token != null && jwtTokenProvider.isValidToken(token)) {
|
||||
String username = jwtTokenProvider.getSubject(token);
|
||||
|
||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String path = request.getServletPath();
|
||||
|
||||
// JWT 필터를 타지 않게 할 URL 패턴들
|
||||
for (String pattern : EXCLUDE_PATHS) {
|
||||
if (PATH_MATCHER.match(pattern, path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String resolveToken(HttpServletRequest request) {
|
||||
String bearer = request.getHeader("Authorization");
|
||||
if (bearer != null && bearer.startsWith("Bearer ")) {
|
||||
return bearer.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final UserDetailsService userDetailsService;
|
||||
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
|
||||
private static final String[] EXCLUDE_PATHS = {
|
||||
"/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/api/members/*/password"
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String token = resolveToken(request);
|
||||
|
||||
if (token != null && jwtTokenProvider.isValidToken(token)) {
|
||||
String username = jwtTokenProvider.getSubject(token);
|
||||
|
||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String path = request.getServletPath();
|
||||
|
||||
// JWT 필터를 타지 않게 할 URL 패턴들
|
||||
for (String pattern : EXCLUDE_PATHS) {
|
||||
if (PATH_MATCHER.match(pattern, path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String resolveToken(HttpServletRequest request) {
|
||||
String bearer = request.getHeader("Authorization");
|
||||
if (bearer != null && bearer.startsWith("Bearer ")) {
|
||||
return bearer.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import javax.crypto.SecretKey;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.access-token-validity-in-ms}")
|
||||
private long accessTokenValidityInMs;
|
||||
|
||||
@Value("${jwt.refresh-token-validity-in-ms}")
|
||||
private long refreshTokenValidityInMs;
|
||||
|
||||
private SecretKey key;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// HS256용 SecretKey
|
||||
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public String createAccessToken(String subject) {
|
||||
return createToken(subject, accessTokenValidityInMs);
|
||||
}
|
||||
|
||||
public String createRefreshToken(String subject) {
|
||||
return createToken(subject, refreshTokenValidityInMs);
|
||||
}
|
||||
|
||||
private String createToken(String subject, long validityInMs) {
|
||||
Date now = new Date();
|
||||
Date expiry = new Date(now.getTime() + validityInMs);
|
||||
return Jwts.builder().subject(subject).issuedAt(now).expiration(expiry).signWith(key).compact();
|
||||
}
|
||||
|
||||
public String getSubject(String token) {
|
||||
var claims = parseClaims(token).getPayload();
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
public boolean isValidToken(String token) {
|
||||
try {
|
||||
Jws<Claims> claims = parseClaims(token);
|
||||
return !claims.getPayload().getExpiration().before(new Date());
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Jws<Claims> parseClaims(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(key) // SecretKey 타입
|
||||
.build()
|
||||
.parseSignedClaims(token);
|
||||
}
|
||||
|
||||
public long getRefreshTokenValidityInMs() {
|
||||
return refreshTokenValidityInMs;
|
||||
}
|
||||
}
|
||||
package com.kamco.cd.training.auth;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import javax.crypto.SecretKey;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.access-token-validity-in-ms}")
|
||||
private long accessTokenValidityInMs;
|
||||
|
||||
@Value("${jwt.refresh-token-validity-in-ms}")
|
||||
private long refreshTokenValidityInMs;
|
||||
|
||||
private SecretKey key;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// HS256용 SecretKey
|
||||
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public String createAccessToken(String subject) {
|
||||
return createToken(subject, accessTokenValidityInMs);
|
||||
}
|
||||
|
||||
public String createRefreshToken(String subject) {
|
||||
return createToken(subject, refreshTokenValidityInMs);
|
||||
}
|
||||
|
||||
private String createToken(String subject, long validityInMs) {
|
||||
Date now = new Date();
|
||||
Date expiry = new Date(now.getTime() + validityInMs);
|
||||
return Jwts.builder().subject(subject).issuedAt(now).expiration(expiry).signWith(key).compact();
|
||||
}
|
||||
|
||||
public String getSubject(String token) {
|
||||
var claims = parseClaims(token).getPayload();
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
public boolean isValidToken(String token) {
|
||||
try {
|
||||
Jws<Claims> claims = parseClaims(token);
|
||||
return !claims.getPayload().getExpiration().before(new Date());
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Jws<Claims> parseClaims(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(key) // SecretKey 타입
|
||||
.build()
|
||||
.parseSignedClaims(token);
|
||||
}
|
||||
|
||||
public long getRefreshTokenValidityInMs() {
|
||||
return refreshTokenValidityInMs;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user