-조건
SSR방식임 : 서버에서 HTML을 만들어 클라이언트 쪽으로 내려주는 방식
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@Controller
@RequestMapping("/members")
public class MemberController {
private final MemberService memberService;
private final MemberMapper mapper;
...
...
// 회원 가입 폼에서 전송한 회원 정보가 DB에 저장되는 핸들러 메소드
@PostMapping("/register")
public String registerMember(@Valid MemberDto.Post requestBody) {
Member member = mapper.memberPostToMember(requestBody);
memberService.createMember(member);
System.out.println("Member Registration Successfully");
return "login";
}
}
registerMember() 핸들러 메서드를 통해 회원 가입 폼에서 전송한 회원 정보가 우리가 잘 알고 있는 서비스 계층과 데이터 액세스 계층을 거쳐서 데이터베이스에 저장됩니다.
package com.codestates.auth;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/auths")
public class AuthController {
@GetMapping("/login-form")
public String loginForm() {
return "login";
}
@GetMapping("/access-denied")
public String accessDenied() {
return "access-denied";
}
// 로그인 버튼 누르면 이 메소드 요청됨
@PostMapping("/login")
public String login() {
System.out.println("Login successfully!");
return "home";
}
}
적용하면 우리가 원하는 인증 방식과 웹페이지에 대한 접근 권한을 설정할 수 있다.
package com.codestates.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class SecurityConfiguration {
@Bean
public UserDetailsManager userDetailsService() {
// (1)
UserDetails userDetails =
User.withDefaultPasswordEncoder() // (1-1)
.username("kevin@gmail.com") // (1-2)
.password("1111") // (1-3)
.roles("USER") // (1-4)
.build();
// (2)User 클래스를 이용해서 사용자의 인증 정보를 생성한다
return new InMemoryUserDetailsManager(userDetails);
}
}
InMemory Single User 인증 정보 설정
전에는 실행 할때마다 랜덤으로 생성되는 패스워드를 이용해야했는데
사용자 계정 정보를 메모리상에 지정했기때문에 사용자 계정 정보가 안 바뀐다.
코드 설명
UserDetails
인터페이스는 인증된 사용자의 핵심 정보를 포함하고 있다.
1-1 withDefaultPasswordEncoder()
는 디폴트 패스워드 인코더를 이용해 사용자 패스워드를 암호화합니다. →• (1-3)의 password()
메서드의 파라미터로 전달한 “1111”을 암호화해 줍니다.
1-2 이메일을 username으로 지정
1-3 사용자의 password를 설정
1-4 역할을 지정하는 메소드 : (ex) 일반 사용자,관리자)
UserDetailsManager
인터페이스 : 사용자의 핵심 정보를 포함한 UserDetails를 관리함하지만 이 코드에선 메모리상에서 UserDetails를 관리 하기에 InMemoryUserDetailsManager라는 구현체 쓴다.
(2) new InMemoryUserDetailsManager(userDetails)
를 통해 UserDetailsManager
객체를 Bean으로 등록하면 Spring에서 해당 Bean이 가지고 있는 사용자 인증 정보가 클라이언트의 요청으로 넘어 올경우에 정상적인 인증 프로세스를 수행한다.
@Configuration
public class SecurityConfiguration {
// (1)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// HttpSecurity를 통해 HTTP 요청에 대한 보안 설정을 구성한다.
...
...
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("kevin@gmail.com")
.password("1111")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
(1) HttpSecurity를 파라미터로 가지고, SecurityFilterChain을 리턴하는 형태의 메서드를 정의하면 HTTP 보안 설정을 구성할 수 있습니다.
커스텀 로그인 페이지 설정
package com.codestates.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // (1)
.formLogin() // (2)
.loginPage("/auths/login-form") // (3) 페이지 사용
.loginProcessingUrl("/process_login") // (4)
.failureUrl("/auths/login-form?error") // (5)
.and() // (6)
.authorizeHttpRequests() // (7)
.anyRequest() // (8)
.permitAll(); // (9)
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("kevin@gmail.com")
.password("1111")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
Spring Security의 보안 구성 중에서 우리가 만들어 둔 커스텀 로그인 페이지를 사용하기 위한 최소한의 설정만 추가한 코드입니다.
코드 기능들
(1) CSRF(Cross-Site-Request Forgery) 공격에 대한 Spring Security에 대한 설정을 비활성화 한다.
Spring Security는 기본적으로 아무 설정 안하면 csrf() 공격을 방지하기 위해 클라이언트로부터 CSRF Token을 수신 후 검증함.
만약, csrf().disable()
설정을 하지 않는다면 403 에러로 인해 정상적인 접속이 불가능합니다.
(2) formLogin()
을 통해 기본적인 인증 방법을 폼 로그인 방식으로 지정합니다.
(3) loginPage("/auths/login-form")
메서드를 통해 우리가 템플릿 프로젝트에서 미리 만들어 둔 커스텀 로그인 페이지를 사용하도록 설정합니다.
여기서 "/auths/login-form"
은 AuthController의 loginForm()
핸들러 메서드에 요청을 전송하는 요청 URL입니다.
(4) loginProcessingUrl("/process_login")
메서드를 통해 로그인 인증 요청을 수행할 요청 URL을 지정합니다.
"/process_login"
은 우리가 만들어 둔 login.html에서 form 태그의 action 속성에 지정한 URL과 동일합니다.(5) failureUrl("/auths/login-form?error")
메서드를 통해 로그인 인증에 실패할 경우 어떤 화면으로 리다이렉트 할 것인가를 지정합니다.
로그인 실패 할경우 failureUrl()의 파라미너로 커스터 로그인 페이지 URL인 "/auths/login-form?error"을 보여준다.
(6) and() 메소드로 Spring Security 보안 설정을 메소드 체인 형태로 구성할 수 있다.
7), (8), (9)를 통해서 클라이언트의 요청에 대해 접근 권한을 확인합니다. 접근을 허용할지 여부를 결정합니다.
사용자에게 부여된 Role을 이용해서 샘플 애플리케이션의 request URI에 접근 권한을 부여
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@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()
.exceptionHandling().accessDeniedPage("/auths/access-denied") // (1)
.and()
.authorizeHttpRequests(authorize -> authorize // (2)
.antMatchers("/orders/**").hasRole("ADMIN") // (2-1)
.antMatchers("/members/my-page").hasRole("USER") // (2-2)
.antMatchers("⁄**").permitAll() // (2-3)
);
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("kevin@gmail.com")
.password("1111")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
위에 코드에서 .authorizeHttpRequests().anyRequest().permitAll(); 설정을 통해 로그인 인증에 성공했을때 모든 화면에 접근 가능 했던 부분을 이번 코드로 사용자의 Role 별로 request URI에 접근 권한이 부여 되도록 수정.
(1) 권한이 없는 사용자가 특정 request URI에 접근 할 경우 발생하는 403 에러를 처리하는 페이지를 설정하는 코드다.
exceptionHandling()
메서드는 메서드의 이름 그대로 Exception을 처리하는 기능한다.
리턴하는 ExceptionHandlingConfigurer
객체를 통해 구체적인 Exception 처리를 할 수 있습니다.
accessDeniedPage()
메서드는 403 에러 발생 시, 파라미터로 지정한 URL로 리다이렉트 되도록 해줍니다.
(2) authorizeHttpRequests()
메서드는 람다 표현식을 통해 request URI에 대한 접근 권한을 부여할 수 있다.
antMatchers()
메소드는 ant라는 빌드 툴에서 사용되는 Path Pattern을 이용해 매치되는 URL을 표현/orders/*
라는 URL을 지정했다면 /orders/1
ADMIN Role을 가진 사용자 추가
@Configuration
public class SecurityConfiguration {
...
...
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("kevin@gmail.com")
.password("1111")
.roles("USER")
.build();
// (1)
UserDetails admin =
User.withDefaultPasswordEncoder()
.username("admin@gmail.com")
.password("2222")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
1)과 같이 admin@gmail.com
이라는 InMemory User 하나를 더 추가하였으며, admin@gmail.com
에게는 ADMIN
Role이 부여되었습니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@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() // (1)
.logoutUrl("/logout") // (2)
.logoutSuccessUrl("/") // (3)
.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();
}
...
...
}
href=”/logout”
과 동일해야 합니다.여기선 로그아웃 이후 샘플 애플리케이션의 메인 화면으로 리다이렉트하도록 지정했음.
InMemory User를 사용하는 방식은 테스트 환경이나 데모 환경에서 사용할 수 있는 방법입니다.
회원 가입 폼을 통해 InMemory User를 등록하기 위한 작업 순서는 다음과 같습니다.
PasswordEncoder는 Spring Security에서 제공하는 패스워드 암호화 기능을 제공하는 컴포넌트이다.
그래서 회원 가입 폼에서 전달받은 패스워드는 InMemory User로 등록하기 전에 암호화 되어야한다.
PasswordEncoder Bean 등록
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
@Configuration
public class SecurityConfiguration {
...
...
@Bean
public UserDetailsManager userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("kevin@gmail.com")
.password("1111")
.roles("USER")
.build();
UserDetails admin =
User.withDefaultPasswordEncoder()
.username("admin@gmail.com")
.password("2222")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
// (1) PasswordEncoder을 Bean으로 등록
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); // (1-1)
}
}
(1-1)의 PasswordEncoderFactories.createDelegatingPasswordEncoder();
를 통해 DelegatingPasswordEncoder
를 먼저 생성하는데, 이 DelegatingPasswordEncoder
가 실질적으로 PasswordEncoder구현 객체를 생성해 준다.
→우리가 userDetailsService() 메서드에서 미리 생성하는 InMemoryUser의 패스워드는 내부적으로 디폴트 PasswordEncoder를 통해 암호화된다!!!
InMemory User 등록을 위한 InMemoryMemberService 클래스
InMemory User를 등록하기 위한 MemberService 인터페이스 구현 클래스인 클래스다.
package com.codestates.member;
public class InMemoryMemberService implements MemberService {
public Member createMember(Member member) {
return null;
}
}
데이터베이스에 User를 등록하기 위한 DBMemberService 클래스
데이터베이스에 User를 등록하기 위한 MemberService 인터페이스의 구현 클래스인 DBMemberService
클래스이다.
JavaConfiguration 구성
JavaConfiguration 클래스에서는 MemberService 인터페이스의 구현 클래스인 InMemoryMemberService를 Spring Bean으로 등록한다.
import com.codestates.member.InMemoryMemberService;
import com.codestates.member.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.UserDetailsManager;
@Configuration
public class JavaConfiguration {
// (1)
@Bean
public MemberService inMemoryMemberService(UserDetailsManager userDetailsManager,
PasswordEncoder passwordEncoder) {
return new InMemoryMemberService(userDetailsManager, passwordEncoder);
}
}
(1)은 InMemoryMemberService Bean 객체를 생성함.
데이터 베이스 연동 없이 메모리에 Spring Secudiry의 User를 등록해야 해서 UserDetailsManager 객체가 필요하다.
그리고 User 등록시 , 패스워드를 암호화한 후에 등록해야 해서 Spring Security에서 제공하는 PasswordEncoder객체가 필요하다.
그래서 이 두 객체를 InMemoryMemberService 객체 생성 시, DI 해 준다.
회원 가입 정보를 전달받아 Spring Security의 User를 메모리에 등록해 주는 InMemoryMemberService 클래스의 코드이다.
package com.codestates.member;
import com.codestates.auth.utils.AuthorityUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.UserDetailsManager;
import java.util.List;
public class InMemoryMemberService implements MemberService { // (1)
private final UserDetailsManager userDetailsManager;
private final PasswordEncoder passwordEncoder;
// (2)
public InMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
this.userDetailsManager = userDetailsManager;
this.passwordEncoder = passwordEncoder;
}
public Member createMember(Member member) {
// (3) 권한 목록 생성 및 권한 지정
List<GrantedAuthority> authorities = createAuthorities(Member.MemberRole.ROLE_USER.name());
// (4) User 패스워드 암호화
String encryptedPassword = passwordEncoder.encode(member.getPassword());
// (5)User 등록
UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities);
// (6)
userDetailsManager.createUser(userDetails);
return member;
}
private List<GrantedAuthority> createAuthorities(String... roles) {
// (3-1)
return Arrays.stream(roles)
.map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList());
}
}
implements MemberService
를 지정합니다. 우리가 여태껏 @Service
애너테이션을 사용해 특정 서비스 클래스를 Bean으로 등록하는 방법을 사용해 왔지만 여기서는 @Service
을 사용하지 않고, JavaConfiguration을 이용해 Bean을 등록하고 있다는 사실을 기억하기 위해 이렇게 한다.(2) UserDetailsManager,PasswordEndcoder를 DI받음
SecurityConfiguration
에서 Bean으로 등록한 UserDetailsManager는 InMemoryUserDetailsManager
이므로 여기서 DI 받은 UserDetailsManager 인터페이스의 하위 타입은InMemoryUserDetailsManager
라는 사실을 기억 해야한다.(3) Spring Security에서 User 를 등록 할려면 해당 User의 권한을 지정해줘야한다.
그래서 createAuthorities(Member.MemberRole.ROLE_USER.name());를 해서 User 권한 목록을 List로 생성했다.
Member 클래스에는 MemberRole 이라는 enum이 정의 되어 있고, ROLE_USER
와 ROLE_ADMIN
이라는 enum 타입이 정의되어 있다.
SimpleGrantedAuthorit
를 사용해 Role 베이스 형태의 권한을 지정할 때 ‘ROLE_’ + 권한 명
형태로 지정해 주어야 한다. 안하면 적절한 권한 매핑이 이루어지지 않는다.(3-1) 에서 Java Stream API로 생성자 파라미터로 해당 USer Role을 전달 해 SimpleGrantedAuthority 객체를 생성해 List 형태로 리턴한다.
(4) PasswordEncoder를 이용해 등록할 User의 패스워드를 암호화한다.
만약 암호화 안하고 User 등록 한다면 User등록은 되는데 로그인 인증시
→ java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null”
이 에러가 뜬다. 그래서 무조건 패스워드를 암호화 해야한다.
(5) User 등록하기 위해 UserDetails를 생성함.
UserDetails
로 관리한다(6) UserDetailsManager의 createUser() 메소드를 이용해 User를 등록한다.
Member 엔티티 클래스로 회원 인증 정보를 포함해 회원 정보를 데이터베이스 테이블에서 관리해보자
Spring Security에서는 User 인증 정보를 테이블에 저장하고, 테이블에 저장된 인증 정보를 이용해 인증 프로세스를 진행할 수 있는 방법이 여러가지 있는데
그중 하나가 Custom UserDetailsService를 이용하는 방법이다.
package com.codestates.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin() // (1) 이건 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();
}
}
(1) H2사용하기 위한 설정이다.
여기서 frameOptions()는 HTML 태그 중에서 <frame>
이나 <iframe>
, <object>
태그에서 페이지를 렌더링할지의 여부를 결정하는 기능을 합니다.
.frameOptions().sameOrigin()
을 호출하면 동일 출처로부터 들어오는 request만 페이지 렌더링을 허용한다.
@Configuration
public class JavaConfiguration {
// (1)
@Bean
public MemberService dbMemberService(MemberRepository memberRepository,
PasswordEncoder passwordEncoder) {
return new DBMemberService(memberRepository, passwordEncoder); (1-1)
}
}
(1)과 같이 데이터베이스에 User의 정보를 저장하기 위해 MemberService 인터페이스의 구현 클래스를 DBMemberService로 변경합니다.
MemberRepository
와 PasswordEncoder
객체를 DI 해줍니다.DMMemberSerive는 User의 인증 정보를 데이터베이스에 저장하는 역할한다.
→ 이 말은 이 Member 엔티티 클래스의 필드에 인증 정보를 담는 password 필드가 포함 된다고 생각하면 된다.
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Transactional
public class DBMemberService implements MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
// (1) 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()); // (2)
member.setPassword(encryptedPassword); // (3)
Member savedMember = memberRepository.save(member);
System.out.println("# Create Member in DB");
return savedMember;
}
...
...
}
무조건 패스워드는 암호화 된 상태에서 복호화 할 이유가 없기땜누에 단방향 암호화 방식으로 암호화 되어야한다.
데이터베이스에서 조회한 User 인증정보를 기반으로 인증 처리 한다.
기억 해야할것
우리가 InMemory User를 등록하는 데 사용했던 InMemoryUserDetailsManager
는 UserDetailsManager
인터페이스의 구현체
UserDetailsManager
는 UserDetailsService
를 상속하는 확장 인터페이스
HelloUserDetailsService
데이터베이스에서 조회한 인증 정보를 기반으로 인증을 처리하는 Custom UserDetailsService인 HelloUserDetailsService
클래스의 코드
package com.codestates.auth;
import com.codestates.auth.utils.HelloAuthorityUtils;
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import com.codestates.member.Member;
import com.codestates.member.MemberRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@Component
public class HelloUserDetailsServiceV1 implements UserDetailsService { // (1)
private final MemberRepository memberRepository;
private final HelloAuthorityUtils authorityUtils;
// (2)
public HelloUserDetailsServiceV1(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.authorityUtils = authorityUtils;
}
// (3)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
// (4)
Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember.getEmail());
// (5)
return new User(findMember.getEmail(), findMember.getPassword(), authorities);
}
}
일단 코드 설명은
UserDetailsService
인터페이스를 구현해야 한다.→ DB에서 User의 인증 정보만 Spring Security에 넘겨 주고, 인증 처리는 Spirng Security가 대신 해준다.
—>UserDetailsService에 의해 로드되어 인증을 위해 사용되는 핵심 User 정보를 표현하는 인터페이스이다.
UserDetails
인터페이스의 구현체는 Spring Security에서 보안 정보 제공을 목적으로 직접 사용되지는 않고, Authentication
객체로 캡슐화 되어 제공 된다.
HelloAuthorityUtils
HelloUserDetailsService에서 Role 기반의 User 권한을 생성하기 위해 사용한 HelloAuthorityUtils 코드
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class HelloAuthorityUtils {
// (1)
@Value("${mail.address.admin}")
private String adminMailAddress;
// (2)
private final List<GrantedAuthority> ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER");
// (3)
private final List<GrantedAuthority> USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER");
public List<GrantedAuthority> createAuthorities(String email) {
// (4)
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES;
}
return USER_ROLES;
}
}
User 의 권한을 매핑, 생성하는 HelloAuthorityUtils
(1) application.yml에 추가한 프로퍼티를 가져오는 표현식이다.
@Value(”${프로퍼티 경로}”) 의 표현식 형태로 작성하면 application.yml에 정의되어 있는 프로퍼티의 값을 클래스 내에서 사용할 수 있다.
(1)에서 application.yml에 미리 정의한 관리자 권한을 가질 수 있는 이메일 주소를 불러오고 있다.
application.yml 파일에 정의한 관리자용 이멜 주소는 회원 등록 시, 특정 이메일 주소에 관리자 권한을 부여할 수 있는지를 결정하기 위해 사용된다.
application.yml파일에 이렇게 관리자 이메일 주소를 정의 해야함.
...
...
mail:
address:
admin: admin@gmail.com
(2)에서는 Spring Security에서 지원하는 AuthorityUtils 클래스를 이용해 관리자용 권한 목록을 List 객체로 미리 생성함
관리자 권한의 경우, 일반 사용자의 권한까지 추가로 포함되어 있다.
(3)에서는 Spring Security에서 지원하는 AuthorityUtils 클래스를 이용해 일반 사용 권한 목록을 List 객체로 미리 생성한다.
(4)에선 파라미터로 전달 받은 이메일 주소가 application.yml파일에서 나온 관리자용 이메일 주소와 동일하면 관리자용 권한인 ADMIN_ROLES를 리턴한다.
회원 가입 메뉴에서 회원 등록하고 H2에서 확인하면
이렇게 등록이 된다.
PASSWORD는 역시 암호화가 되어 있고, 로그인 해보면 정상적으로 로그인이 된다.
여기서까지 DB에 회원 인증 정보를 저장하고, 저장된 인증 정보를 기반으로 로그인 인증 하는 데 문제는 없다.
여기서 조금더 깔끔하게 코드를 구성 하면
HelloUserDetailsService코드를
import com.codestates.auth.utils.HelloAuthorityUtils;
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import com.codestates.member.Member;
import com.codestates.member.MemberRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@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 {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember);
// (1) 개선하면 좋은 포인트
return new User(findMember.getEmail(), findMember.getPassword(), authorities);
}
}
개선을 하면 이렇게 짠다.
import com.codestates.auth.utils.HelloAuthorityUtils;
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import com.codestates.member.Member;
import com.codestates.member.MemberRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@Component
public class HelloUserDetailsServiceV2 implements UserDetailsService {
private final MemberRepository memberRepository;
private final HelloAuthorityUtils authorityUtils;
public HelloUserDetailsServiceV2(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.authorityUtils = authorityUtils;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
return new HelloUserDetails(findMember); // (1) 개선된 부분
}
// (2) HelloUserDetails 클래스 추가
private final class HelloUserDetails extends Member implements UserDetails { // (2-1)
// (2-2)
HelloUserDetails(Member member) {
setMemberId(member.getMemberId());
setFullName(member.getFullName());
setEmail(member.getEmail());
setPassword(member.getPassword());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityUtils.createAuthorities(this.getEmail()); // (2-3) 리팩토링 포인트
}
// (2-4)
@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;
}
}
}
기존v1버전은 loadUserByUsername() 메서드의 리턴 값으로 new User(findMember.getEmail(), findMember.getPassword(), authorities);
을 리턴했지만,
v2에서는 new HelloUserDetails(findMember);으로 Custom UserDetails 클래스의 생성자로 findMember를 전달하면서 더 깔끔하게 나타냈다.
(2) HelloUserDetails 클래스는 UserDetails 인터페이스를 구현하고 있으며 Member 엔티티 클래스르 상속하고 있다
→ 이렇게 구성하면 DB에 조회한 회원 정보를 Spring Security의 User 정보로 변환하는 과정과 User 권한 정보를 생성하는 과정을 캡슐화 할수 있다.
Member 엔티티 클래스를 상속하고 있기에 HelloUserDetails를 리턴 받아 사용하는 측에서는 두개 클래스의 객체를 모두 다 손쉽게 캐스팅 해서 사용 가능하다느 장점이 있다.
(2-3) 에선 HelloAuthorityUtils의 createAuthorities() 메소드를 이용해 User 권한 정보를 생성하고 있다.
(2-4) username을 Member 클래스의 email 주소로 채우고 있다.getUsername()
의 리턴 값은 null일 수 없습니다.
Custom UserDetailsService를 이용한 로그인 인증의 마지막 단계이다.
User의 권한 정보를 데이터베이스에서 관리하기 위해서는 다음과 같은 과정이 필요한다.
User의 권한 정보를 저장하기 위한 테이블 생성
회원 가입 시, User의 권한 정보(Role)를 데이터베이스에 저장하는 작업
로그인 인증 시, User의 권한 정보를 데이터베이스에서 조회하는 작업
User의 권한 정보 테이블 생성
User의 권한 정보 테이블을 생성하기 전에 User와 User의 권한 정보 간에 관계
를 먼저 생각해야 한다
관계?는 테이블간의 연관관계 → JPA로 연관 관계를 맺을 수 있다.
Spring Security의 User 역할을 하는 Member 엔티티 클래스에 User 권한 정보 매핑
import com.codestates.audit.Auditable;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member extends Auditable implements Principal{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
@Column(length = 100, nullable = false)
private String fullName;
@Column(nullable = false, updatable = false, unique = true)
private String email;
@Column(length = 100, nullable = false)
private String password;
@Enumerated(value = EnumType.STRING)
@Column(length = 20, nullable = false)
private MemberStatus memberStatus = MemberStatus.MEMBER_ACTIVE;
// (1) User의 권한 정보 테이블과 매핑되는 정보
@ElementCollection(fetch = FetchType.EAGER)
private List<String> roles = new ArrayList<>();
public Member(String email) {
this.email = email;
}
public Member(String email, String fullName, String password) {
this.email = email;
this.fullName= fullName;
this.password = password;
}
@Override
public String getName() {
return getEmail();
}
public enum MemberStatus {
MEMBER_ACTIVE("활동중"),
MEMBER_SLEEP("휴면 상태"),
MEMBER_QUIT("탈퇴 상태");
@Getter
private String status;
MemberStatus(String status) {
this.status = status;
}
}
public enum MemberRole {
ROLE_USER,
ROLE_ADMIN
}
}
(1)과 같이 List, Set 같은 컬렉션 타입의 필드는 @ElementCollection
애너테이션을 추가하면 User 권한 정보와 관련된 별도의 엔티티 클래스를 생성하지 않아도 간단하게 매핑 처리가 됩니다.
회원 가입 시, 해당 회원의 권한 정보를 MEMBER_ROLES
테이블에 저장해보자
DBMemberService
회원 등록 시, 권한 정보를 DB에 저장
import com.codestates.auth.utils.HelloAuthorityUtils;
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Transactional
public class DBMemberService implements MemberService {
...
...
private final HelloAuthorityUtils authorityUtils;
...
...
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
String encryptedPassword = passwordEncoder.encode(member.getPassword());
member.setPassword(encryptedPassword);
// (1) Role을 DB에 저장
List<String> roles = authorityUtils.createRoles(member.getEmail());
member.setRoles(roles);
Member savedMember = memberRepository.save(member);
return savedMember;
}
...
...
}
DBMemberService에서 회원 등록 시, 회원의 권한 정보를 데이터베이스에 저장하는 코드가 추가한다.
(1)에서 authorityUtils.createRoles(member.getEmail());
를 통해 회원의 권한 정보(List<String> roles
)를 생성한 뒤 member 객체에 넘겨준다.
HelloAuthorityUtils
클래스의 코드
회원의 Role 정보를 생성하는 createRoles() 메소드 추가함
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class HelloAuthorityUtils {
@Value("${mail.address.admin}")
private String adminMailAddress;
...
...
private final List<String> ADMIN_ROLES_STRING = List.of("ADMIN", "USER");
private final List<String> USER_ROLES_STRING = List.of("USER");
...
...
// (1) DB 저장용
public List<String> createRoles(String email) {
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES_STRING;
}
return USER_ROLES_STRING;
}
}
(1)에서 파라미터로 전달된 이메일 주소가 application.yml 파일의 mail.address.admin 프로퍼티에 정의된 이메일 주소와 동일하면 관리자 Role 목록 ADMIN_ROLES_STRING 리턴하고, 아니면 일반사용자 ROLE 목록을 리턴한다.
마지막 작업
로그인 인증에 성공 시, 제공하는 User 권한 정보를 DB 테이블에서 관리되는 Role을 기반으로 생성한다.
개선된 HelloUserDetailsServiceV3
DB에서 조회한 Role을 기반으로 User의 권한 정보 생성
package com.codestates.auth;
import com.codestates.auth.utils.HelloAuthorityUtils;
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import com.codestates.member.Member;
import com.codestates.member.MemberRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@Component
public class HelloUserDetailsServiceV3 implements UserDetailsService {
private final MemberRepository memberRepository;
private final HelloAuthorityUtils authorityUtils;
public HelloUserDetailsServiceV3(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.authorityUtils = authorityUtils;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
return new HelloUserDetails(findMember);
}
private final class HelloUserDetails extends Member implements UserDetails {
HelloUserDetails(Member member) {
setMemberId(member.getMemberId());
setFullName(member.getFullName());
setEmail(member.getEmail());
setPassword(member.getPassword());
setRoles(member.getRoles()); // (1)
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// (2) DB에 저장된 Role 정보로 User 권한 목록 생성
return authorityUtils.createAuthorities(this.getRoles());
}
...
...
}
}
데이터베이스의 MEMBER_ROLES 테이블에서 조회한 Role을 기반으로 User의 권한 목록(List<GrantedAuthority>
)을 생성하는 로직이 추가된 HelloUserDetailsService 클래스이다.
extends Member
)에 전달한 Role 정보를 authorityUtils.createAuthorities() 메서드의 파라미터로 전달해서 권한 목록(List<GrantedAuthority>
)을 생성한다.참고로
데이터베이스에서 Role 정보를 가지고 오지 않았을 때는 authorityUtils.createAuthorities(this.getRoles());
아니라 authorityUtils.createAuthorities(this.getEmail());
이였다는것
HelloAuthorityUtils
데이터베이스에서 조회한 Role 정보를 기반으로 User의 권한 목록 생성하는 createAuthorities(List<String> roles)
메서드가 추가된 HelloAuthorityUtils
클래스
데이터베이스에서 조회한 Role 정보를 기반으로 User의 권한 목록 생성
package com.codestates.auth.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class HelloAuthorityUtils {
@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");
private final List<String> ADMIN_ROLES_STRING = List.of("ADMIN", "USER");
private final List<String> USER_ROLES_STRING = List.of("USER");
// 메모리 상의 Role을 기반으로 권한 정보 생성.
public List<GrantedAuthority> createAuthorities(String email) {
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES;
}
return USER_ROLES;
}
// (1) DB에 저장된 Role을 기반으로 권한 정보 생성
public List<GrantedAuthority> createAuthorities(List<String> roles) {
List<GrantedAuthority> authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // (2)
.collect(Collectors.toList());
return authorities;
}
...
...
}
단순히 데이터베이스에서 가지고 온 Role 목록(List<String> roles
)을 그대로 이용해서 권한 목록(authorities
)을 만들면 된다.
(2)와 같이 SimpleGrantedAuthority
객체를 생성할 때 생성자 파라미터로 넘겨주는 값이 “ USER
" 또는 “ADMIN
"으로 넘겨주면 안 되고 “ROLE_USER
" 또는 “ROLE_ADMIN
" 형태로 넘겨주어야 한다
이 앞에는 Custom AuthenticationProvider을 사용해서 Spring Security가 내부적으로 인증을 대신 처리 해주는 방식인데,
이번에는 Custom AuthenticationProvider을 이용해 우리가 직접 로그인 인증 처리하는 방법이다.
HelloUserAuthenticationProvider(V1)
Custom AuthenticationProvider인 HelloUserAuthenticationProvider
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@Component
public class HelloUserAuthenticationProvider implements AuthenticationProvider { // (1)
private final HelloUserDetailsServiceV1 userDetailsService;
private final PasswordEncoder passwordEncoder;
public HelloUserAuthenticationProvider(HelloUserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
// (3)
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication; // (3-1)
// (3-2)
String username = authToken.getName();
Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid User name or User Password"));
// (3-3)
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String password = userDetails.getPassword();
verifyCredentials(authToken.getCredentials(), password); // (3-4)
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities(); // (3-5)
// (3-6)
return UsernamePasswordAuthenticationToken.authenticated(username, password, authorities);
}
// (2) 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");
}
}
}
AuthenticationProvider 인터페이스의 구현 클래스로 정의한다.
(1)과 같이 AuthenticationProvider 인터페이스의 구현 클래스로 정의한다.
AuthenticationProvider를 구현한 구현 클래스가 Spring Bean으로 등록되어 있다면 해당 AuthenticationProvider를 이용해서 인증을 진행한다.
클라이언트 쪽에서 로그인 인증을 시도하면 우리가 구현한 HelloUserAuthenticationProvider가 직접 인증을 처리하게 됩니다.
authenticate(Authentication authentication)
메서드와 supports(Class<?> authentication)
메서드를 구현해야 합니다. 그중에서 (2)의 supports(Class<?> authentication)
메서드는 우리가 구현하는 Custom AuthenticationProvider(HelloUserAuthenticationProvider
)가 Username/Password 방식의 인증을 지원한다는 것을 Spring Security에 알려주는 역할을 합니다. supports()
메서드의 리턴값이 true
일 경우, Spring Security는 해당 AuthenticationProvider의 authenticate() 메서드를 호출해서 인증을 진행합니다.만약 회원 가입을 하지 않고 로그인을 시도할 경우(회원 가입 이후에는 상관없습니다)인증에 실패하고, 이런 화면을 만난다.
HelloAuthenticationProvider을 통한 인증 실패 시 화면
1번 그림
우리가 앞에서 HelloUserDetailsService 를 이용해 인증을 처리할 경우에는 인증 실패 시, Spring Security 내부에서 인증 실패에 대한 전용 Exception인 AuthenticationException 을 throw 하게 되고 이 AuthenticationException 이 throw 되면 결과적으로 SecurityConfiguration에서 설정한 .failureUrl("/auths/login-form?error") 을 통해 로그인 폼으로 리다이렉트 하면서 아래와 같이 “로그인 인증에 실패했습니다.”라는 인증 실패 메시지를 표시한다.
2번 그림
HelloUserDetailsService 를 이용해 인증 처리 시, 인증 실패 화면
Custom AuthenticationProvider를 이용할 경우에는 회원가입 전 인증 실패 시 2번 그림 같은 화면이 표시되지 않고 1번 그림과 같은 “Whitelebel Error Page”가 표시되는 걸까요?
→ MemberService에서 등록된 회원 정보가 없으면, BusinessLogicException을 throw 하는데 이 BusinessLogicException이 Cusotm AuthenticationProvider를 거쳐 그대로 Spring Security 내부 영역으로 throw 되기 때문이다.
Spring Security에서는 인증 실패 시, AuthenticationException
이 throw 되지 않으면 Exception에 대한 별도의 처리를 하지 않고, 서블릿 컨테이너인 톰캣 쪽으로 이 처리를 넘긴다.
결국 서블릿 컨테이너 영역에서 해당 Exception에 대해 “/error” URL로 포워딩하는데 우리가 특별히 “/error” URL로 포워딩 되었을 때 보여줄 뷰 페이지를 별도로 구성하지 않았기 때문에 디폴트 페이지인 “Whitelebel Error Page”를 브라우저에 표시하는 것이다.
해결책은 Custom AuthenticationProvicer에서 Exception이 발생할 경우, 이 Exception을 catch해서 AuthenticationException으로 rethrow를 해주면 됩니다.
개선된 HelloUserAuthenticationProvider(V2)
AuthenticationException
이 아닌 다른 Exception이 발생할 경우 AuthenticationException
으로 다시 rethrow 하도록 개선된 HelloUserAuthenticationProvider 코드
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Optional;
@Component
public class HelloUserAuthenticationProvider implements AuthenticationProvider {
private final HelloUserDetailsServiceV3 userDetailsService;
private final PasswordEncoder passwordEncoder;
public HelloUserAuthenticationProvider(HelloUserDetailsServiceV3 userDetailsService,
PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
// V2: AuthenticationException을 rethrow 하는 개선 코드
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authToken = (UsernamePasswordAuthenticationToken) authentication;
String username = authToken.getName();
Optional.ofNullable(username).orElseThrow(() -> new UsernameNotFoundException("Invalid User name or User Password"));
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String password = userDetails.getPassword();
verifyCredentials(authToken.getCredentials(), password);
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
return UsernamePasswordAuthenticationToken.authenticated(username, password, authorities);
} catch (Exception ex) {
throw new UsernameNotFoundException(ex.getMessage()); // (1) AuthenticationException으로 다시 throw 한다.
}
}
@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");
}
}
}
(1)에서 UsernameNotFoundException
을 throw 하도록 수정되었는데, UsernameNotFoundException
은 AuthenticationException
을 상속하는 하위 Exception이기 때문에 이 UsernameNotFoundException
이 throw되면 Spring Security 쪽에서 정상적으로 catch해서 그림2번 같이 정상적인 인증 실패 화면으로 리다이렉트 시켜준다.
Custom AuthenticationProvider에서 AuthenticationException
이 아닌 Exception이 발생할 경우에는 꼭 AuthenticationException
을 rethrow 하도록 코드를 구성해야 한다
AuthenticationProvider