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);
}
}
'Web > Spring' 카테고리의 다른 글
[Login구현] 권한부여(Role) (0) | 2021.08.10 |
---|---|
[Login구현] 사용자 등록하기(Register) (0) | 2021.08.10 |
[MultipartFile] 첨부파일 다운로드 (0) | 2021.08.06 |
[MultipartFile] 첨부파일 업로드 (0) | 2021.08.06 |
[properties] server.port vs server.http.port (0) | 2021.08.06 |