728x90
1.맴버 권한 나누기
지금 Member에는 role 로 역할을 나누고 있고 CUSTOMER ,SELLER , ADMIN 은 각각 접근할수있는 엔드포인트가 달라야 하므로 이를 설정할것이다.
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "member_id", nullable = false, unique = true)
private String memberId;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String nickname;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
public enum Role {
CUSTOMER, SELLER, ADMIN
}
}
2.JWT에 role 추가하기
JWT 토큰을 생성할때 payload 부분에 role이 추가되도록만들어야하므로 토큰 생성을 할때 다음과 같은 부분을 추가해야한다.
.claims().empty().add("role", role) // role 추가
2-1.createToken
기존 userId 만 매개변수로 받아와서 subject로 사용했지만 이제 role 또한 매개변수로 받아서 payload 에 추가하였다.
// JWT 토큰 생성
public String createToken(String userId, String role) {
return Jwts.builder()
.claims().empty().add("role", role) // role 추가
.and()
.subject(userId)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(key, Jwts.SIG.HS256)
.compact();
}
2-2.로그인(토큰 생성 메소드 사용)
매개변수에 role 를 추가했으므로 createToken메소드를 사용하는 로그인 부분또한 맞게 수정해준다.
//로그인 (JWT 반환)
public AuthResponseDto login(AuthRequestDto request) {
Member member = memberRepository.findBymemberId(request.getMemberId())
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) {
throw new RuntimeException("비밀번호가 일치하지 않습니다.");
}
String token = jwtTokenProvider.createToken(member.getMemberId(),member.getRole().name());
return new AuthResponseDto(token);
}
2-3.getRoleFromToken
추후 Token 에서 Role 을 추출해야하므로 getRoleFromToken 메소드를 만들어서 JwtTokenProvider 클래스에 넣었다.
// JWT에서 역할 추출
public String getRoleFromToken(String token) {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload()
.get("role", String.class);
}
3.doFilterInternal 수정
이제 기존의 doFilterInternal 메소드를 role이 추가됨에 따라 수정을 해줄것이다.
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain chain) throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
String userId = jwtTokenProvider.getUserIdFromToken(token);
String role = jwtTokenProvider.getRoleFromToken(token);
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role));
UserDetails userDetails = new User(userId, "", authorities);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
null, authorities);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("인증 완료: 사용자 ID = {}, 역할 = {}", userId, role);
}
chain.doFilter(request, response);
}
3-1.role 추출
위에서 getRoleFromToken메소드를 만들어놨기때문에 이제 token에서 role를 추출할수있게 되었다.
String role = jwtTokenProvider.getRoleFromToken(token);
3-2.권한 리스트 생성(GrantedAuthority)
이제 role 을 바탕으로 권한 리스트를 생성해줄것인데 Spring Security에서 권한을 나타내는 GrantedAuthority인터페이스를 사용할것이다. 해당 인터페이스에선 권한은 ROLE_ 접두사를 붙여야하고 추후 hasRole(), @PreAuthorize()를 통해 권한에 접근할수있다.
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role));
3-3.User 객체 생성
Spring Security에서 인증 객체를 만들 때 사용하는 User 객체를 생성해준다.
기본적으로 User 객체는 다음과같이 생성한다.
User(String username, String password, Collection<? extends GrantedAuthority> authorities)
지금은 유저이름과 권한에 대한 정보만 넣고 비밀번호는 비워뒀다.
UserDetails userDetails = new User(userId, "", authorities);
3-4.인증 객체 (UsernamePasswordAuthenticationToken)
UsernamePasswordAuthenticationToken 객체를 통해 로그인한 사용자의 정보를 저장한다.
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
null, authorities);
만든뒤 HTTP 요청과 관련된 세부 정보(IP, 세션 ID 등)를 Authentication 객체에 추가해준다.
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
모든 과정이 끝났으면 Spring Security의 SecurityContext에 현재 인증된 사용자를 저장해준다.
SecurityContextHolder.getContext().setAuthentication(authentication);
4.권한에 따른 엔드포인트 설정
이제 SecurityConfig에서 .hasRole() 을 통해 각각 엔드포인트 마다 접근할수있는 권한을 설정해줄수있다. ADMIN 역할의 경우 모든 엔드포인트에 접근가능하게하였고 나머지 SELLER 와 CUSTOMER가 접근할수있는 엔드포인트를 따로 설정해줬다.
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**", "/member/register", "/h2-console/**", "/product/**", "/images/**",
"/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html", "/test/public")
.permitAll()
.requestMatchers("/**").hasRole("ADMIN")
.requestMatchers("/product/create", "/product/update", "/product/delete", "/test/seller")
.hasRole("SELLER")
.requestMatchers("/test/customer")
.hasRole("CUSTOMER")
.requestMatchers("/test/protected")
.authenticated()
.anyRequest().authenticated())
.csrf(csrf -> csrf.disable())
.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
5.제출 아이디 같은지 검사(AuthService)
현재 상품 제작 엔드포인트는 Seller 만 접근할수있지만, 제출할때 Seller ID 를 다른 회원의 것으로 등록하지않고 인증된 사용자 자신의 아이디인지 검사해야한다.
(추후 Admin 엔드포인트와 Seller 엔드포인트를 분리하여, memberId를 request 받지않고 JWT에서 꺼내는식으로 바꿀예정)
//제출 아이디 같은지 검사
public void validateSameMemberId(String memberId) {
String currentUserId = SecurityUtil.getCurrentUserId();
if (currentUserId != memberId) {
throw new AccessDeniedException(String.format("User ID is not the same as the current logged-in user ID "));
}
}
CreateProduct 메소드에 검증과정을 넣는다.
// 생성
public Product createProduct(ProductCreateRequestDTO requestDTO) {
Seller seller = sellerRepository.findById(requestDTO.getSellerId())
.orElseThrow(() -> {
String errorMessage = String.format("(sellerId: %d)", requestDTO.getSellerId());
return new SellerNotFoundException(errorMessage);
});
authService.validateSameMemberId(seller.getMember().getMemberId());
ProductValidationUtil.validatePriceAndStock(requestDTO.getPrice(), requestDTO.getStock());
Product product = requestDTO.toEntity(seller);
return productRepository.save(product);
}
6.상품 소유 인증(AuthService)
또한 상품의 수정,삭제의 경우 소유자가 아니면 Seller 여도 하면 안되기 때문에 JWT에서 아이디를 찾아서 현재 수정,삭제할려는 상품의 소유자인지 확인한다.
//상품 소유 인증
public Product validateProductOwnership(Long productId) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(String.format("(productId: %d)", productId)));
String currentUserId = SecurityUtil.getCurrentUserId();
if (!product.isOwnedBy(currentUserId)) {
throw new AccessDeniedException(String.format("NO OWNER THIS PRODUCTId: %d",productId));
}
return product;
}
updateProduct, deleteProduct 메소드에 검증과정을 넣는다.
// 수정
public Product updateProduct(Long productId, ProductUpdateRequestDto requestDto) {
Product product = authService.validateProductOwnership(productId);
ProductValidationUtil.validatePriceAndStock(requestDto.getPrice(), requestDto.getStock());
ProductUpdateUtil.updateProductFields(product, requestDto);
return productRepository.save(product);
}
// 삭제
@Transactional
public void deleteProduct(Long productId) {
Product product = authService.validateProductOwnership(productId);
productRepository.delete(product);
}
728x90
'BackEnd > SpringBoot' 카테고리의 다른 글
[SpringBoot] @PreAuthorize 사용설정 (0) | 2025.02.28 |
---|---|
[SpringBoot] 쇼핑몰 상품 엔드포인트제작,예외처리, 로그설정 (0) | 2025.02.24 |
[SpringBoot] JWT와 로그인,로그아웃 기능 제작 (0) | 2025.02.22 |