login을 위한 setting

  • pom.xml
<!--    security-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.thymeleaf.extras</groupId>
      <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    </dependency>

 

  • domain-User
@Getter
@Setter
@Entity
@Table(name = "user", indexes = {@Index(columnList = "username"), @Index(columnList = "name"),
    @Index(columnList = "role"), @Index(columnList = "enabled"), @Index(columnList = "removed")})
@NoArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class User implements UserDetails {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @EqualsAndHashCode.Include
  private Integer id;

  @Column(nullable = false, insertable = false, updatable = false, columnDefinition = "datetime default CURRENT_TIMESTAMP")
  @DateTimeFormat(pattern = DateUtil.PATTERN_YMDHM)
  private LocalDateTime createDate;   //생성일시

  @Size(min = 2, max = 32)
  @Column(unique = true, nullable = false, updatable = false)
  private String username;

  @Size(min = 2, max = 8)
  @Column(nullable = false)
  private String name;

  @Column(nullable = false)
  @JsonIgnore
  private String password;

  @Enumerated(EnumType.STRING)
  @Column(nullable = false, updatable = false, columnDefinition = "varchar(50)")
  private Role role;

  @JsonIgnore
  @DateTimeFormat(pattern = DateUtil.PATTERN_YMD)
  @Column(columnDefinition = "date default CURRENT_TIMESTAMP")
  private LocalDate lastPasswordChangeDate = LocalDate.now();

  // 화면에서 추가인지 수정인지 여부를 보여주기 위함
  @Transient
  @JsonIgnore
  private boolean saved = false;

  @PostLoad
  private void postLoad() {
    this.saved = true;
  }

  @DateTimeFormat(pattern = DateUtil.PATTERN_YMD)
  private LocalDate expireDate;   //계정 만료일(null은 만료없음)

  @JsonIgnore
  @Column(columnDefinition = "bit(1) default 1", nullable = false)
  private boolean enabled = true;

  @JsonIgnore
  @Column(columnDefinition = "bit(1) default 0", nullable = false)
  private boolean locked = false;

  @JsonIgnore
  @Column(columnDefinition = "bit(1) default 0", nullable = false, insertable = false, updatable = false)
  private boolean superUser = false;

  @JsonIgnore
  @Column(columnDefinition = "bit(1) default 0", nullable = false, insertable = false)
  private boolean removed = false;

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    List<GrantedAuthority> authorities = new ArrayList<>(0);
    authorities.add(new SimpleGrantedAuthority(role.name()));
    return authorities;
  }

  /**
   * 신규 사용자이거나 비밀번호 란에 비밀번호를 입력한 경우 비밀번호 유효성 검사 필요
   */
  public boolean hasPasswordChanged() {
    try {
      return !password.isEmpty() || id == null;
    } catch (Exception e) {

    }
    return false;
  }

  @JsonIgnore
  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @JsonIgnore
  @Override
  public boolean isAccountNonExpired() {
//    if (expireDate != null) {
//      return expireDate.isAfter(LocalDate.now());
//    }
    return true;
  }

  @JsonIgnore
  @Override
  public boolean isAccountNonLocked() {
    return !locked && !removed;
  }

  @JsonIgnore
  @Override
  public boolean isEnabled() {
    return enabled && !removed;
  }

//  @JsonIgnore
//  public boolean hasRole(Role role) {
//    return this.role.equals(role);
//  }
}

 

  • domain-Certification
@Getter
@Setter
@Entity
@Table(name = "certification", indexes = {@Index(columnList = "phone")})
@NoArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Certification {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @EqualsAndHashCode.Include
  private Integer id;

  @Size(min = 12, max = 13)
  @NotNull
  private String phone;

  @Size(min = 6, max = 6)
  @NotNull
  private String authNumber;

  @JsonIgnore
  @Column(columnDefinition = "bit(1) default 0", nullable = false)
  private boolean certified = false;

  @JsonFormat(pattern = DateUtil.PATTERN_YMDHM)
  @DateTimeFormat(pattern = DateUtil.PATTERN_YMD)
  private LocalDateTime expireDate = LocalDateTime.now().plusMinutes(3);   //계정 만료일(null은 만료없음)
}

 

  • enumerate
// RoleBase
@Getter
@AllArgsConstructor
public enum RoleBase {
  ADMINISTRATOR("관리자"),
  USER("사용자");

  private String text;
}

//Role
@Getter
@AllArgsConstructor
public enum Role {
  ROLE_ADMIN("관리자", RoleBase.ADMINISTRATOR),
  ROLE_USER("사용자", RoleBase.USER);

  private String text;
  private RoleBase roleBase;

  public static List<Role> findByRoleBase(RoleBase roleBase) {
    return Arrays.stream(Role.values()).filter(p -> p.getRoleBase().equals(roleBase))
        .collect(Collectors.toList());
  }

}

❗ role을 사용하는 이유는 admin을 위해서!! 권한을 부여해줄 예정 ❗

 

  • config-SecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  private static final int TOKEN_VALIDITY_TIME = 604800;

  @NonNull
  private final BCryptPasswordEncoder bCryptPasswordEncoder;
  @NonNull
  private final UserService userService;

  @Bean
  public FilterRegistrationBean<Filter> getSpringSecurityFilterChainBindedToError(
      @Qualifier("springSecurityFilterChain") Filter springSecurityFilterChain) {
    FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
    registration.setFilter(springSecurityFilterChain);
    registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
    return registration;
  }

  @Bean
  public AuthenticationFailureHandler customAuthenticationFailureHandler() {
    return new CustomAuthenticationHandler();
  }

  @Bean
  public AuthenticationSuccessHandler customAuthenticationSuccessHandler() {
    return new CustomAuthenticationHandler();
  }

  @Bean
  public LoginUrlAuthenticationEntryPoint ajaxAwareAuthenticationEntryPoint() {
    return new AjaxAwareAuthenticationEntryPoint(SpringSecurity.LOGIN_URL);
  }

  @Bean
  public AccessDeniedHandler customAccessDeniedHandler() {
    return new CustomAccessDeniedHandler();
  }

  /**
   * 유저 DB의 DataSource와 Query 및 Password Encoder 설정.
   */
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
  }

  /**
   * Spring Security에서 인증받지 않아도 되는 리소스 URL 패턴을 지정해 줍니다.
   */
  @Override
  public void configure(WebSecurity web) {
    web.ignoring().antMatchers("/static/**");
  }

  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
    final CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Collections.singletonList("*"));
    configuration.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH"));
    // setAllowCredentials(true) is important, otherwise:
    // The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
    configuration.setAllowCredentials(true);
    // setAllowedHeaders is important! Without it, OPTIONS preflight request
    // will fail with 403 Invalid CORS request
    configuration.setAllowedHeaders(Collections.singletonList("Authorization"));
    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
  }

  /**
   * Spring Security에 의해 인증받아야 할 URL 또는 패턴을 지정해 줍니다.
   */
  @Override
  protected void configure(HttpSecurity http) throws Exception {

    http.headers().frameOptions().sameOrigin()
        .and().csrf().disable().authorizeRequests()
        .antMatchers("/admin/**","/mypage/**","/comment/**").authenticated()
        .anyRequest().permitAll()
        .and().formLogin().loginPage(SpringSecurity.LOGIN_URL)
        .loginProcessingUrl(SpringSecurity.LOGIN_PROCESS_URL)
        .successHandler(customAuthenticationSuccessHandler())
        .failureHandler(customAuthenticationFailureHandler())
        .usernameParameter(SpringSecurity.PARAM_USERNAME)
        .passwordParameter(SpringSecurity.PARAM_PASSWORD)
        .and().logout()
        .logoutRequestMatcher(new AntPathRequestMatcher(SpringSecurity.LOGOUT_URL))
        .logoutSuccessUrl(SpringSecurity.LOGIN_URL)
        .and().exceptionHandling().authenticationEntryPoint(ajaxAwareAuthenticationEntryPoint())
        .accessDeniedHandler(customAccessDeniedHandler())
        .and().rememberMe().disable();
  }
}

 

  • config-MvcConfig
  @Bean(name = "bCryptPasswordEncoder")
  public BCryptPasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

❗ MvcConfig에 @Bean으로 추가를 해야 사용이 가능하다!! ❗

 

  • config-PasswordConfig
@Getter
@Setter
@Configuration
@ConfigurationProperties("password")
@PropertySources({@PropertySource(value = "classpath:/config/password.properties")})
public class PasswordConfig {

  private Validation validation;

  @Getter
  @Setter
  @Configuration
  @ConfigurationProperties("herbnet.password-validation")
  public static class Validation {

    private int length;
    private boolean specialCharacters;
    private boolean upperCases;
    private boolean numbers;
    private int maxAttemptsCount;
    private int changeCycle;
  }

}

❗ password.properties는 취향 것.. ❗

알아서 설정

  • config-기타
//AjaxAwareAuthenticationEntryPoint
public class AjaxAwareAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {

  public AjaxAwareAuthenticationEntryPoint(String loginUrl) {
    super(loginUrl);
  }

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException authException) throws IOException, ServletException {
    String ajaxHeader = request.getHeader("X-Requested-With");
    boolean isAjax = "XMLHttpRequest".equals(ajaxHeader);

    if (isAjax) {
      response
          .sendError(HttpServletResponse.SC_UNAUTHORIZED, "Ajax Request Denied (Session Expired)");
    } else {
      super.commence(request, response, authException);
    }
  }
}

//CustomAuthenticationHandler
public class CustomAuthenticationHandler
    implements AuthenticationSuccessHandler, AuthenticationFailureHandler {

  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication auth) throws IOException {

    response.sendRedirect(request.getContextPath() + SpringSecurity.LOGIN_SUCCESS_URL);
  }

  @Override
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException exception) throws IOException, ServletException {

    String username = request.getParameter("username");
    String dispatcherURL = SpringSecurity.LOGIN_URL;

    request.setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
    request.setAttribute("SPRING_SECURITY_LAST_USERNAME", username);
    request.setAttribute("username", username);
    request.setAttribute("error", getErrorMessage(exception));
    request.getRequestDispatcher(dispatcherURL).forward(request, response);
  }

  /**
   * Spring Security 가 반환하는 에러 메시지 얻어옴
   *
   * @return 에러메시지
   */
  private String getErrorMessage(AuthenticationException exception) {

    String error;
    if (exception instanceof AccountExpiredException) {
      error = "사용 기간이 만료된 계정입니다.";
    } else if (exception instanceof BadCredentialsException) {
      error = "아이디 또는 비밀번호를 확인해주시기 바랍니다.";
    } else if (exception instanceof LockedException) {
      error = "로그인 반복 실패 또는 관리자에 의해 잠겨있는 계정입니다.\n관리자에게 문의하세요.";
    } else if (exception instanceof InternalAuthenticationServiceException) {
      error = "존재하지 않는 계정입니다.";
    } else if (exception instanceof DisabledException) {
      error = "승인되지 않은 계정입니다. 관리자의 승인을 기다려주세요.";
    } else if (exception instanceof CredentialsExpiredException) {
      error = "만료된 비밀번호 입니다.";
    } else {
      error = exception.getLocalizedMessage();
    }

    return error;
  }
}

//CustomAccessDeniedHandler
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

  @Override

  public void handle(HttpServletRequest request, HttpServletResponse response,
      AccessDeniedException accessDeniedException) throws IOException {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    if (authentication instanceof AnonymousAuthenticationToken) {
      response.sendRedirect(request.getContextPath() + SpringSecurity.LOGIN_URL);
    }
  }
}

 

  • util-ApplicationContextProvider
public class ApplicationContextProvider implements ApplicationContextAware {

  private static ApplicationContext applicationContext;

  public static <T> T getEnvironmentProperty(String key, Class<T> targetClass, T defaultValue) {
    if (key == null || targetClass == null) {
      throw new NullPointerException();
    }

    T value = null;
    if (applicationContext != null) {
      value = applicationContext.getEnvironment().getProperty(key, targetClass, defaultValue);
    }
    return value;
  }

  public void setApplicationContext(@NonNull ApplicationContext applicationContext) {
    ApplicationContextProvider.applicationContext = applicationContext;
  }
}

 

  • util-LoginAuth
public class LoginAuth {

  /* 계정 정보 */
  public static final String PASSWORD_SPECIAL_REGEXP = "(?=.*[!@#$%^&*()\\-_=+\\\\\\|\\[\\]{};:\\'\",.<>\\/?])";
  public static final String PASSWORD_UPPER_REGEXP = "(?=.*[A-Z])";
  public static final String PASSWORD_NUMBER_REGEXP = "(?=.*\\d)";
}

 

  • util-PasswordUtil
@UtilityClass
public class PasswordUtil {

  public static boolean isValidPassword(String password) {
    int length = ApplicationContextProvider
        .getEnvironmentProperty("validation.length", Integer.class, 0);
    boolean specialCharacters = ApplicationContextProvider
        .getEnvironmentProperty("validation.special-characters", Boolean.class,
            false);
    boolean upperCases = ApplicationContextProvider
        .getEnvironmentProperty("validation.upper-cases", Boolean.class, false);
    boolean numbers = ApplicationContextProvider
        .getEnvironmentProperty("validation.numbers", Boolean.class, false);

    String baseRegExp = "";
    String specialRegExp = "";
    String upperRegExp = "";
    String numberRegExp = "";

    if (specialCharacters) {
      specialRegExp = LoginAuth.PASSWORD_SPECIAL_REGEXP;
    }
    if (upperCases) {
      upperRegExp = LoginAuth.PASSWORD_UPPER_REGEXP;
    }
    if (numbers) {
      numberRegExp = LoginAuth.PASSWORD_NUMBER_REGEXP;
    }

    baseRegExp =
        "^" + specialRegExp + upperRegExp + numberRegExp
            + "[A-Za-z\\d!@#$%^&*()\\-_=+\\\\\\|\\[\\]{};:\\'\",.<>\\/?]{" + length + ",}";

    if (!StringUtils.isBlank(password) && password.matches(baseRegExp)) {
      return true;
    }
    return false;
  }

  public static String getPasswordRuleMessage() {
    String message = "비밀번호는 ";
    List<String> ruleIncluded = new ArrayList<>();

    int length = ApplicationContextProvider
        .getEnvironmentProperty("validation.length", Integer.class, 0);
    boolean specialCharacters = ApplicationContextProvider
        .getEnvironmentProperty("validation.special-characters", Boolean.class,
            false);
    boolean upperCases = ApplicationContextProvider
        .getEnvironmentProperty("validation.upper-cases", Boolean.class, false);
    boolean numbers = ApplicationContextProvider
        .getEnvironmentProperty("validation.numbers", Boolean.class, false);

    ruleIncluded.add("영문 소문자");

    if (specialCharacters) {
      ruleIncluded.add("특수문자");
    }

    if (upperCases) {
      ruleIncluded.add("영문 대문자");
    }

    if (numbers) {
      ruleIncluded.add("숫자");
    }

    if (!ruleIncluded.isEmpty()) {
      message += String.format("%s를 포함하여 ", StringUtils.join(ruleIncluded, ", "));
    }

    message += String.format("%d자리 이상으로 구성되어야 합니다.", length);

    return message;
  }
}

 

  • util-SpringSecurity
    public class SpringSecurity {
      public static final String LOGIN_URL = "/auth/login";
      public static final String LOGIN_PROCESS_URL = "/auth/login/process";
      public static final String LOGIN_SUCCESS_URL = "/";
      public static final String LOGOUT_URL = "/auth/logout";
      public static final String PARAM_USERNAME = "username";
      public static final String PARAM_PASSWORD = "password";
      public static final String PARAM_REMEMBER = "remember-me";
      public static final String KEY_REMEMBER = "알아서";
    }​

 

  • exception-UserPasswordValidationException
@SuppressWarnings("serial")
public class UserPasswordValidationException extends Exception {

  @Override
  public String getMessage() {
    return "비밀번호 규칙이 일치하지 않습니다";
  }
}

 

  • exception-DataNotFoundException
@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class DataNotFoundException extends HttpClientErrorException {

  public DataNotFoundException() {
    super(HttpStatus.NOT_FOUND, "데이터를 찾을 수 없습니다.");
  }

  public DataNotFoundException(String message) {
    super(HttpStatus.NOT_FOUND, message);
  }
}

 

 

+ Recent posts