<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-core -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.4.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-web -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.4.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-config -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.4.2</version>
</dependency>
나는 Spring 5.3.x 버전을 사용중이고 그에 맞게 Security도 5.4.2 버전을 사용했다. 더 상위 버전을 사용해도 되는 것으로 알고는 있다.
@Configuration
@EnableWebSecurity
public class SecurityConfig{
private static final String[] AUTH_WHITELIST = {
// -- Swagger UI v2
"/v2/api-docs",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**",
// -- Swagger UI v3 (OpenAPI)
"/v3/api-docs/**",
"/swagger-ui/**",
// default
"/login",
"/api/**"
};
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin").hasRole(Role.ADMIN.getRoleWithoutPrefix())
.antMatchers("/auth/**").hasRole(Role.MEMBER.getRoleWithoutPrefix())
.antMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
.headers().disable()
.httpBasic().disable()
.formLogin().disable()
.rememberMe().disable();
return http.build();
}
}
Security 설정이 굉장히 복잡해 보이지만, 현재는 MVC가 아니라 API를 제공하는 것이므로 csrf, headers, httpBasic, formLogin, rememberMe를 모두 disable했다.
특히 formLogin의 경우 Spring security가 로그인페이지를 제공해주는 기능인데 이것을 사용하지 않고 추후에 POST 방식의 JSON request를 받아 처리하는 필터를 등록할 예정이다.
@Configuration 어노테이션으로 Java Config 클래스임을 명시해준다. 그리고 @EnableWebSecurity 어노테이션으로 SecurityFilterChain을 자동적으로 Spring이 등록하도록 해준다.
우리는 Swagger 관련 페이지에서 API를 명세해주기 때문에 Swagger 관련 요청 url들을 허용해주어야한다. 여기서 whitelist에 매칭되는 요청들은 permitAll()을 해주고 있는데 이것은 시큐리티가 관리하지 않겠다라기보다는 시큐리티 관련 필터 처리 과정을 거치지만 권한을 확인하지 않겠다는 내용을 의미한다.
여튼, Swagger를 사용하고 있다면 해당 url들을 화이트리스트에 등록한다. 그외 /login이나 권한없이 사용할 수 없는 API url들은 화이트리스트에 등록해놓고 허용해준다. 이렇게 하면 코드의 가독성을 올릴 수 있다.
/admin
으로 들어오는 요청은 Role에 Admin 값을 가져야 하고, /auth/**
로 들어오는 요청은 Memebr 권한을 가져야한다. 좀 더 자세히 설명하면 인증에 성공되고 시큐리티에 저장된 사용자 정보 엔티티의 권한에 따라 특정 url은 접근 가능하고, 특정 url은 접근이 불가능하고 이런 것들이 결정된다.
그러면 이 Role은 어떻게 설정하냐~ 그걸 아래에서 설명한다. 그 전에 Security 관련 설정을 초기화하려면 하나 더 추가해주어야 하는 클래스가 필요하다.
public class SpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer {
}
이렇게만 추가해주면 알아서 SecurityConfig에 있는 설정들이 등록되고 초기화된다.
@Getter
@RequiredArgsConstructor
public enum Role {
MEMBER ("ROLE_MEMBER"),
ADMIN("ROLE_ADMIN");
private final String role;
private final String PREFIX = "ROLE_";
public String getRoleWithoutPrefix() {
return this.role.substring(PREFIX.length());
}
}
여기서 왜 "ROLE_" 없이 실제 역할만 반환하는 메소드가 존재하는지 궁금할 수 있다. Spring security에서 역할을 검증할 때, 자동적으로 역할 앞에 "ROLE_"를 붙인다. 그래서 SecurityConfig에서는 "ROLE_" 없는 ADMIN 또는 MEMBER만 필요하다. 그러나 실제로 엔티티는 ROLE_ADMIN
또는 ROLE_MEMBER
를 가지고 있어야 한다!!
그래서 해당 메소드를 선언해놓고 사용한다. 그렇다면 Entity는 이 Enum을 어떻게 가지고 있으면 되나? 이건 아주 간단하다.
public class User implements Serializable{
private static final long serialVersionUID = 12345L;
...
@Column(name = "role")
@Enumerated(EnumType.STRING)
private Role role;
public User(SignUpRequest signUpRequest, int address_id) {
...
this.role = Role.MEMBER;
}
}
일단 테이블에 role이라는 컬럼을 추가해주자 물론 필수적인 것은 아니고, 메뉴얼하게 받아온 유저 엔티티에 코드 상에서 역할을 쥐어줘도 되지만 나중을 위해서 유저 정보에 권한 정보가 들어가있는 것을 추천한다.
이렇게 그냥 Role 타입의 role을 가지고 있으면 되고, 생성자에서는 어떤 역할인지 저장해준다. 여기서 User 클래스가 Serializable을 가지고 있는 이유는~ 현재 내가 맡은 프로젝트에서 Spring session jdbc를 사용하고 있기 때문에 직렬화를 통해 세션에 객체가 저장될 때 필요하다.
또 @Enumerated()는 왜 붙냐면 이것은 Hibernate 적용된 프로젝트여서 User 클래스가 하나의 테이블과 대응된 Entity이다. 근데 Role 타입의 데이터 형식은 없기 때문에 해당 Enum 타입을 문자열 그대로 String으로 저장해야할 필요가 있다. 그럴 때 사용될 수 있다.
하지만 시큐리티에서는 이렇게만 하면되냐~ 절대 아니다 호락호락하지 않다. UserDetails라는 시큐리티에서 사용되는 엔티티 형태로 변경해주어야한다.
public class SecurityUserDetails implements UserDetails, Serializable {
private static final long serialVersionUID = 1L;
private User user;
public SecurityUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(user.getRole().getRole()));
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getU_id();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
아까 그 User에 의존하는 UserDetails 클래스를 만들어준다. 여기서 getUsername()과 getPassword() 그리고 권한 목록을 반환해주는 getAuthorities()는 User 객체를 기준으로 해주고, 나머지는 유저 테이블에 없는 내용이여서 강제로 true를 반환해주고 있다.
계정이 만료되었는지, 잠긴 계정인지, 비밀번호가 만료되었는지 사용불가능한 계정인지 등에 대한 정보를 반환해줄 수 있다. 우리는 그런게 없기 때문에 true를 반환해주자~
나중에 이 엔티티는 인증과정에서 시큐리티에서 처리된다.
이제 사용자 엔티티에 권한을 부여해주었고, 나중에 Security에서 Json을 이용한 로그인을 구현하고, 여러가지 에러 핸들러들을 통해서 Exception에 대처하는 방식에 대해서 알아보자