feat: project skeleton + DB migrations V1-V12 + ga-common framework
- Gradle multi-module: ga-common, ga-core, ga-api, ga-batch, ga-admin - Flyway V1-V12: org/product/rule/receive/ledger/settle/batch/code/menu/system + seed - ga-common (complete): - model: ApiResponse, PageResponse, SearchParam, TreeNode - exception: ErrorCode, BizException, GlobalExceptionHandler - annotation + aop: @RequirePermission, @DataChangeLog, ApiLog - auth + security: JWT, SecurityConfig - mybatis: BaseMapper, EncryptTypeHandler, JsonTypeHandler, AuditInterceptor - code (Redis cached) / menu (RBAC tree) / file / system / notification - excel: SXSSF+Cursor export, SAX+batch import (1M/700K rows) - util: Date/Money/Mask/Encrypt/Security
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package com.ga.common.auth;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.ApiResponse;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Authorization: Bearer xxx 헤더에서 JWT 추출 → SecurityContext 설정.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final String HEADER = "Authorization";
|
||||
private static final String PREFIX = "Bearer ";
|
||||
|
||||
private final JwtTokenProvider tokenProvider;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String header = request.getHeader(HEADER);
|
||||
if (header != null && header.startsWith(PREFIX)) {
|
||||
String token = header.substring(PREFIX.length());
|
||||
try {
|
||||
Claims claims = tokenProvider.parse(token);
|
||||
Long userId = claims.get("uid", Long.class);
|
||||
String loginId = claims.getSubject();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> roles = (List<String>) claims.getOrDefault("roles", List.of());
|
||||
|
||||
LoginUser user = LoginUser.builder()
|
||||
.userId(userId)
|
||||
.loginId(loginId)
|
||||
.roleCodes(roles)
|
||||
.build();
|
||||
|
||||
UsernamePasswordAuthenticationToken auth =
|
||||
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
} catch (BizException e) {
|
||||
writeError(response, e.getErrorCode());
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
log.warn("JWT processing error: {}", e.getMessage());
|
||||
writeError(response, ErrorCode.TOKEN_INVALID);
|
||||
return;
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private void writeError(HttpServletResponse response, ErrorCode ec) throws IOException {
|
||||
response.setStatus(ec.getHttpStatus());
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
objectMapper.writeValue(response.getWriter(),
|
||||
Map.of("success", false, "code", ec.getCode(), "message", ec.getMessage()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.ga.common.auth;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.ExpiredJwtException;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JWT 발급/검증.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
|
||||
private final SecretKey key;
|
||||
private final long accessExpire;
|
||||
private final long refreshExpire;
|
||||
|
||||
public JwtTokenProvider(
|
||||
@Value("${ga.jwt.secret}") String secret,
|
||||
@Value("${ga.jwt.access-token-expire:7200000}") long accessExpire,
|
||||
@Value("${ga.jwt.refresh-token-expire:604800000}") long refreshExpire
|
||||
) {
|
||||
byte[] raw = secret.getBytes(StandardCharsets.UTF_8);
|
||||
// 256비트 미만이면 안전하게 패딩
|
||||
if (raw.length < 32) {
|
||||
byte[] padded = new byte[32];
|
||||
System.arraycopy(raw, 0, padded, 0, raw.length);
|
||||
raw = padded;
|
||||
}
|
||||
this.key = Keys.hmacShaKeyFor(raw);
|
||||
this.accessExpire = accessExpire;
|
||||
this.refreshExpire = refreshExpire;
|
||||
}
|
||||
|
||||
public String createAccessToken(Long userId, String loginId, List<String> roles) {
|
||||
return create(userId, loginId, roles, accessExpire, "ACCESS");
|
||||
}
|
||||
|
||||
public String createRefreshToken(Long userId, String loginId) {
|
||||
return create(userId, loginId, List.of(), refreshExpire, "REFRESH");
|
||||
}
|
||||
|
||||
private String create(Long userId, String loginId, List<String> roles, long expireMs, String type) {
|
||||
Date now = new Date();
|
||||
return Jwts.builder()
|
||||
.subject(loginId)
|
||||
.claims(Map.of(
|
||||
"uid", userId,
|
||||
"roles", roles,
|
||||
"type", type
|
||||
))
|
||||
.issuedAt(now)
|
||||
.expiration(new Date(now.getTime() + expireMs))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public Claims parse(String token) {
|
||||
try {
|
||||
return Jwts.parser().verifyWith(key).build()
|
||||
.parseSignedClaims(token).getPayload();
|
||||
} catch (ExpiredJwtException e) {
|
||||
throw new BizException(ErrorCode.TOKEN_EXPIRED);
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
throw new BizException(ErrorCode.TOKEN_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isValid(String token) {
|
||||
try {
|
||||
parse(token);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public long getAccessExpire() { return accessExpire; }
|
||||
public long getRefreshExpire() { return refreshExpire; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.ga.common.auth;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* SecurityContext 에 들어갈 인증 주체.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public class LoginUser implements UserDetails {
|
||||
|
||||
private final Long userId;
|
||||
private final String loginId;
|
||||
private final String userName;
|
||||
private final Long agentId;
|
||||
private final List<String> roleCodes;
|
||||
|
||||
@Override public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return roleCodes == null ? List.of() :
|
||||
roleCodes.stream().<GrantedAuthority>map(r -> new SimpleGrantedAuthority("ROLE_" + r)).toList();
|
||||
}
|
||||
|
||||
@Override public String getPassword() { return null; }
|
||||
@Override public String getUsername() { return loginId; }
|
||||
@Override public boolean isAccountNonExpired() { return true; }
|
||||
@Override public boolean isAccountNonLocked() { return true; }
|
||||
@Override public boolean isCredentialsNonExpired() { return true; }
|
||||
@Override public boolean isEnabled() { return true; }
|
||||
}
|
||||
Reference in New Issue
Block a user