[실습] 스프링시큐리티 로그인처리
목차
- [이론] 스프링 시큐리티 1
- [이론] 스프링 시큐리티2
- [실습] 스프링 시큐리티 Form Login
- [추가] CustomAuthenticationProvider vs DaoAuthenticationProvider
- [이론] 스프링 시큐리티3
- [이론] 스프링 시큐리티4
- [추가] AuthorizeReqeusts vs AuthorizeHttpRequests
- [실습] 스프링 시큐리티 Json data Login 처리
- [실습] 스프링 시큐리티 JWT 설정
- [실습] 스프링 시큐리티 JWT 처리
- OAuth2tory.com/entry/%EC%8B%A4%EC%8A%B5-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-JWT-%EC%84%A4%EC%A0%95)
기본적으로 form login을 사용하여 로그인을 진행을 할 것이다
http.authorizeHttpRequests()
부분은 인증에 관한 것인데 일단은 loginhome페이지로 가는 것만 권한을 가져야 한다고 알고 있다
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeHttpRequests()
.mvcMatchers("/loginhome").authenticated()
.anyRequest().permitAll();
http.formLogin()
.loginPage("/login")
/*.successHandler((request, response, authentication) -> {
response.sendRedirect("/loginhome");
})*/
.defaultSuccessUrl("/loginhome");
return http.build();
}
}
User
@Entity
@Getter
@NoArgsConstructor
@Table(name = "USERS")
public class User {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
private String email;
private String name;
private String password;
public User(String email, String name, String password) {
this.email = email;
this.name = name;
this.password = password;
}
}
UserDetails
@RequiredArgsConstructor
public class CustomUser implements UserDetails {
private final User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> "ROLE_USER");
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getName();
}
//생략
}
PasswordEncoder
기본적으로 제공되는 BCryptPasswordEncoder를 사용할 것이다
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
UserDetailsService
앞서 이론 부분을 보고 충분히 이해했다면 쉽게 해당 코드를 이해할 것 이다
loadUserByUsername
에서 email을 통해 user를 가져온 이유는 로그인시 email과 password를 통해 진행을 할 예정이기 때문이다
@RequiredArgsConstructor
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findUserByEmail(username);
return new CustomUser(user);
}
public void saveUSer(SignUpRequest request) {
User user = request.toEntity(passwordEncoder);
userRepository.save(user);
}
}
AuthenticationProvider
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final UserService userService;
private final PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getPrincipal().toString();
UserDetails user = userService.loadUserByUsername(username);
if (passwordEncoder.matches(password, user.getPassword())) {
return new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword(),
user.getAuthorities()
);
}
throw new BadCredentialsException("BAD Credentials");
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(UsernamePasswordAuthenticationToken.class);
}
}
Provider에서 왜 UsernamePasswordAuthenticationToken를 생성하는 지는 뒷부분에 나오니 일단은 넘어가자
여기까지 진행을 하면 모든 준비는 완료되었고 이제 빈을 등록하고 조합을 해주기만 하면된다
물론 repository, controller, view는 직접 만들어야 하지만 현재는 Security에 초점을 맞추고 있으므로 해당 글에선 제외를 하겠다
Spring을 사용해봤다면 그렇게 어려운 부분은 아니기 때문에 충분히 직접 작성할 수 있을 것이다
BeanConfig
@Configuration
@RequiredArgsConstructor
public class ProjectBeanConfig {
private final UserRepository userRepository;
@Bean
public AuthenticationProvider authenticationProvider(){
return new CustomAuthenticationProvider(userService(), passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public UserService userService() {
return new UserService(userRepository, passwordEncoder());
}
}
앞서 만들어둔 PasswordEncoder, UserService, 그리고 해당 두개를 가진 Provider를 등록을 하였다
다른데서 찾아보면 AuthenticationManagerBuilder에 Provider를 등록을 해주는 과정을 설명해주는 곳도 있던데 이론 스프링 시큐리티2이부분을 보게되면 굳이 하나의 Provider를 사용할 때는 등록을 해주지 않아도 된다는 것을 알 수 있다
추가 Spring Security Login 과정
Spring Security는 기본적으로 FilterChain이라는 것을 통해서 인증, 인가에 대한 로직을 처리한다는 것만 우선 알고있자
filter를 모르면 Spring filter에 대해서 선행으로 알고오자
filter목록을 보고 싶다면 설정파일 윗 상단에
@EnableWebSecurity(debug = true)
를 작성을 하고 페이지를 로드하면 목록을 볼 수 있다
이러한 filter중에서는 앞서 설정파일에서 .formLogin()
을 주었기 때문에 추가된 UsernamePasswordAuthenticationFilter
가 있다
UsernamePasswordAuthenticationFilter
이 호출되면 되어 doFilter()가 실행이 되게 된다 더 정확히는 상속하고 있는 AbstractAuthenticationProcessingFilter
의 doFilter()이다
4개의 붉은 사각형을 순차적으로 보게되면
- 인증의 필요성을 확인
- 인증
- 후처리
- 성공시 SecurityContext에 객체 담고, successHandler 호출
- 실패시 failureHandler 호출
이와 같은 순서를 따라 처리가 된다
해당코드를 차래대로 확인해보자
인증 필요성 확인
여기서 requiresAuthenticationRequestMatcher
는 UsernamePasswordAuthenticationFilter
에서new AntPathRequestMatcher("/login", "POST")
로 정의되어있다
/login에 대해서 POST요청이면 filter를 적용한다는 것이다
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (this.requiresAuthenticationRequestMatcher.matches(request)) {
return true;
}
if (this.logger.isTraceEnabled()) {
this.logger
.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
}
return false;
}
인증
attemptAuthentication는 확인해보면 추상메소드로 이 또한 UsernamePasswordAuthenticationFilter
에서 정의된다
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
위코드를 요약하자면
- /login Post로 온 요청에 대해서
- request.getParameter를 통해 username, password 추출
- 위 정보를 바탕으로 UsernamePasswordAuthenticationToken 생성
- AuthenticationManager를 통해 인증 후 authentication객체 반환
1,2,3번의 과정은 어려운 것이 없고 4번 manager호출에 대해서 디버깅을 통해 확인을 해보면 ProviderManager가 호출이 되는 것을 알 수 있다
ProviderManager에 관한 것은 이론2에서 정리를 해뒀기 때문에 궁금하면 보면된다
앞서 CustomAuthenticationProvider
를 작성할때 UsernamePasswordAuthenticationToken을 사용하는 것에 대해서 다시 말해보면 ProviderManager에서 파라미터로 넘어온 Authentication객체에 대해서 인증이 가능한 Provider인지를 확인하기 위해서 support에서 확인하고 authenticate에서 사용을 한 것이다
로그인과정 요약
- /login Post로 들어온지 확인
- AbstractAuthenticationProcessingFilter.doFilter 실행, 내부에서 UsernamePasswordAuthenticationFilter.attemptAuthentication 호출
- username과 password 꺼내서 Token 생성
- ProviderManager는 여러 Provider중에서 해당 Token을 검증 가능한 Provider를 호출하여 인증 진행
- UserDetailsService.loadUserByUsername, PasswordEncoder.matches를 이용해 사용자 인증
- 최종적으로 Authenticaiton객체로 변환하여 반환 이때 Principal은 Username, Credentials는 Password이다
추가
현재 글에서는 AuthenticationProvider를 직접 커스텀하여 구현했지만 Spring Security에는 DaoAuthenticatinoProvider를 제공해준다
Bean등록시 다음과 같이 CustomProvider대신 Provider를 등록하여 사용해주면 된다
@Bean
public AuthenticationProvider authenticationProvider(){
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(userService());
return provider;
}
실행결과
코드
[GitHub - Juhongseok/SpringSecurityLogin: 스프링 시큐리티를 이용한 로그인 프로젝트
스프링 시큐리티를 이용한 로그인 프로젝트. Contribute to Juhongseok/SpringSecurityLogin development by creating an account on GitHub.
github.com](https://github.com/Juhongseok/SpringSecurityLogin)