Authentication
는 인증된 결과만 저장하는 것이 아니고, 인증을 하기 위한 정보와 인증을 받기 위한 정보가 하나의 객체에 동시에 들어 있다.
-> 인증을 제공해줄 제공자(AuthenticationProvider)가 어떤 Authentication에 대해서 허가를 내줄 것인지 판단하기 위해서는 직접 입력된 Authentication을 보고 허가된 Authentication을 내주는 방식이기 때문
-> AuthenticationProvider 는 처리 가능한 Authentication에 대해 알려주는 support
메소드를 지원, authenticate()
에서 Authentication을 입력값과 동시에 출력값으로도 사용
Authorities
에는 '어디를 갈 수 있는지', '어떤 역할을 할 수 있는지'에 대한 권한 정보로 이들이 구현(implement)한 GrantedAuthority 인터페이스에 관한 정보들이 저장되어있다
UsernamePasswordAuthenticationFilter 가 해주던 일을 직접 구현해야 함
CustomLoginFilter 를 쓸 경우 successHandler와 failureHandler 를 별도로 구현
default 페이지와 caching 된 request 페이지로 redirect 하는 기능도 직접 구현
-> 현재로서는 CustomLoginFilter 와 UsernamePasswordAuthenticationFilter 를 동시에 사용하는 것이 가장 현명한 대안
Controller
@Controller
public class HomeController {
@GetMapping("/")
public String index(){
return "index";
}
@GetMapping("/login")
public String login(){
return "loginForm";
}
@GetMapping("/login-error")
public String loginError(Model model){
model.addAttribute("loginError", true);
return "loginForm";
}
@GetMapping("/access-denied")
public String accessDenied(){
return "accessDenied";
}
@ResponseBody
@GetMapping("/auth")
public Authentication auth(){
return SecurityContextHolder.getContext().getAuthentication();
}
}
@Controller
@RequestMapping("/student")
public class StudentController {
@PreAuthorize("hasAnyAuthority('ROLE_STUDENT')")
@GetMapping("/main")
public String main(){
return "StudentMain";
}
}
@Controller
@RequestMapping("/teacher")
public class TeacherController {
@PreAuthorize("hasAnyAuthority('ROLE_TEACHER')")
@GetMapping("/main")
public String main(){
return "TeacherMain";
}
}
Student
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
// principal 즉 인증 대상을 만들기
public class Student {
private String id;
private String username;
//Authentication 인증을 하려면 GrantedAuthority 필요
private Set<GrantedAuthority> role;
}
StudentAuthenticationToken
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
// 사이트에 들어올 학생이 가지게 되는 일종의 통행증( 인증 토큰), Authentication의 구현체
public class StudentAuthenticationToken implements Authentication {
private Student principal;
private String credentials;
private String details;
private boolean authenticated;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return principal == null ? new HashSet<>() : principal.getRole();
}
@Override
public String getName() {
return principal == null ? "" : principal.getUsername();
}
}
StudentManager(ProductManager)
// 통행증을 발급할 매니저
// component이기에 initalizingBean 사용 가능
// StuentManager가 AuthenticationProvider가 되어 Hong이라는 ID의 사용자가 오면 Student를 담은 Authentication Token
// 즉 통행증을 발행해 주겠다
// Authentication의 Manager에 등록하기 위해 SecurityConfig에 코드 작성
@Component
public class StudentManager implements AuthenticationProvider, InitializingBean {
// 매니저가 가지는 학생 리스트
private HashMap<String, Student> studentDB = new HashMap<>();
// token을 StudentAuthenticationToken으로 발급하겠다
// 인자로는 UserNameFilter를 거친 Authentication이 도착하고
// 이를 StudentAuthenticationToken(Authenciation 구현체) 형태로 return
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
StudentAuthenticationToken token = (StudentAuthenticationToken) authentication;
if(studentDB.containsKey(token.getCredentials())){
Student student = studentDB.get(token.getCredentials());
return StudentAuthenticationToken.builder()
.principal(student)
.credentials(null)
.details(student.getUsername())
.authenticated(true)
.build();
}
return null; // 내가 처리할 수 없는 Authentication, false로는 X
}
// UsernamePasswordAuthenticationFilter 형태의 토큰을 처리할수 있다고 Authentication Manager에게 알려줌!!
// 검증을 해주는 Provider 역할을 하겠다
// 이후 커스터마이징한 토큰 처리하게 하였음
@Override
public boolean supports(Class<?> authentication) {
return authentication == StudentAuthenticationToken.class;
}
// 빈이 초기화 되었을 때 StudentDb에 저장할 학생 목록
@Override
public void afterPropertiesSet() throws Exception {
Set.of(
new Student("hong", "홍길동", Set.of(new SimpleGrantedAuthority("ROLE_STUDENT"))),
new Student("kang", "강아지", Set.of(new SimpleGrantedAuthority("ROLE_STUDENT"))),
new Student("rang", "호랑이", Set.of(new SimpleGrantedAuthority("ROLE_STUDENT")))
).forEach(s ->
studentDB.put(s.getId(),s)
);
}
}
Teacher은 Student와 구조 비슷
Security Config
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final StudentManager studentManager;
private final TeacherManager teacherManager;
public SecurityConfig(StudentManager studentManager, TeacherManager teacherManager) {
this.studentManager = studentManager;
this.teacherManager = teacherManager;
}
// Authentication Provider 등록
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(studentManager);
auth.authenticationProvider(teacherManager);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
CustomLoginFilter filter = new CustomLoginFilter(authenticationManager());
http
.authorizeRequests(request->
request.antMatchers("/", "/login").permitAll()
.anyRequest().authenticated()
)
// .formLogin(
// // login페이지를 통해 UsernameAuthenticationFilter가 동작하도록
// login->login.loginPage("/login").permitAll()
// .defaultSuccessUrl("/", false)
// .failureUrl("/login-error")
// )
// 커스터 마이징한 필터 위치 추가
// 만약 위 코드 주석 처리를 풀면 두개가 동시에 동작, 밑에 코드만으로는 에러및 성공 핸들링 불가
// 주석 풀고 동작시 에러 및 성공 Handling이 가능
.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class )
.logout(logout->logout.logoutSuccessUrl("/"))
.exceptionHandling(e -> e.accessDeniedPage("/access-denied"))
;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
;
}
}
Custom Login Filter
public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter {
public CustomLoginFilter(AuthenticationManager authenticationManager){
super(authenticationManager);
}
// 토큰 만드는 코드
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
// 선생인지 학생인지
String type = request.getParameter("type");
if(type == null || !type.equals("teacher")){
//student
StudentAuthenticationToken token = StudentAuthenticationToken.builder()
.credentials(username).build();
return this.getAuthenticationManager().authenticate(token);
} else{
//teacher
TeacherAuthenticationToken token = TeacherAuthenticationToken.builder()
.credentials(username).build();
return this.getAuthenticationManager().authenticate(token);
}
}
}
인증 제공자들을 관리하는 인터페이스가 AuthenticationManager (인증 관리자)
이고, 이 인증 관리자를 구현한 객체가 ProviderManager
ProviderManager 도 복수개 존재 가능
개발자가 직접 AuthenticationManager를 정의해서 제공하지 않는다면, AuthenticationManager 를 만드는 AuthenticationManagerFactoryBean 에서 DaoAuthenticationProvider
를 기본 인증제공자로 등록한 AuthenticationManage를 만듬
DaoAuthenticationProvider
는 반드시 1개의 UserDetailsService 를 발견할 수 있어야 한다. 만약 없으면 InmemoryUserDetailsManager
에 [username=user, password=(서버가 생성한 패스워드)]인 사용자가 등록되어 제공
실제 개발시에는 Provider Manager 개발하는 경우 X