뭔가 깔끔하게 정리된 곳이 없는 것 같아서 개인적인 용도로 정리해보고자 한다.
우선 스프링 시큐리티의 설정을 담당할 Config 파일을 만든다.
자세한 설명은 코드 주석으로 적어놓았다.
부가적으로 이전에 올려둔 Spring Security 기초를 참고하면 좋을 것 같다.
기본적으로 회원 관련(ex.Member,User 등 직접 만든 이용자 엔티티) 엔티티가 작성 되어 있는 것을 가정하고 작성한다.
@EnableWebSecurity
적용 및 WebSecurityConfigurerAdapter
를 상속한 Config 파일을 만든다.
밑의 설정들은 테스트하고 그러느라 임의로 설정해둔 것이니 참고만 할 것.
@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// UserDetailsService를 구현한 객체를 DI
private final PrincipalDetailsService principalDetailsService;
// 비밀번호를 암호화할 PasswordEncoder를 빈 등록, 여기서는 BCryptPasswordEncoder 사용
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// AuthenticationManager 세팅
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 커스텀한 AuthenticationProvider,UserDetailsService를 AuthenticationManager에 세팅할 수 있다.
// 부가적으로 비밀번호 검증에 사용할 PasswordEncoder도 여기서 등록해준다.
auth.userDetailsService(principalDetailsService).passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
// 정적 자원에 대해서는 Security 적용 X -> 정적 자원들은 굳이 시큐리티 필터를 탈 필요가 없기 때문에 자원 낭비를 줄이기 위해서 설정
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
// 이거 설정할 때 아래와 같은 식으로 설정도 가능하다.
// web.ignoring()
// .antMatchers("/resources/**")
// .antMatchers("/css/**");
}
// 시큐리티 설정
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 일단 csrf는 당장 사용을 안하니 꺼둔다.
.cors().disable() // cors 도 꺼두자. 나중에 api로 프론트 서버랑 백엔드 서버 왓다갔다할때 설정 해두는 방법이 따로 있다.
.authorizeRequests() // 보호된 리소스 URI 설정
.antMatchers("/auth/**").permitAll() // permitAll은 권한이 하나도 없어도 접근 가능능
.antMatchers("/admin/**").hasRole(Role.ADMIN.name()) // 권한별 접근 가능한 uri 지정
.antMatchers("/member/**").hasRole(Role.MEMBER.name())
.antMatchers("/").authenticated() // authenticated는 아무 권한이나 있어야 접근가능
.and()
.formLogin() // 폼 로그인 사용 설정
.loginPage("/auth/login") // 로그인 페이지 경로, 앞으로 로그인은 이 경로에서 수행된다.
.loginProcessingUrl("/auth/login") // 로그인 처리 경로, 로그인 form의 action과 일치시켜주어야한다.
.usernameParameter("email") // form태그에서 <input name = username>이 시큐리티의 id 기본 값인데 이를 바꾸고 싶으면 이렇게 쓰면 된다.
.passwordParameter("pw") // 위와 마찬가지로 password의 name을 바꾸고 싶을 때 사용
.defaultSuccessUrl("/",false) // 로그인 완료 후 이동할 기본 url 설정, alwaysUse를 false로 해두면 로그인을 요청하기 이전 페이지로 이동한다.
.failureUrl("/auth/login") // 로그인 실패 시 이동할 url
.permitAll()// 혹시라도 로그인이 권한에 막히거나 그럴 수 있으니 permitAll을 걸어둔다.
.and()
.logout() // 로그아웃 설정
.logoutRequestMatcher(new AntPathRequestMatcher("/auth/logout")) // 로그아웃 url 설정
.logoutSuccessUrl("/auth/login") // 로그아웃 성공시 돌아갈 url
.invalidateHttpSession(true); // 저장된 세션 삭제
}
}
이거는 구현안하고 User 라는 객체를 사용해도 된다.
public class PrincipalDetails implements UserDetails {
private Member member; // 콤포지션
// 생성자를 통해서 유저 정보(Member)를 넣어준다.
public PrincipalDetails(Member member) {
this.member = member;
}
// 해당 User의 권한을 리턴하는 곳.
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
// 권한을 넣어준다. 권한이 여러개라면 여러개 넣어줄 수도 있다.
collect.add(new SimpleGrantedAuthority(member.getRole().name()));
return collect;
}
@Override
public String getPassword() {
return member.getPw();
}
@Override
public String getUsername() {
return member.getEmail();
}
// 아래의 녀석들은 기본 값이 다 false로 되어있는데 true로 바꿔두자. 지금 당장 쓸일이 없기 때문, 만약 쓸일이 생기면 로직넣어서 true,false 골라주면 된다.
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 사용가능한지 물어보는데 이거를 사용해서 로그인 한지 얼마 지나면 계정이 잠기도록 하는 로직을 짤 수도 있다. false로 바꾸면된다.
@Override
public boolean isEnabled() {
return true;
}
}
// 시큐리티에 설정에서 loginProcessingUrl 을 통해 요청이 오면
// 자동으로 UserDetailsService 타입으로 IoC에 등록되어 있는 빈을 찾은 후 loadUserByUsername 메소드가 실행된다.
// Authentication에 UserDetails를 집어넣어주는 작업을 하는 Service
@RequiredArgsConstructor
@Service
public class PrincipalDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
// 넘어온 username(email)으로 레파지토리에 유저가 있는지 확인.
Member member = memberRepository.findMemberByEmail(email);
// 있으면 만들어둔 UserDetails 구현 객체에 넣어준다.
if (member != null) {
// 반환된 구현 객체는 Security Session 내부의 Authentication 객체 내부의 UserDetails로 들어간다.
return new PrincipalDetails(member);
}
return null;
}
}