Spring Security
Spring MVC 기반 애플리케이션의 인증, 인가 기능을 지워하는 보안 프레임워크
- Principal (주체) : 일반적으로 인증 프로세스가 성공적으로 수행된 사용자의 계정 정보를 의미
- Authentication(인증) : 애플리케이션을 사용하는 사용자가 본인이 맞음을 증명하는 절차
- Authorization(인가) : 인증이 정상적으로 수행된 사용자에게 권한을 부여하여 특정 애플리케이션의 특정 리소스에 접근할 수 있게 허가하는 과정
- Access Control(접근 제어) : 사용자가 애플리케이션의 리소스에 접근하는 행위를 제어하는 것
InMemory User를 사용하여 Spring Security 적용 (데이터베이스 연동 X)
[ build.gradle ]
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-security' // spring-boot-starter-security 스타터 추가
}
[ InMemory User 인증 ]
@Configuration
public class SecurityConfiguration {
@Bean
public UserDetailsManager userDetailsService() { // 사용자의 인증 정보를 생성
UserDetails userDetails = // 인증된 사용자의 핵심정보를 포함
User.withDefaultPasswordEncoder() // 사용자의 패스워드를 암호화
.username("hoon@gmail.com") // 사용자 식별
.password("1111") // 사용자의 password 설정
.roles("USER") // 역할 지정
.build();
return new InMemoryUserDetailsManager(userDetails); // 사용자 인증정보 return
}
}
[ HTTP 보안 구성 & 커스텀 로그인 페이지 설정 ]
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterchain(HttpSecurity http) thows Excepion { // HttpSecurity를 통해 HTTP 요청에 대한 보안 설정을 구성
http
.csrf().disable() // csrf 공격에 대한 설정을 비활성화
.formLogin() // 기본 인증 방법을 폼 로그인 방식으로 설정
.loginPage("/auths/login-form") // 로그인 페이지를 /auths/login-form 로 설정
.loginProcessingUrl("/process_login") // 로그인 인증 요청을 수행할 URL 설정
.failureUrl("/auths/login-form?error") // 로그인에 실패할 경우 화면 설정
.and() // spring security 보안 설정 메서드를 체인 형태로 구성
.authorizeHttpRequests() // 요청이 들어오면 접근 권한을 확인 하겠다고 정의
.anyRequest() // 클라이언트의 모든 요청에 대한 접근을 허용
.permitAll() // 클라이언트의 모든 요청에 대한 접근을 허용
return http.build();
}
@Bean
public UserDetailsManager userDetailsService() {
// 사용자의 인증 정보를 생성
UserDetails userDetails =
User.withDefaultPasswordEncoder() // 사용자 패스워드를 암호화
.username("kevin@gmail.com") // 사용자의 usrname을 설정
.password("1111") // 사용자의 password를 설정
.roles("USER") // 사용자의 역할을 지정
.build();
return new InMemoryUserDetailsManager(userDetails);
}
}
[ request URI 접근 권한 ]
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.formLogin()
.loginPage("/auths/login-form")
.loginProcessingUrl("/process_login")
.failureUrl("/auths/login-form?error")
.and()
.logout() // 로그아웃에 대한 추가 설정
.logoutUrl("/logout") // 로그아웃을 수행하기 위한 request URL을 지정
.logoutSuccessUrl("/") // 로그아웃을 성공적으로 수행한 이후 리다이렉트 할 URL을 지정
.and()
.exceptionHandling().accessDeniedPage("/auths/access-denied") // 권한이 없는 사용자가 접근할 경우 에러 처리
.and()
.authorizeHttpRequests(authorize -> authorize // request URI에 대한 접근 권한 부여
.antMatchers("/orders/**").hasRole("ADMIN") // ADMIN ROLE을 부여받은 사용자만 /order로 시작하는 모든 URL에 접근 가능
.antMatchers("/members/my-page").hasRole("USER") // USER ROLE을 부여받은 사용자만 /member/my-page RUL에 접근 가능
.antMatchers("⁄**").permitAll() // 앞서 지정한 URL 이외의 모든 RUL에 접근 가능
);
return http.build();
}
}
[ passwordEncoder Bean 등록 ]
@Configuration
public class SecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); // DelegatingPasswordEncoder가 실질적으로 PasswordEncoder 구현 객체를 생성
}
}
[ InMemoryMemberService 구현 ]
public class InMemoryMemberService implements MemberService {
private final UserDetailsManager userDetailsManager;
private final PasswordEncoder passwordEncoder;
public InMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
this.userDetailsManager = userDetatilsManager; // UserDetailsManager => User를 관리하는 관리자 역할
this.passwordEncoder = passwordEncoder; // PasswordEncoder => 패스워드를 암호화
}
public Member createMember(Member member) {
List<GrantedAuthority> authorities = createAuthoritied(Member.MemberRole.ROLE_USER.name()); // User의 권한 목록은 List<GrantedAuthority> 로 생성
String encryptedPassword = passwordEncoder.encode(member.getPassword()); // PasswordEncoder를 사용하여 암호화
UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities); // UserDetails 생성
userDetatilsManager.createUser(userDetails); // User 정보 등록
return member;
}
private List<GrantedAuthority> createAuthorities(String ... roles) {
return Arrays.stream(roles)
.map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList());
}
}
[ PasswordEncoder Bean 등록 ]
@Configuration
public class JavaConfiguration {
// PasswordEncoder 구현 객체를 생성
@Bean
public MemberService inMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
return new InMemoryMemberService(userDetailsManager, passwordEncoder);
}
}
[ JavaConfiguration 구성 ]
@Configuration
public class JavaConfiguration {
// InMemoryMemberService 클래스의 Bean 객체를 생성
@Bean
public MemberService inMemoryMemberService(UserDetailsManager userDetailsManager,
PasswordEncoder passwordEncoder) {
return new InMemoryMemberService(userDetailsManager, passwordEncoder);
}
}
[ InMemoryMemberService 구현 ]
public class InMemoryMemberService implements MemberService {
private final UserDetailsManager userDetailsManager;
private final PasswordEncoder passwordEncoder;
public InMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
this.userDetailsManager = userDetailsManager;
this.passwordEncoder = passwordEncoder;
}
public Member createMember(Member member) {
// User의 권한 목록을 List<GrantedAuthority>로 생성
List<GrantedAuthority> authorities = createAuthorities(Member.MemberRole.ROLE_USER.name());
// User의 패스워드를 암호화
String encryptedPassword = passwordEncoder.encode(member.getPassword());
// Spring Security User로 등록하기 위해 UserDetails를 생성
UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities);
// User를 등록
userDetailsManager.createUser(userDetails);
return member;
}
private List<GrantedAuthority> createAuthorities(String... roles) {
return Arrays.stream(roles)
.map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList());
}
}
Custom UserDetailsService 사용 (데이터 베이스 연동 O)
[ SecurityConfiguration 설정 변경 및 추가 ]
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOprions().sameOrigin() // H2 웹 콘솔을 사용하기 위한 설정
.and()
.csrf().disable()
.formLogin()
.loginPage("/auths/login-form")
.loginProcessingUrl("/process_login")
.failureUrl("/auths/login-form?error")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.and()
.exceptionHandling().accessDeniedPage("/auths/access-denied")
.and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/orders/**").hasRole("ADMIN")
.antMatchers("/members/my-page").hasRole("USER")
.antMatchers("/**").permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
[ JavaConfiguration의 Bean 등록 변경 ]
@Configuration
public class JavaConfiguration {
// 데이터베이스에 User의 정보를 저장하기 위해 MemberService 인터페이스의 구현 클래스를 DBMemberService로 변경
@Bean
public MemberService dbMemberService(MemberRepository memberRepository,
PasswordEncoder passwordEncoder) {
return new DBMemberService(memberRepository, passwordEncoder);
}
}
[ DBMEmberService 구현 ]
@Transactional
public class DBMemberService implements MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
public DBMemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
this.memberRepository = memberRepository;
this.passwordEncoder = passwordEncoder;
}
public Member createMemeber(Member member) {
verifyExistsEmail(member.getEmail());
String encryptedPassword = passwordEncoder.encode(member.getPassword()); // Password 암호화
member.setPassword(encryptedPassword); // 암호화된 Password를 password 필드에 다시 할당
Member savedMember = memberRepository.save(member);
System.out.println("# create Member in DB");
return saveMember;
}
...
...
}
[ Custom UserDetailsService 구현 ]
@Component
public class HelloUserDetailsServiceV1 implements UserDetailsService {
private final MemberRepository memberRepository;
private final HelloAuthorityUtils authorityUtils;
public HelloUserDetailsServiceV1(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.authorityUtils = authorityUtils;
}
@Override
public userDetails loadUserByUsername(String username) throws UsernameNotFoundException { // UserDetailsService 인터페이스를 구현하는 클래스는 loadUserByUsername(String username)이라는 추상 메서드 구현 필요
Optional<Member> optionalMember = memberRepository.finByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.Member_NOT_FOUND));
Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember.getEmail()); // HelloAuthorityUtils를 이용해 데이터베이스에서 조회한 이메일 정보를 Role기반의 권한정보 컬렉션에 생성
return new User(findMember.getEmail(), finMember.getPassword(), authorities);
}
}
[ HelloAuthorityUtils ]
@Component
public class HelloAuthorityUtils {
@Value("${mail.address.admin}") // application.yml에 추가한 프로퍼티를 가져옴
private String adminMailAddress;
private final List<GrantedAuthority> ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER"); // 관리자용 권한 목록을 생성
private final List<GrantedAuthority> USER_ROLES = authorityUtils.createAuthorityList("ROLE_USER"); // 일반 사용자 권한 목록 생성
public List<GrantedAuthority> createAuthorities(String email) { // 파라미터로 받은 이메일 정보와 application.yml 파일의 이메일 정보가 동일 하다면 관리자용 권한인 ADMIN_ROLES 리턴
if(email.equals(adminMailAddress)) {
return ADMIN_ROLES;
}
return USER_ROLES;
}
}