Authorization
1. GrantedAuthority
: 사용자가 누구인지 인증되면 사용자에 따라 접근할 수 있는 서비스를 나눌 수 있다!
== UserDetails가 나타내는 사용자가 가진 권한을 묘사하는 interface.
- userDetails.getAuthorities() 를 보면 반환값이 Collection<? extends GrantedAuthority>이다.
> GrantedAuthority는 인터페이스로 Authentication 객체에 주어진 권한을 보여준다.
-Spring Security 내부에서는 보통 String 형식으로 권한을 표현하는 SimpleGrantedAuthority를 사용한다.
1) 사용자 권한 표현
- UserEntity 객체에 Column 형식으로 부여해준다.
// UserEntity class
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
private String email;
private String phone;
// 권한 (문자열 하나에 ','로 구분해 권한을 묘사)
// ROLE_USER, ROLE_ADMIN, READ_AUTHORITY, WRITE_AUTHORITY
private String authorities;
}
- CustomUserDetails에도 추가해준다.
+ getAuthorities() 메서드 내부 구현 : String으로 주어진 권한들을 ',' 기준으로 나누어 SimpleGrantedAuthority의 리스트 형태로 반환.
+ getRawAuthorities() (즉, getter 생성) : 이름이 authorities로 같기 때문에 이름을 다르게 getter를 만들어주어야 한다.
// CustomUserDetails class
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {
@Getter
private Long id;
private String username;
private String password;
@Getter
private String email;
@Getter
private String phone;
private String authorities;
public String getRawAuthorities() {
return this.authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// String authorities = "ROLE_USER, ROLE_ADMIN, READ_AUTHORITY, WRITE_AUTHORITY";
String[] authorityArray = authorities.split(", ");
List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
for (String s : authorityArray) {
authorityList.add(new SimpleGrantedAuthority(s));
}
return authorityList;
}
//...
}
- CustomUserDetailsManager에서도 생성시 권한을 함께 저장한다.
@Slf4j
@Service
public class CustomUserDetailsManager implements UserDetailsManager {
private final UserRepository userRepository;
public CustomUserDetailsManager(UserRepository userRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
// ROLE_USER
CustomUserDetails userDetails = CustomUserDetails.builder()
.id(1L)
.username("user1")
.password(passwordEncoder.encode("password1"))
.email("email@naver.com")
.phone("01011112222")
.authorities("ROLE_USER")
.build();
createUser(userDetails);
// ROLE_USER, ROLE_ADMIN
CustomUserDetails userDetails2 = CustomUserDetails.builder()
.username("admin")
.password(passwordEncoder.encode("password1"))
.email("email@gmail.com")
.phone("01012341234")
.authorities("ROLE_USER, ROLE_ADMIN")
.build();
createUser(userDetails2);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// repository에서 username으로 찾아오기
Optional<UserEntity> optionalUser = userRepository.findByUsername(username);
// username이 없다면 예외처리
if (optionalUser.isEmpty()) {
throw new UsernameNotFoundException("username not found");
}
// 있다면 엔티티 가져오기
UserEntity userEntity = optionalUser.get();
// 리턴을 UserDetails로 해야하기 때문에 UserDetails 형태로 바꿔주기
UserDetails userDetails = CustomUserDetails.builder()
.id(userEntity.getId())
.username(userEntity.getUsername())
.password(userEntity.getPassword())
.email(userEntity.getEmail())
.phone(userEntity.getPhone())
.authorities(userEntity.getAuthorities())
.build();
return userDetails;
}
// ...
}
이렇게 해서
admin의 jwt 토큰을 발급받고 header에 넣어주며 my-profile로 가면 된다.
>> 이렇게 하면 결과가 admin으로 나오고, 이 사용자의 authorities 까지 잘 나온다.
2) 권한의 종류
- Role : 사용자가 어떤 계급, 또는 역할의 사용자인지 나타냄 (사용자, 관리자 등)
- Authority : 역할이나 계급과 상관없이 특정 작업을 할 수 있는 권한을 나타냄 (게시글 작성 권한, 댓글 작성 권한 등)
- GrantedAuthority.getAuthority() 반환이 : ROLE_(prefix)로 시작하면 role, 아니면 authority로 취급.
+ 미인증(로그인 안한) 사용자 : ROLE_ANONYMOUS
3) Authentication에 권한 부여
- Spring Security가 제공하는 인증 방식을 사용하고 있다면 > Authentication에 자동으로 사용자 권한이 설정된다.
- Filter를 사용해 직접 Authentication을 설정한다면 > Authentication에도 사용자 권한을 설정해주어야 한다.
>> 아래에서 UsernamePasswordAuthenticationToken에서 계속 new ArrayList로 설정해두었던 authorities 부분을 바꿔주었다.
// JwtTokenFilter
@Slf4j
@RequiredArgsConstructor
public class JwtTokenFilter extends OncePerRequestFilter {
//토큰을 확인하는 메서드 (JwtTokenUtils.validate())를 위해
private final JwtTokenUtils jwtTokenUtils;
private final UserDetailsManager manager;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
// 1. authorization 헤더를 회수
// String authHeader = request.getHeader("Authorization");
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
// 2. 헤더가 존재하는지, Bearer로 시작하는지 확인
if (authHeader != null && authHeader.startsWith("Bearer ")) {
// 3. 존재하면 유효한 토큰인지
// 헤더에서 Bearer 뒤의 토큰만 가져오기
String token = authHeader.split(" ")[1];
// 토큰을 확인하는 메서드 (JwtTokenUtils.validate()) boolean값 반환
if (jwtTokenUtils.validate(token)) {
// 4. 유효하다면 해당 토큰을 바탕으로 사용자 정보를 SecurityContext에 등록
// contextholder에서 context 만들고,
SecurityContext context = SecurityContextHolder.createEmptyContext();
// 토큰에서 사용자 정보 가져오기
String username = jwtTokenUtils.parseClaims(token).getSubject();
UserDetails userDetails = manager.loadUserByUsername(username);
for (GrantedAuthority authority : userDetails.getAuthorities()) {
log.info("authority : {}", authority.getAuthority());
}
// 인증 정보 생성
AbstractAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(
//principal : UserDetailsManager에서 실제 사용자 정보 가져오기
userDetails,
// credentials : token
token,
// authorities
userDetails.getAuthorities()
);
// 인증 정보 등록
context.setAuthentication(authenticationToken);
SecurityContextHolder.setContext(context);
log.info("=== set security context with jwt");
} else {
log.warn("jwt validation failed!!");
}
}
// 5. 다음 필터 호출
filterChain.doFilter(request, response);
}
}
2. 권한에 따른 접근 제어
- AuthorizationController를 만들어 준다.
// AuthorizationController
@Slf4j
@RestController
@RequestMapping("/auth")
public class AuthorizationController {
// ROLE_USER를 가졌을 때 요청 가능
@GetMapping("/user-role")
public String userRole() {
return "userRole";
}
// ROLE_ADMIN를 가졌을 때 요청 가능
@GetMapping("/admin-role")
public String adminRole() {
return "adminRole";
}
}
1) Role에 따른 접근 제어
- USER role 필요 vs. ADMIN role 필요
- hasAnyRole() : 주어진 인자 중 하나라도
- hasRole() : 주어진 인자를 정확하게
- 앞의 ROLE_은 생략하고 작성한다.
- 이것을 제어하기 위해 WebSecurityConfig에서 설정해준다.
// WebSecurityConfig class
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtTokenUtils jwtTokenUtils;
private final UserDetailsManager manager;
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http
) throws Exception {
http
// csrf 보안 해제
.csrf(AbstractHttpConfigurer::disable)
// url에 따른 요청 인가
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/no-auth",
"/users/home",
"/tests",
"/token/validate",
"/token/issue"
)
.permitAll()
.requestMatchers("/users/my-profile")
.authenticated()
// 여기부터
.requestMatchers("/auth/user-role")
.hasRole("USER")
.requestMatchers("/auth/admin-role")
.hasRole("ADMIN")
// 여기까지 추가
.requestMatchers("/users/login",
"/users/register")
.anonymous()
.anyRequest().permitAll()
)
// JWT를 사용하기 때문에 보안 관련 세션 해제
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// JWT 필터를 권한 필터 앞에 삽입
.addFilterBefore(new JwtTokenFilter(jwtTokenUtils, manager),
AuthorizationFilter.class)
;
return http.build();
}
}
그러면 결과는 user로 토큰을 발급받으면 admin-role에는 접속 불가,
admin으로 토큰을 발급받으면 admin-role, user-role에 둘다 접속 가능하게 나온다.
2) Authority에 따른 접근 제어
- WRITE_AUTHORITY authority 필요 vs. READ_AUTHORITY authority 필요
- hasAnyAuthority() : 주어진 인자 중 하나라도
- hasAuthority() : 주어진 인자를 정확하게
- 권한 이름을 전부 적어야 한다.
- 사실 위와 같은 방식으로 진행하면 된다.
// AuthorizationController
@GetMapping("/read-authority")
public String readAuth() {
return "able to read";
}
@GetMapping("/write-authority")
public String writeAuth() {
return "able to write";
}
// CustomUserDetailsManager 생성자
public CustomUserDetailsManager(UserRepository userRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
CustomUserDetails userDetails = CustomUserDetails.builder()
.id(1L)
.username("user1")
.password(passwordEncoder.encode("password1"))
.email("email@naver.com")
.phone("01011112222")
.authorities("ROLE_USER, READ_AUTHORITY")
.build();
createUser(userDetails);
CustomUserDetails userDetails2 = CustomUserDetails.builder()
.username("admin")
.password(passwordEncoder.encode("password1"))
.email("email@gmail.com")
.phone("01012341234")
.authorities("ROLE_USER, ROLE_ADMIN, WRITE_AUTHORITY")
.build();
createUser(userDetails2);
}
// WebSecurityConfig securityFilterChain에 추가
.requestMatchers("/auth/read-authority")
.hasAuthority("READ_AUTHORITY")
.requestMatchers("/auth/write-authority")
.hasAuthority("WRITE_AUTHORITY")
>> 결과는 user1일 때는 read-authority 접속 가능/ write-authority 접속 불가능
admin일 때는 read-authority 접속 불가능/ write-authority 접속 가능
'Programming > Spring, SpringBoot' 카테고리의 다른 글
OAuth2 - Kakao Login 구현 (1) | 2024.01.30 |
---|---|
OAuth2 - 소셜 로그인 (0) | 2024.01.30 |
JWT (1) | 2024.01.29 |
HandlerInterceptor & Filter / SecurityFilterChain (0) | 2024.01.27 |
Spring의 Dispatcher Servlet (3) | 2024.01.26 |