로그인을 하려면 상태를 관리할 수 있는 세션이 필요하다.
스프링 시큐리티의 경우 ‘Secruity Session’을 제공한다.
이 시큐리티 세션은 Security Context Holder 내에서 관리된다. 관계는 다음과 같다.
가장 먼저, configure()메서드에 loginProcessingUrl(”/login”)과 defaultSuccessUrl(”/”)을 추가한다.
loginProcessingUrl()은 “/login” url이 호출되면 스프링 시큐리티가 인터셉트해서 대신 로그인을 진행해준다.
해당 로그인이 완료되면, Security Context Holder 내에 시큐리티 세션을 만들어준다.
defaultSuccessUrl()은 로그인 후 리다이렉트되는 주소를 지정해준다.
단, 사용자가 진입하고자 했던 url이 있으면 해당 url로 보내준다.
예를 들어, 사용자가 /api/diary/a 라는 url을 접근하고자 했으면, 로그인 진행 후에 해당 url로 리다이렉트 해준다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/api/diary/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/loginForm")
.loginProcessingUrl("/login")// 로그인 주소가 호출이 되면 시큐리티가 인터셉트해서 대신 로그인 진행해줌. 이렇게 하면 컨트롤러에서 /login 매핑할 필요가 없음.
.defaultSuccessUrl("/");//로그인 하게 되면 "/" url 로 이동됨. 단, 진입하고자 했던 url이 있었으면 해당 url로 이동함.
}
}
Security Context Holder 내에 들어갈 수 있는 타입은 Authentication 타입 뿐이다.
그런데, 이 Authentication 타입 객체 내에는 유저의 정보가 필요하다.
이 유저 정보를 어떻게 Authentication 객체에 넣을 수 있을까?
방법은 UserDetails 인터페이스를 구현한 객체를 넣으면 된다.아래의 PrincipalDetails는 해당 인터페이스를 구현한 객체이다.
하지만 PrincipalDetails 에는 “유저(또는 작성자)의 정보”가 존재하지 않는다. 그래서 Composition(합성)을 이용해 Writer 엔티티를 포함시킨다.
//Authentication 객체에 넣기 위한 래퍼 객체
public class PrincipalDetails implements UserDetails {
//실제 엔티티를 참조함.
private final Writer writer;
public PrincipalDetails(Writer writer) {
this.writer = writer;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return writer.getRole().name();
}
});
return collection;
}
@Override
public String getPassword() {
return writer.getPassword();
}
@Override
public String getUsername() {
return writer.getName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
회원 로그인 시, 정보가 일치하지 않아 로그인이 안될 때의 예외 캐치 필요
회원 가입 시, 유니크 조건을 깨뜨릴 경우의 예외 캐치 필요→회원 가입할 때 이미 있는 엔티티인지, 또는 프로퍼티인지 확인하는 로직 추가 필요.
먼저 Security 설정용 클래스에 EnableGlobalMethodSecurity 어노테이션을 부착한다.
securedEnabled = true를 적용하면 @secured 어노테이션을, prePostEnabled =true를 적용하면 @preauthorize 어노테이션을 어플리케이션 전체에서 사용할 수있게 된다.
@Controller
public class IndexController {
@Secured("ROLE_ADMIN")
@GetMapping("/info")
public @ResponseBody String info(){
return "개인정보";
}
@PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
@GetMapping("/data")
public @ResponseBody String data(){
return "데이터 정보";
}
}
그 다음, 해당 어노테이션들을 부착한다.
두 어노테이션 모두, 메소드를 실행 하기 전에 권한이 있는지를 먼저 체크한다.
차이점이 있다면, PreAuthorize는 표현식을 사용할 수 있다는 것이다. (PreAuthorize가 Secured보다 진보된 어노테이션이라 한다.)
권한이 없는 사용자가 해당 메소드의 url에 접근하려 한다면 403 에러를 뱉는다.
@Controller
public class IndexController {
@Secured("ROLE_ADMIN")
@GetMapping("/info")
public @ResponseBody String info(){
return "개인정보";
}
@PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
@GetMapping("/data")
public @ResponseBody String data(){
return "데이터 정보";
}
}