Security 설정
Spring Security는 WebSecurityConfigurerAdapter 를 상속받아 Security 설정을 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록됨
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/**").authenticated()
.antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/loginForm")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/");
}
}
HttpSecurity.csrf()
CSRF(Cross-Site Request Forgery) 보호를 활성화한다. CSRF 취약점은 공격자가 사용자가 의도하지 않은 요청을 수행하게 하는 취약점이다. 사용자의 권한 범위 내에서 사용자의 쿠키와 세션을 이용해서 공격할 수 있다. 만약 REST api를 이용할 서버라면 세션 기반 인증과 다르게 stateless하기 때문에 유저 정보를 세션에 저장하지 않고, JWT 같은 토큰을 사용해서 인증하기 때문에 CSRF 취약점에 대해서 어느정도 안전하다. 따라서, REST api 서버라면 HttpSecurity.csrf()를 disable 해도 된다.
HttpSecurity.authorizeRequests()
RequestMatcher 구현체를 사용하는 HttpServletRequest 기반으로 권한을 제한한다.
HttpSecurity.antMatchers()
특정 url에 대한 접근 조건을 지정한다. denyAll(), permitAll(), hasRoll(), authenticated() 메소드를 이용해 조건을 정해준다.
Httpsecurity.anyRequest()
antMatchers에서 지정한 url 이외의 모든 url에 대한 접근 조건을 지정한다.
Httpsecurity.formLogin()
스프링 시큐리티는 로그인 페이지를 지정해주지 않으면 시큐리티에서 제공하는 로그인 페이지로 넘어간다. 그래서 loginPage()로 로그인 페이지 url을 지정해 줘야한다. loginProcessingUrl()은 유저의 이름과 암호를 제출할 url이다. 시큐리티 사용 시 기본값으로 “/login”이 설정되어 있다. defaultSuccessUrl()은 로그인 성공 후 이동할 url을 지정한다.
UserDetails 구현체 생성
로그인 진행이 완료되면 SecurityContextHolder가 만들어 진다. 이 세션안에 Authentication 객체가 저장되며 Authentication 안에 유저 정보가 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class PrincipalDetails implements UserDetails {
private User user;
public PrincipalDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collect;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetailsService 구현체 생성
유저 정보를 담을 UserDetails 구현체를 만들고 UserDetailsService를 통해서 유저 정보를 DB에서 불러와야 한다. 로그인 요청이 오면 자동으로 UserDetailsService 타입으로 IoC되어 있는 loadUserByUsername 함수가 실행된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class PrincipalDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public PrincipalDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepository.findByUsername(username);
if(userEntity != null) {
return new PrincipalDetails(userEntity);
}
return null;
}
}
로그인 객체 사용하기
정리하자면, 로그인 요청이 들어오면 스프링 시큐리티가 로그인 처리를 진행하면서 PrincipalDetailsService의 loadUserByUsername을 호출한다. loadUserByUsername은 DB에서 유저정보를 조회하고, 그 유저정보를 PrincipalDetails에 담아서 반환한다.
그럼 시큐리티 인증 후 우리는 어떻게 로그인 객체의 정보에 접근할까?
- Bean을 통해 사용자 정보를 가져온다.
- Controller에서 사용자 정보를 얻어온다.
- @Authentication Principal을 사용한다.
Bean을 통해 사용자 정보 가져오기
1
2
3
4
5
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
UserDetails userDetails = (UserDetails)principal;
String username = principal.getUsername();
Controller에서 사용자 정보 가져오기
1
2
3
4
5
6
7
8
9
10
11
@Controller
public class SecurityController {
@GetMapping("/username")
@ResponseBoby
public String currentUserName(Principal principal) {
return principal.getName();
}
}
@Authentication Principal
해당 방법은 CustomUser 객체가 있을 때 사용한다.
1
2
3
4
5
6
7
8
9
10
11
@Controller
public class SecurityController {
@GetMapping("/username")
@ResponseBody
public String currentUserName(@AuthenticationPrincipal CustomUser customUser) {
return customUser.getUsername();
}
}