회원 가입 기능을 만들경우 절대 입력한 문자열을 그대로 DB에 저장하면 안된다. 보안에 매우 취약하기 때문이다. 그렇기때문에 패스워드를 해싱 하여 저장해야하는데 BCrypt가 가장 많이쓰이는 해싱 방법이다. (참고로 해싱된 패스워드를 다시 encode할 수 있으면 안됨 그렇기 때문에 요즘 웹사이트에서 비밀번호 찾기를 할 경우 비밀번호를 알려주는 게 아니라 재 설정을 하게 하는 것 입니다.)
예시
@Bean
UserDetailsManager users(DataSource dataSource) {
UserDetails user = User.builder()
.username("user")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
.roles("USER", "ADMIN")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.createUser(user);
users.createUser(admin);
return users;
}
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http
.httpBasic(httpBasic -> httpBasic.disable())
.csrf(AbstractHttpConfigurer::disable) // post 방식으로 값을 전송할 때 token을 사용해야하는 보안 설정을 해제
.authorizeRequests(authorizeRequests ->
.requestMatchers("/board/**").hasRole(Role.USER.name())
.requestMatchers("/adminpage").hasRole(Role.ADMIN.name())
.requestMatchers("/swagger-ui/**", "/v3/apiXdocs/**", "/swagger-ui-jung.html", "/webjars/**").permitAll()
.requestMatchers("/users/signup").permitAll()
.requestMatchers("/signupform").permitAll()
.requestMatchers("/signinform").permitAll()
.requestMatchers("/").permitAll()
.anyRequest().authenticated()
)
.formLogin(
login ->
login.loginPage("/signinform")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/board/mainpage")
);
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/v3/api-docs/**");
}
}
비밀번호 암호화를 하기 위해 위와 같이 BCryptPasswordEncoder
를 빈에 등록해줍니다.
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/*service 구현*/
public Long signUp(UserSignUpRequest userSignUpRequest) throws Exception {
if(this.isEmailExist(userSignUpRequest.getEmail())) {
throw new Exception("Your Mail already Exist.");
}
Users users = userSignUpRequest.toEntity();
users.hashPassword(bCryptPasswordEncoder);
return usersRepository.save(users).getId();
}
/*USERS 메소드*/
public Users hashPassword(PasswordEncoder passwordEncoder) {
this.password = passwordEncoder.encode(this.password);
return this;
}
데이터 저장 시, 아래와 같이 비밀번호가 암호화가 된 것을 확인할 수 있습니다.
/login
주소 요청이 요면 낚아채서 로그인을 진행시킵니다.security contectHolder
)Authentication
타입객체)Authentication
안에 User
정보가 있어야함.User
오브젝트의 타입은 UserDetails
타입객체로 정해져있음.즉, Security Session => Authentication => UserDetails
package com.wanted.jungproject.config.userDetail.domain;
import com.wanted.jungproject.domain.user.domain.Users;
import lombok.AllArgsConstructor;
import lombok.Builder;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
@Builder
public class CustomUserDetails implements UserDetails {
private Users users; //콤포지션
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return users.getRole().getKey();
}
});
return collect;
}
public CustomUserDetails(Users users) {
this.users = users;
}
@Override
public String getPassword() {
return users.getPassword();
}
@Override
public String getUsername() {
return users.getName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
메소드들을 오버라이딩해줘야합니다. (로그인 유효기간, 휴면계정 설정 등 다양한 조건을 추가할 수 있습니다.) 그 중 특히 해당 Users의 권한을 리턴하는 getAuthorities를 신경써서 권한값을 반환할 수 있도록 수정해줍니다.
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return users.getRole().getKey();
}
});
return collect;
}
유저 역할
package com.wanted.jungproject.domain.user.domain;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반사용자"),
ADMIN("ADMIN_USER", "관리자");
private final String key;
private final String title;
}
package com.wanted.jungproject.config.userDetail.application;
import com.wanted.jungproject.config.userDetail.domain.CustomUserDetails;
import com.wanted.jungproject.domain.user.domain.Users;
import com.wanted.jungproject.domain.user.domain.UsersRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {
@Autowired
private final UsersRepository usersRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Users> users = usersRepository.findByEmail(username);
if (users.isPresent()) {
return new CustomUserDetails(users.get());
}
return null;
}
}
시큐리티 설정에서
loginProcessingUrl(“/login”)
을 걸어놨기 때문에/login
요청이 오면 자동으로UserDetailsService
타입으로 Ioc 되어 있는loadUserByUsername
함수가 실행
클라이언트로 전달받은 username을 기반으로 유저를 검색하고, UserDetails => Authentication => 시큐리티 Session으로 전달하여 로그인 확인 절차 진행