지난 글에서는 Spring Security의 흐름을 이해하기 위해 각 인터페이스를 알아보았다. 이번 글에서는 코드를 기능 단위(회원 가입/로그인/권한 처리)로 정리해보았다.
회원 가입 회원가입은 단순 저장이기에, 로그인 인증처럼 시큐리티 필터를 사용하지 않는다. 단, 도메인단만 시큐리티에서 사용하는 UserDetails를 상속받아 구현하였다. 이 프로젝트에서는 User, Admin 두 가지 권한 중 하나를 선택해서 가입할 수 있다. 비밀번호는 BCryptPasswordEncoder을 통해 암호화되어 저장된다.
도메인단 : User, UserRole 도메인단은 UserDetails를 상속받아 구현하였다. // User : 유저엔티티 @Table(name = “user”) public class User implements UserDetails {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String name;
private String password;
@Column
@Enumerated(EnumType.STRING)
private UserRole userRole;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> roles = new HashSet<>();
roles.add(new SimpleGrantedAuthority(userRole.getValue()));
return roles;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.name;
}
@Override // 만료 계정인지
public boolean isAccountNonExpired() {
return true;
}
@Override // 잠긴 계정인지
public boolean isAccountNonLocked() {
return true;
}
@Override // 비밀번호 만료되었는지
public boolean isCredentialsNonExpired() {
return true;
}
@Override // 계정 활성화 여부
public boolean isEnabled() {
return true;
}
}
// UserRole : 계정 권한을 enum타입으로 설정 public enum UserRole { USER(“ROLE_USER”), ADMIN(“ROLE_ADMIN”);
private String value;
} 서비스단 : UserService 회원가입 로직은 단순 저장하는 역할이다. 즉, 서비스단에서 시큐리티를 사용할 필요가 없다. 따로 UserDetailsService 를 상속받아 구현하지 않았다. public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public User createUser(UserJoinRequest userJoinRequest) {
User user = userRepository.save(
User.builder()
.name(userJoinRequest.getName())
.userRole(userJoinRequest.getUserRole())
.password(bCryptPasswordEncoder.encode(userJoinRequest.getPassword())).build());
return user;
}
} 레포지토리단 : UserRepository 회원가입 로직에서는 레포지토리단 또한 단순 저장하는 역할이다. 이 또한 시큐리티를 따로 사용하지 않았다. @Repository public interface UserRepository extends JpaRepository<User, Integer> { public User findByName(String name); } 컨트롤러단 : UserController /join은 컨트롤러에만 있고 따로 SpringSecurityConfig에 등록하지 않았다. @RestController @RequiredArgsConstructor public class UserController {
private final UserService usersService;
@PostMapping("/join")
public void createUser(UserJoinRequest userJoinRequest, HttpServletResponse response) throws IOException {
usersService.createUser(userJoinRequest);
response.sendRedirect("/login");
}
}
로그인/로그아웃 스프링 시큐리티는 필터체인이라고 할 수 있다. Config 파일을 통해서 해당 어플리케이션의 모든 요청을 낚아채서 먼저 로그인하게 만들었다. 실패 시 / 성공 시 각자 계정 권한마다 화면이 다르다.
SpringSecurityConfig : 필터 체인(SpringSecurityFilterChain)에 등록, 페이지 별 접근 권한 설정, 로그인 / 로그아웃 url 매핑 등의 설정을 해주었다. Manager에 Provider 등록해주었다. @EnableWebSecurity // SpringSecurityFilterChain 에 등록 @RequiredArgsConstructor public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
// 인증에서 제외
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/script/**", "/image/**", "/fonts/**", "lib/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/login", "/join").permitAll() // 모두 접근 가능
.antMatchers("/admin").hasRole("ADMIN") // ADMIN 만 접근 가능
.antMatchers("/main").authenticated() // 인증해야 접근 가능
/* 로그인 폼 */
.and().formLogin()
.loginPage("/login")
.usernameParameter("name")
.passwordParameter("password")
.defaultSuccessUrl("/main")
.failureUrl("/fail")
.permitAll()
/* 로그아웃 */
.and().logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
.permitAll();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// Manager 에 Provider 등록
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider());
}
// Provider 생성
@Bean
public CustomAuthenticationProvider customAuthenticationProvider() {
return new CustomAuthenticationProvider(userDetailsService, bCryptPasswordEncoder());
}
} HomeController @Controller @RequiredArgsConstructor public class HomeController {
private final UserRepository userRepository;
@GetMapping(value = {"/", "/login"})
String login() {
return "login";
}
@GetMapping("/fail")
String fail() {
return "fail";
}
@GetMapping("/join")
String join() {
return "join";
}
@GetMapping("/admin")
ModelAndView adminModel(Authentication authentication) {
User user = Optional.ofNullable(userRepository.findByName(authentication.getName()))
.map(u -> User.builder().name(u.getName()).password(u.getPassword()).userRole(u.getUserRole()).build())
.orElseThrow(IllegalArgumentException::new);
ModelAndView modelAndView = new ModelAndView("/admin");
modelAndView.addObject("user", user);
return modelAndView;
}
@GetMapping(value ="/main")
ModelAndView mainModel(Authentication authentication) {
User user = Optional.ofNullable(userRepository.findByName(authentication.getName()))
.map(u -> User.builder().name(u.getName()).password(u.getPassword()).userRole(u.getUserRole()).build())
.orElseThrow(IllegalArgumentException::new);
ModelAndView modelAndView = new ModelAndView("/main");
modelAndView.addObject("user", user);
return modelAndView;
}
}
CustomAuthenticationProvider : AuthenticationProvider을 상속받아 구현 authenticate()을 통해 입력받은 Authentication와 DB의 User의 아이디/비번이 같은지 검증한다. @RequiredArgsConstructor public class CustomAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder passwordEncoder;
// 인증 메소드
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = (String) authentication.getCredentials();
User user = (User) userDetailsService.loadUserByUsername(name);
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException(user.getUsername() + "비밀번호를 다시 입력해주세요.");
}
return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
} CustomUserDetailsService : UserDetailsService 을 상속받아 구현 loadUserByUsername()을 통해 DB의 User을 가져온다, public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String name) {
User user = userRepository.findByName(name);
if (user == null) {
throw new UsernameNotFoundException("Can't find user");
}
return user;
}
} 도메인단은 회원가입에 정리해놓았으므로 생략하였다.
전체 스프링 시큐리티 로그인 구현 코드는 다음 깃허브 브랜치에 있다❕