Spring Security 프로젝트 설정 10 - 권한 설정
✒️ 2025-05-28 14:28 내용 수정
- code : https://github.com/ase10git/SpringSecurityTest
- SpringSecurity 프로젝트 설정 목록
- Spring Security 기본 사용자 추가 및 테스트
- Spring Security 프로젝트 설정 1 - DB연결과 JPA 설정
- Spring Security 프로젝트 설정 2 - JwtService와 Filter 설정
- Spring Security 프로젝트 설정 3 - Security Config
- Spring Security 프로젝트 설정 4 - Authentication Service와 Controller
- Spring Security 프로젝트 설정 5 - Security CORS 설정
- Spring Security 프로젝트 설정 6 - JWT Refresh Token 생성 및 저장
- Spring Security 프로젝트 설정 7 - JWT Refresh Token 재발급
- Spring Security 프로젝트 설정 8 - JWT 클라이언트 저장
- Spring Security 프로젝트 설정 9 - JWT 로그아웃
- Spring Security 프로젝트 설정 10 - 권한 설정
역할과 권한 설정
- 공식 문서 : Spring Security Authorization Architecture, Spring Security Authorize HTTP Requests, Spring Security Method Security
@PreAuthorize로 설정할 때와HttpSecurity로 설정할 때의 장점을 비교하여 프로젝트에 맞는 방법으로 설정한다.
HttpSecurity인스턴스로 HTTP 요청에 대한 기본적인 권한 설정을 할 수 있다.HttpSecurity로 설정 시 장점- 한 곳에서 Security 설정을 관리할 때 효과적이다.
- 명확하고 직관적인 URL과 ROLE의 매핑 관계를 사용하는 경우에 유용하다.
- Security 전체에 적용할 global 설정 및 모든 엔드포인트 설정을 지정할 때 사용한다.
- 전체적인 Security 설정을 확인할 때 편리하다.
| DSL | 설명 |
|---|---|
permitAll |
권한이 필요없는 public 엔드포인트 |
denyAll |
어떤 상황에서든 접근 불가능한 엔드포인트 |
hasAuthority |
접근하려면 Authentication이 주어진 값과 일치하는 GrantedAuthority를 가지고 있어야 함 |
hasRole |
hasAuthority의 단축형으로 ROLE_ 접두사나 기본 접두사를 사용 |
hasAnyAuthority |
접근하려면 Authentication이 주어진 값들 중에 일치하는 GrantedAuthority를 가지고 있어야 함 |
hasAnyRole |
hasAnyAuthority의 단축형으로 ROLE_ 접두사나 기본 접두사를 사용 |
access |
AuthorizationManager가 접근을 결정 |
- 예시는 공식 문서의 내용을 가져왔다.
import static jakarta.servlet.DispatcherType.*;
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
// ...
.authorizeHttpRequests(authorize -> authorize
.dispatcherTypeMatchers(FORWARD, ERROR).permitAll()
.requestMatchers("/static/**", "/signup", "/about").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/db/**").access(allOf(hasAuthority("db"), hasRole("ADMIN")))
.anyRequest().denyAll()
);
return http.build();
}
HttpSecurity에서 권한 설정했던 내용을 Controller에서@PreAuthorize를 통해 설정할 수 있다.@PreAuthorize("hasRole('ADMIN')")으로 접근 가능한 역할을 지정한다.- 각 메소드에
@PreAuthorize("hasAuthority('admin:read')")로 메소드 접근에 필요한 권한을 설정한다. - SecurityConfig에
@EnableMethodSecurity를 추가한다. @PreAuthorize으로 설정 시 장점- Security 설정을 구성 및 재구성 시 전체를 바꿀 필요 없이 Method에서 Annotation을 사용하여 관리할 수 있다.
- 메소드 별로 관리할 때 편하다.
- 복잡한 권한 설정을 해야 하는 경우 Annotation 기반으로 설정하는 것이 더 편하다.
- 계층 구조 형태의 역할 및 권한을 사용할 때와 custom 권한을 사용할 때 효과적이다.
- 가독성 측면에서도
HttpSecurity에서 지정하는 것 보다 좋다.
@Controller
@PreAuthorize("hasRole('ADMIN')")
public class TestController {
@GetMapping
@PreAuthorize("hasAuthority('admin:read')")
public String endpoint() {
return "GET endpoint";
}
}
- Spring Security 6.x 버전에선
prePostEnabled = true가 기본 설정이다. - 이전 버전에선
@EnableGlobalAuthentication을 사용했으며,prePostEnabled = false가 기본 설정으로 되어 있어@EnableGlobalMethodSecurity(prePostEnabled = true)로 사용한다.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // @PreAuthorize를 사용하기 위해 필요
public class SecurityConfig {
// ...
}
테스트용 Controller 추가
- AdminController :
ADMIN역할을 가진 사용자만 접근 가능한 Controller다.
package com.example.security.auth;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
// ADMIN만 접근 가능
@RestController
@RequestMapping("/api/v1/admin")
@RequiredArgsConstructor
public class AdminController {
@GetMapping
public String get() {
return "GET:: admin controller";
}
@PostMapping
public String post() {
return "POST:: admin controller";
}
@PutMapping
public String put() {
return "PUT:: admin controller";
}
@DeleteMapping
public String delete() {
return "DELETE:: admin controller";
}
}
- ManagementController :
ADMIN과MANAGER역할을 가진 사용자만 접근 가능한 Controller다.
package com.example.security.auth;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
// MANANGER와 ADMIN만 접근 가능
@RestController
@RequestMapping("/api/v1/management")
@RequiredArgsConstructor
public class ManagementController {
@GetMapping
public String get() {
return "GET:: management controller";
}
@PostMapping
public String post() {
return "POST:: management controller";
}
@PutMapping
public String put() {
return "PUT:: management controller";
}
@DeleteMapping
public String delete() {
return "DELETE:: management controller";
}
}
권한 관련 클래스
- Permission enum 클래스 추가
- 사용자의 역할 별 동작을 저장한 enum 클래스로, 특정 권한의 사용자가 어떤 동작을 할 수 있는지 CRUD에 따라 지정하였다.
- 예를 들어
ADMIN사용자가 특정 자원을 볼 수 있도록 하는 권한은ADMIN_READ("admin:read")로 설정한다. - Lombok의
@Getter를 사용하여 이 권한 목록을 가져오도록 한다.
package com.example.security.user;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum Permission {
ADMIN_READ("admin:read"),
ADMIN_UPDATE("admin:update"),
ADMIN_CREATE("admin:create"),
ADMIN_DELETE("admin:delete"),
MANAGER_READ("manager:read"),
MANAGER_UPDATE("manager:update"),
MANAGER_CREATE("manager:create"),
MANAGER_DELETE("manager:delete");
@Getter
private final String permission;
}
- Role 클래스 수정
- 기존의
USER, ADMIN역할에서 테스트를 위해MANAGER도 추가한다. - 각각의 역할에
Set으로 값을 주며, 여기에는Permission에서 지정한 상수들을 입력한다. ADMIN은ADMIN의 권한과MANAGER의 권한을 모두 가지고,MANAGER는MANAGER의 권한만 가지며, 사용자는 아무 권한이 없다.
- 기존의
package com.example.security.user;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@RequiredArgsConstructor
public enum Role {
USER(Collections.emptySet()),
ADMIN(
Set.of(
Permission.ADMIN_READ,
Permission.ADMIN_UPDATE,
Permission.ADMIN_CREATE,
Permission.ADMIN_DELETE,
Permission.MANAGER_READ,
Permission.MANAGER_UPDATE,
Permission.MANAGER_CREATE,
Permission.MANAGER_DELETE
)
),
MANAGER(
Set.of(
Permission.MANAGER_READ,
Permission.MANAGER_UPDATE,
Permission.MANAGER_CREATE,
Permission.MANAGER_DELETE
)
);
@Getter
// 중복 없이 권한 정보 가져오기
private final Set<Permission> permissions;
// Authorities 가져오기
// user에 getAuthorities에서도 사용
public List<SimpleGrantedAuthority> getAuthorities() {
var authorities = getPermissions()
.stream()
// spring에서 role = authorities
.map(permission ->
new SimpleGrantedAuthority(permission.getPermission())
)
.collect(Collectors.toList());
// prefix로 ROLE_을 추가한 권한을 마지막에 추가
authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name()));
return authorities;
}
}
getAuthorities는 특정Role에 있는Permission의 값을 가져오며, 마지막에 현재 역할도ROLE_ADMIN같은 형태로 추가한다.- Spring에서 권한을 비교할 때 접두사 "ROLE_"이 붙은 문자열을 역할로 간주한다.
// Authorities 가져오기
// user에 getAuthorities에서도 사용
public List<SimpleGrantedAuthority> getAuthorities() {
var authorities = getPermissions()
.stream()
// spring에서 role = authorities
.map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
.collect(Collectors.toList());
// prefix로 ROLE_을 추가한 역할을 마지막에 추가
authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name()));
return authorities;
}
GrantedAuthorityDefaults를 사용하여 접두사를 수정할 수 있다.
@Bean static GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("MYPREFIX_");
}
- 사진은
ADMIN사용자의 권한을 출력한 내용이다.
- 권한 비교 시
hasRole("ADMIN")은 내부적으로ROLE_ADMIN와 비교하지만,hasAuthority("ROLE_ADMIN")는"ROLE_"을 명시적으로 포함 해야ROLE_ADMIN과 비교한다.
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/admin").hasAuthority("ROLE_ADMIN")
- User 클래스 수정
- User 클래스에서 사용자의 권한을 가져오는
getAuthorities()를Role의getAuthorities()를 호출하도록 수정한다.
- User 클래스에서 사용자의 권한을 가져오는
package com.example.security.user;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
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;
@Data
@Entity // Entity임을 명시
@Builder // for Object building
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "user") // DB에 테이블 이름 지정
public class User implements UserDetails {
// Spring Security의 UserDetails
@Id // id로 지정
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstname;
private String lastname;
private String email;
private String password;
@Enumerated(EnumType.STRING) // Role이 Enum임을 명시
// EnumType.STRING은 String value 순으로 정렬
private Role role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 권한 List를 반환
return role.getAuthorities();
}
//... 생략
}
SecurityConfig 수정
- SecurityrConfig에서
requestMatchers()를 사용하여 AdminController의 메소드와 ManagementController의 메소드에 대한 권한 확인을 추가한다.hasAnyRole()로 사용자의 역할을 먼저 확인한 후, HTTP 요청 별 사용자의 권한을hasAnyAuthority()로 지정했다.
package com.example.security.config;
import com.example.security.user.Permission;
import com.example.security.user.Role;
import lombok.RequiredArgsConstructor;
// ... 생략
import java.util.Collections;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final CustomLogoutHandler customLogoutHandler;
// 가시성을 위한 static 처리
private static final Role ADMIN = Role.ADMIN;
private static final Role MANAGER = Role.MANAGER;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http
// session stateless로 인해 꺼 둠
.csrf((auth)->auth.disable())
.authorizeRequests()
// 요청 제어
.requestMatchers("/api/v1/auth/**") // 나열된 요청들은
.permitAll() // 모두 허용
// 권한이 필요한 요청 설정
// ManagementController
.requestMatchers("/api/v1/management/**").hasAnyRole(ADMIN.name(), MANAGER.name())
.requestMatchers(HttpMethod.GET, "/api/v1/management/**").hasAnyAuthority(
Permission.ADMIN_READ.name(), Permission.MANAGER_READ.name()
)
.requestMatchers(HttpMethod.POST, "/api/v1/management/**").hasAnyAuthority(
Permission.ADMIN_CREATE.name(), Permission.MANAGER_CREATE.name()
)
.requestMatchers(HttpMethod.PUT, "/api/v1/management/**").hasAnyAuthority(
Permission.ADMIN_UPDATE.name(), Permission.MANAGER_UPDATE.name()
)
.requestMatchers(HttpMethod.DELETE, "/api/v1/management/**").hasAnyAuthority(
Permission.ADMIN_DELETE.name(), Permission.MANAGER_DELETE.name()
)
// AdminController
.requestMatchers("/api/v1/admin").hasAnyRole(ADMIN.name())
.requestMatchers(HttpMethod.GET, "/api/v1/admin/**").hasAnyAuthority(
Permission.ADMIN_READ.name()
)
.requestMatchers(HttpMethod.POST, "/api/v1/admin/**").hasAnyAuthority(
Permission.ADMIN_CREATE.name()
)
.requestMatchers(HttpMethod.PUT, "/api/v1/admin/**").hasAnyAuthority(
Permission.ADMIN_UPDATE.name()
)
.requestMatchers(HttpMethod.DELETE, "/api/v1/admin/**").hasAnyAuthority(
Permission.ADMIN_DELETE.name()
)
.anyRequest() // 그 외의 모든 요청은
.authenticated() // 인증 필요
.and()
.sessionManagement((session)->
session // session state는 저장되면 안되므로 stateless로 설정
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter,
UsernamePasswordAuthenticationFilter.class); // jwt 필터 가동
// ... 생략
return http.build();
}
// ... 생략
}
Test
- Application을 실행한 후 콘솔에 출력되는 Admin의 Access Token을 복사한다.
- POSTMAN에 접속하고, Admin의 Access Token을 복사하여 Bearder Token에 넣은 후
http://localhost:port/api/v1/admin으로 GET요청을 보내면 status=200과 함께 Controller에서 지정한 String이 출력 된다.
- Admin의 Access Token을 그대로 사용하여
http://localhost:port/api/v1/management로 GET 요청을 보내도 status=200과 함께 Controller에서 지정한 String이 출력 된다.
- 이번엔 Manager의 Access Token을 사용하여
http://localhost:port/api/v1/management로 GET 요청을 보내 결과를 확인하면 status=200과 함께 String이 출력 된다.
- Manager의 Access Token으로
http://localhost:port/api/v1/admin에 GET 요청을 보내면 status=403이 뜨며 접근이 제한된다.
HttpSecurity 대신 @PreAuthorize 사용
- Controller에
@PreAuthorize("hasRole()")를 추가하고, 각 메소드에@PreAuthorize("hasAuthorize()")를 추가한다.
package com.example.security.auth;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
// ADMIN만 접근 가능
@RestController
@RequestMapping("/api/v1/admin")
@PreAuthorize("hasRole('ADMIN')") // ADMIN만 접근 가능
@RequiredArgsConstructor
public class AdminController {
@GetMapping
@PreAuthorize("hasAuthority('admin:read')")
public String get() {
return "GET:: admin controller";
}
@PostMapping
@PreAuthorize("hasAuthority('admin:create')")
public String post() {
return "POST:: admin controller";
}
@PutMapping
@PreAuthorize("hasAuthority('admin:update')")
public String put() {
return "PUT:: admin controller";
}
@DeleteMapping
@PreAuthorize("hasAuthority('admin:delete')")
public String delete() {
return "DELETE:: admin controller";
}
}
- SecurityConfig 파일에는
@EnableMethodSecurityAnnotation을 추가한다.- Spring Security 6.x 버전에선
prePostEnabled = true가 기본 설정이다. - 이전 버전에선
@EnableGlobalAuthentication을 사용했으며,prePostEnabled = false가 기본 설정으로 되어 있어@EnableGlobalMethodSecurity(prePostEnabled = true)로 사용한다.
- Spring Security 6.x 버전에선
package com.example.security.config;
// ... 생략
import java.util.Collections;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity // @PreAuthorize를 사용하기 위해 필요
// 최신 버전에선 prePostEnabled = true가 기본 설정이나
// 구버전에선 prePostEnabled = false가 기본 설정
// 구버전에선 @EnableGlobalMethodSecurity(prePostEnabled = true)로 사용
public class SecurityConfig {
// ... 생략
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http
// session stateless로 인해 꺼 둠
.csrf((auth)->auth.disable())
.authorizeRequests()
// 요청 제어
.requestMatchers("/api/v1/auth/**") // 나열된 요청들은
.permitAll() // 모두 허용
// ... 생략
// AdminController
// 주석 부분의 동작을 Annotation으로 똑같이 구현 가능
// .requestMatchers("/api/v1/admin").hasAnyRole(ADMIN.name())
//
// .requestMatchers(HttpMethod.GET, "/api/v1/admin/**").hasAnyAuthority(Permission.ADMIN_READ.name())
// .requestMatchers(HttpMethod.POST, "/api/v1/admin/**").hasAnyAuthority(Permission.ADMIN_CREATE.name())
// .requestMatchers(HttpMethod.PUT, "/api/v1/admin/**").hasAnyAuthority(Permission.ADMIN_UPDATE.name())
// .requestMatchers(HttpMethod.DELETE, "/api/v1/admin/**").hasAnyAuthority(Permission.ADMIN_DELETE.name())
// ... 생략
return http.build();
}
// ... 생략
}