[Spring Security] Spring Security의 기본 구조, 동작 방식 (1) -InMemory User
Spring Security 흐름 적용을 위해 CSR 방식이 아닌 SSR 방식을 이용함 각각의 화면에 맞는 html 파일이 있다! (타임리프 방식) 화면 설명 ⬇ 커피 보기 화면 - 모든 사용자 접근 가능 전체 주문 목록 보기
wlikeoxy.tistory.com
이 글에 이어서 작성!
애플리케이션 사용자에 Spring Security 를 적용시키는 방법을 두 가지 배웠다.
이번에는 InMemory User 가 아닌
DB에 저장되는 User 인증 방식을 정리한다.
Custom UserDetailsService
: User의 인증정보를 테이블에 저장,
테이블에 저장된 인증 정보를 이용해 인증 프로세스 진행
* User : 인증을 시도하는 주체
(≒ Principal : User의 구체적인 정보 ex.Username)
Member 엔티티 - Spring Security 의 User 정보를 포함한다.
1. Security Configuration 설정 변경 및 추가
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin() // 동일 출처 request 만 렌더링 허용
.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();
}
}
: 기존 InMemory 방식은 userDetailsService() 메서드에서 User 등록을 해줬지만
이제 User 등록은 DB에서 하게 될 것이므로 userDetailsService() 메서드 제거!
2. JavaConfiguration의 Bean 등록 변경
@Configuration
public class JavaConfiguration {
//DI 변경 ✨
//inMemoryMemberService ➡ dbMemberService
//userDetailsManager ➡ memberRepository
@Bean
public MemberService dbMemberService(MemberRepository memberRepository,
PasswordEncoder passwordEncoder) {
return new DBMemberService(memberRepository, passwordEncoder);
}
}
3. DBMemberService 구현
(이전에 만들어놨던 MemberService 인터페이스의 구현체)
@Transactional
public class DBMemberService implements MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
// DI
public DBMemberService(MemberRepository memberRepository,
PasswordEncoder passwordEncoder) {
this.memberRepository = memberRepository;
this.passwordEncoder = passwordEncoder;
}
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
//패스워드 암호화
String encryptedPassword = passwordEncoder.encode(member.getPassword());
member.setPassword(encryptedPassword); //암호화된 패스워드 필드에 할당
Member savedMember = memberRepository.save(member);
System.out.println("# Create Member in DB");
return savedMember;
}
4.Custom UserDetailsService 구현 (= HelloUserDetailsService)
: db에서 조회한 User의 인증 정보 기반 ➡ 인증 처리
기존의 InMemory 방식: InMemoryUserDetailsManager
= UserDetailsManager 인터페이스의 구현체
UserDetailsManager : UserDetailsService 를 상속하는 확장 인터페이스
@Component
public class HelloUserDetailsService implements UserDetailsService { //Custom UserDetailService
private final MemberRepository memberRepository;
private final HelloAuthorityUtils authorityUtils;
// DI
public HelloUserDetailsServiceV1(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.authorityUtils = authorityUtils; //User Role 정보 생성
}
// loadUserByUsername(String username) : 추상 메서드 구현
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
//✨ 조회한 회원의 이메일 정보 ➡ 권한 정보 컬렉션 생성(GrantedAuthority)
Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember.getEmail());
// Spring Security 가 인증 절차 수행
return new User(findMember.getEmail(), findMember.getPassword(), authorities);
}
}
: db 가 User의 인증 정보만 Spring Security 에게 넘겨주고,
인증 처리를 Spring Security 가 대신 해준다.
* authorityUtils : User의 권한을 매핑, 생성
@Component
public class HelloAuthorityUtils {
// application.yml 프로퍼티 **
@Value("${mail.address.admin}")
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) {
// 파라미터 이메일 주소 = 관리자용 이메일 주소?
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES; //관리자 권한 ㄱㄱ
}
return USER_ROLES; //아니라면 일반 사용자 권한 ㄱㄱ
}
}
⬇
* application.yml 파일에
mail:
address:
admin: admin@gmail.com
이런 식으로 관리자용 이메일을 지정해줄 수 있다.
* HelloUserDetailsService 리턴 부분 리팩토링
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember);
// 기존 코드 🔥
return new User(findMember.getEmail(), findMember.getPassword(), authorities);
⬇
//개선된 코드 🔥
return new 💥 HelloUserDetails(findMember);
// 💥 HelloUserDetails 클래스 추가
private final class HelloUserDetails extends Member implements UserDetails {
HelloUserDetails(Member member) {
setMemberId(member.getMemberId());
setName(member.getName());
setEmail(member.getEmail());
setPassword(member.getPassword());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityUtils.createAuthorities(this.getEmail());
}
@Override
public String getUsername() {
return getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
}
HelloUserDetails 클래스 : UserDetails 인터페이스를 구현
: User 정보 변환 과정, User 권한 정보 생성 과정 캡슐화 가능
현재까지의 User 권한 정보 :
db에서 관리되는 것이 아닌, db에서 조회한 User 정보를 기준으로 ➡ 코드 상에서 조건에 맞게 생성했었다!
(yml 프로퍼티와 동일하면 admin~ 아니면 user~)
⬇
User의 권한 정보를 db에서 관리하도록 변경
처리 과정
- User의 권한 정보를 저장하기 위한 테이블 생성
- 회원 가입 시, User의 권한 정보(Role)를 데이터베이스에 저장하는 작업
- 로그인 인증 시, User의 권한 정보를 데이터베이스에서 조회하는 작업
1. User의 권한 정보를 저장하기 위한 테이블 생성
: User ~ User 권한 정보 간에 관계 맺기 (테이블 간 연관관계)
➡ JPA 를 이용해서 연관 관계 맺기!
Member 엔티티 클래스
public class Member extends Auditable implements Principal{
...
...
// User의 권한 정보 테이블과 매핑되는 정보
@ElementCollection(fetch = FetchType.EAGER)
private List<String> roles = new ArrayList<>();
...
}
* MEMBER_ROLE 엔티티 클래스를 새로 만들어야 하지 않나? 🤔
➡ @ElementCollection : List, Set 과 같은 컬렉션 타입 필드는
별도의 엔티티 클래스 생성하지 않아도 간단하게 매핑 처리 가능! (테이블 자동 생성됨)
2. 회원 가입 시, User의 권한 정보(Role)를 데이터베이스에 저장하는 작업
DBMemberService
@Transactional
public class DBMemberService implements MemberService {
...
...
private final HelloAuthorityUtils authorityUtils;
...
...
//회원 등록 시 권한 정보 DB에 저장
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
String encryptedPassword = passwordEncoder.encode(member.getPassword());
member.setPassword(encryptedPassword);
// Role을 DB에 저장
List<String> roles = authorityUtils.createRoles(member.getEmail());
member.setRoles(roles);
Member savedMember = memberRepository.save(member);
return savedMember;
}
}
+ * authorityUtils 에 createRoles 메서드 추가
3. 로그인 인증 시, User의 권한 정보를 데이터베이스에서 조회하는 작업
* HelloUserDetailsService 수정된 부분
...
private final class HelloUserDetails extends Member implements UserDetails {
HelloUserDetails(Member member) {
setMemberId(member.getMemberId());
setName(member.getName());
setEmail(member.getEmail());
setPassword(member.getPassword());
//HelloUserDetails 가 Member에 List<String> roles 전달
setRoles(member.getRoles());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// DB에 저장된 Role 정보로 User 권한 목록 생성 ✨
return authorityUtils.createAuthorities(this.getRoles());
}
...
+ * HelloAuthorityUtils 에 createAuthorities 메서드가 List<String> roles 를 받도록 설정
위의 Custom UserDetailsService 이용해서 로그인 인증 처리 ?
➡ Spring Security 가 내부적으로 인증 처리!
↕
* Custom AuthenticationProvider ?
➡ 직접 로그인 인증 처리 하는 방식
@Component
//AuthenticationProvider 의 구현체
public class HelloUserAuthenticationProvider implements AuthenticationProvider {
private final MemberService memberService;
private final HelloAuthorityUtils authorityUtils;
private final PasswordEncoder passwordEncoder;
public HelloUserAuthenticationProvider(MemberService memberService,
HelloAuthorityUtils authorityUtils,
PasswordEncoder passwordEncoder) {
this.memberService = memberService;
this.authorityUtils = authorityUtils;
this.passwordEncoder = passwordEncoder;
}
// 직접 작성한 인증 처리 로직
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//토큰 얻기
UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication;
// 해당 사용자의 username 얻기 - 존재하는지 체크
String username = authToken.getName();
Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid User name or User Password"));
// 해당 사용자 조회
Member member = memberService.findMember(username);
String password = member.getPassword();
//로그인 정보 패스워드 = db 저장된 사용자 패스워드 일치?
verifyCredentials(authToken.getCredentials(), password);
//사용자의 권한 생성
Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(member.getRoles());
//인증된 사용자의 인증정보 리턴
return new UsernamePasswordAuthenticationToken(username, password, authorities);
}
// HelloUserAuthenticationProvider가 Username/Password 방식의
//인증을 지원한다는 것을 Spring Security에게 알려준다.
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
private void verifyCredentials(Object credentials, String password) {
if (!passwordEncoder.matches((String)credentials, password)) {
throw new BadCredentialsException("Invalid User name or User Password");
}
}
}
최종본 코드 기반 정리 🔥
📁auth
ㄴ 📁utils - HelloAuthorityUtils
- AuthController : 각종 폼과 화면, html 파일들 매핑
- HelloUserDetailsService
: UserDetailsService 구현체 (user 정보 로드하는 핵심 인터페이스)
* memberRepository, authorityUtils DI 받음
(db에서 멤버 찾음, 권한 정보 생성 위해)
* UserDetails : 로드되는 핵심 User 정보 표현 인터페이스
- HelloUserAuthenticationProvider : Spring Security 가 아닌 직접 로그인 인증 처리 줄 경우 코드
📁config
- JavaConfiguration
: memberRepository, passwordEncoder, helloAuthorityUtils DI
해당하는 memberService Bean으로 등록
- SecurityConfiguration
: SecurityFilterChain, HttpSecurity
1) security 관련 폼 설정, 보안 설정, 권한 uri 설정
2) passwordEncoder Bean으로 등록
📁member
- MemberService : 인터페이스
- InMemoryMemberService : Inmemory 방식 (db 저장 X, 코드에서 User 생성 및 권한 부여)
- DBMemberService : DB에 User 정보 저장, role(권한)도 저장
- MemberRepository
'Spring > Spring Security' 카테고리의 다른 글
[JWT] JWT(JSON Web Token)란? (0) | 2022.11.23 |
---|---|
[JWT] 자격 증명 방식 : 세션 기반 vs 토큰 기반 (0) | 2022.11.23 |
[Spring Security] 웹 요청의 일반적 처리 흐름 / Spring Security 의 인증 & 권한 부여 처리 흐름 (1) | 2022.11.22 |
[Spring Security] Spring Security 란? + 용어 정리 (0) | 2022.11.22 |
[Spring Security] Spring Security의 기본 구조, 동작 방식 (1) - InMemory User (0) | 2022.11.21 |
댓글