* 인증 ( Authentication )
Authentication은 사이트에서 "통행증" 역할을 한다. Authentication을 구현하는 객체들은 대체로 메서드 이름 맨 뒤에 AuthenticationToken이 붙는다. 그래서 구현체들을 인증토큰이라고 부른다.
Credentials : 인증을 위해 필요한 정보(input, 비밀번호)
Principal : 인증된 결과, 인증대상(output)
Details : 기타 정보, 인증 관련 주변 정보들(ip, session 관련)
Authorities : 권한 정보들
(권한을 담당하는 GrantedAuthority를 구현한 객체들을 Authorities에 넣어둔다. )
Authentication 객체는 SecurityContextHolder를 통해 세션이 있건 없건 언제든 접근할 수 있도록 필터체인에서 보장해준다.
Authentication에는 인증 결과를 저장할 뿐만 아니라, 인증을 위한 정보도 들어있다. 왜냐하면, 인증 제공자(AuthenticationProvider)에게 Authentication이 넘겨져 인증 절차를 거쳐야 하기 때문이다. AuthenticationProvider는 인증을 검사 할 Authentication대상을 알려주는 supports()를 지원한다. supports()에 자신이 담당하는 인증토큰 방식을 정의하여 내가 담당한 인증토큰을 찾는다. authenticate()에서는 Authentication을 입력값과 출력값으로 사용하여 검증한다.
* 인증제공자 ( AuthenticationProvider )
인증 제공자(AuthenticationProvider)는 supports()로 자신이 인증 검사 할 인증 토큰을 정한다. 실제 검증 시에는, Authentication을 받아서 credentials을 검증하고 Principal Authentication을 리턴한다. input, output 모두 Authentication인데 일반적으로 인증을 위해 input되는 것은 credentials이고 인증 후 output 되는 것은 Principal이다. (혹은 input으로 Principal이 들어올 수도 있으며 확인하고 리턴한다.)
인증 제공자는 어떤 인증을 확인할지 인증 관리자(AuthenticationManager)에게 알려줘야 한다. supports() 메서드로 검사한다. 인증 대상과 방식이 다양할 수 있기 때문에 인증 제공자도 여러개 존재한다.
* 인증 관리자 ( Authentication Manager )
인증 제공자를 관리하는 인터페이스가 인증관리자(AuthenticationManger) 인터페이스이고, 인증관리자를 구현한 객체가 ProviderManager이다. ( 복수개 존재 가능 )
개발자가 인증관리자(AuthenticationManager)를 직접 구현하지 않는다면, AuthenticationManager를 만드는 AuthenticationManagerFactoryBean에서 DaoAuthenticationProvider를 기본 인증제공자로 등록하여 AuthenticationManager를 만든다.
DaoAuthenticationProvider는 반드시 1개의 UserDetailsService를 가져야 한다. 만약 없으면 InmemoryUserDetailsManager에 [username = user, password=(서버가 생성한 패스워드)]인 사용자가 제공된다.
일반적으로 실무에서 AuthenticationManager를 직접 정의하는 경우는 없다. UserDetails를 구현한 객체를 만들고 UserDeatilsService에 제공하면 DaoAuthenticationProvider가 인증 제공자에 위임해서 처리하기 때문이다.
* Authentication 구현하는 인증토큰 만들기
@Data
@Builder
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();
}
}
* 인증 제공자의 인증
@Component
public class StudentManager implements AuthenticationProvider, InitializingBean {
private final Map<String, Student> studentDB = new HashMap<>();
@Override
public boolean supports(Class<?> authentication) {
return authentication == UsernamePasswordAuthenticationToken.class;
}
UsernamePasswordAuthenticationToken 인증토큰을 발급받기 위해서 AuthenticationManager는 인증을 처리 할 AuthenticationProvider을 찾는다. 현재 정의된 supports()의 의미는 UsernamePasswordAuthenticationToken 를 내가 처리하겠다는 의미이다. 인증제공자에게 권한을 위임하도록 boolean으로 검사해서 true이면 해당 인증토큰을 진행한다.
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
if(studentDB.containsKey(token.getName())) {
Student student = studentDB.get(token.getName());
return StudentAuthenticationToken.builder()
.principal(student)
.credentials(null)
.details(student.getUsername())
.authenticated(true)
.build();
}
return null;
}
AuthenticationManager는 성공적으로 authenticate()가 완료되면 StudentAuthenticationToken 인증토큰을 발급한다.
@Override
public void afterPropertiesSet() throws Exception {
Set.of(
new Student("hong", "홍길동", Set.of(new SimpleGrantedAuthority("ROLE_USER"))),
new Student("gang", "강호동", Set.of(new SimpleGrantedAuthority("ROLE_USER"))),
new Student("bong", "봉태호", Set.of(new SimpleGrantedAuthority("ROLE_USER")))
).forEach(s ->
studentDB.put(s.getId(), s));
}
InitializingBean를 구현하여 afterPropertiesSet()으로 계정을 만든다.
* 인증 만들기
private final StudentManager studentManager;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(studentManager);
}
로그인 시, UsernamePasswordAuthenticationToken 타입의 토큰인증을 발급한다.
{
"principal": {
"id": "hong",
"username": "홍길동",
"role": [
{
"authority": "ROLE_USER"
}
]
},
"credentials": null,
"details": "홍길동",
"authenticated": true,
"name": "홍길동",
"authorities": [
{
"authority": "ROLE_USER"
}
]
}
*TeacherManager를 로그인 계정에 추가하기
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(studentManager);
auth.authenticationProvider(teacherManager);
}
인증매니저가 계정을 쌓아두고 있기 때문에, 여러개를 추가해도 된다. 검사 방식은 쌓아둔 계정을 비교해본다. 선생님 아이디로 로그인을 하면, StudentManager의 authenticate()를 갔다가 null을 반환하고, TeacherManager의 authenticate에서 계정을 확인하고 로그인 정보를 리턴한다.
* formLogin으로 UsernamePasswordAuthenticationFilter 필터를 사용하고 있는데, UsernamePasswordAuthenticationToken를 커스터마이징하고 싶다면?
UsernamePasswordAuthenticationFilter 을 상속해서 내부 구현을 바꿔주면, 인증방식을 커스터마이징 할 수 있다. attemptAuthentication이 토큰을 만드는 메서드이기 때문에 해당 메서드를 재정의해서 원하는대로 변경한다.
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 : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
로그인 시도 할 때, UsernamePasswordAuthenticationFilter가 인증을 거치는 과정 중에 핵심을 가져와서 인증토큰을 만드는 부분이다. (참고로 생성자에 AuthenticationManager를 추가해주는 것을 잊지 않는다.!)
@Override
protected void configure(HttpSecurity http) throws Exception {
CustomLoginFilter filter = new CustomLoginFilter(authenticationManager());
http
.authorizeRequests(request->
request
.antMatchers("/", "/login").permitAll()
.anyRequest().authenticated()
)
// .formLogin(
// login -> login.loginPage("/login")
// .permitAll()
// .defaultSuccessUrl("/", false)
// .failureUrl("/login-error")
// )
.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class)
.logout(logout -> logout.logoutSuccessUrl("/"))
.exceptionHandling(e -> e.accessDeniedPage("/access-denied"));
}
기존의 formLogin 대신에 커스터마이징한 filter를 통해서 로그인을 제어할 수 있다. addFilterAt()에 CustomLoginFilter를 이용한다. ( 구현 내용을 보면 UsernamePasswordAuthenticationToken 방식을 이용하는 것은 그대로 한다.) request에 "/" 뿐만 아니라, "/login"을 추가해서 루트 페이지와 로그인 페이지는 어떠한 권한 상태에서도 접속할 수 있게 해주어야 한다.
*선생님, 학생 각 토큰 인증방식을 사용하기(필터 커스터마이징)
UsernamePasswordAuthenticationToken의 토큰 인증을 쓰지 않고 다른 토큰 인증방식을 사용할 수 있다. attemptAuthentication 메서드를 커스터마이징 하면 된다. 각 StudentManager, TeacherManager가 허용하는 인증토큰을 검사하는 supports() 함수를 그대로 같이 변경하면 된다. (기존에는 return authentication == UsernamePasswordAuthenticationToken.class; 를 사용하고 있는 상태였다.)
@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");
System.out.println(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);
}
}
UsernamePasswordAuthenticationToken 실제 구현 내용을 이용해서 커스터마이징 한 메서드이다. type을 검사해서 teacher인 경우 TeacherAuthenticationToken을 만들고, 아닌 경우 StudentAuthenticationToken을 만든다.
* Student
@Override
public boolean supports(Class<?> authentication) {
return authentication == StudentAuthenticationToken.class;
}
StudentAuthenticationToken.class 의 인증토큰인 경우에 인증을 진행한다.
@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;
}
studentDB에 token의 credential 정보가 있는지 확인해서 있으면 StudentAuthenticationToken을 만들어 리턴한다.
*Teacher
@Override
public boolean supports(Class<?> authentication) {
return authentication == TeacherAuthenticationToken.class;
}
TeacherAuthenticationToken.class 의 인증토큰인 경우에 인증을 진행한다.
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
TeacherAuthenticationToken token = (TeacherAuthenticationToken) authentication;
if(teacherDB.containsKey(token.getCredentials())) {
Teacher teacher = teacherDB.get(token.getCredentials());
return TeacherAuthenticationToken.builder()
.principal(teacher)
.credentials(null)
.details(teacher.getUsername())
.authenticated(true)
.build();
}
return null;
}
teacherDB에 token의 credential 정보가 있는지 확인해서 있으면 TeacherAuthenticationToken을 만들어 리턴한다.
문제는 기존에 있는 인증토큰을 사용하지 않고 커스터마이징한 인증토큰을 사용할 때 리다이렉트 주소와 로그인 실패 주소를 사용할 수 없다는 것이다. formLogin을 사용하면 로그인 성공 시 리다이렉트를 해주는 기능을 이용할 수 있다.
http
.authorizeRequests(request->
request
.antMatchers("/", "/login").permitAll()
.anyRequest().authenticated()
)
.formLogin(
login -> login.loginPage("/login")
.permitAll()
.defaultSuccessUrl("/", false)
.failureUrl("/login-error")
)
.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class)
.logout(logout -> logout.logoutSuccessUrl("/"))
.exceptionHandling(e -> e.accessDeniedPage("/access-denied"));
'Spring > Spring Security' 카테고리의 다른 글
UserDetailsService (0) | 2021.10.11 |
---|---|
BaiscToken으로 웹/모바일 로그인 인증 구현하기 (0) | 2021.10.11 |
BasicAuthentication 인증 (0) | 2021.10.11 |
basic login 과정 (0) | 2021.10.08 |
간단한 로그인 페이지 만들기 (0) | 2021.10.07 |