지난 글에서는 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;
}

} 도메인단은 회원가입에 정리해놓았으므로 생략하였다.

전체 스프링 시큐리티 로그인 구현 코드는 다음 깃허브 브랜치에 있다❕