강의를 보며 학습한 내용을 복습차 다시 정리 하였습니다. 프론트단 코드는 작성 제외 하였습니다. (
mustache)
학습 목표
MariaDB, JPA 를 이용해 DB단 연결
Mustache를 이용해 프론트 SSL로 프론트단 간단히 구현
SpringSecurity Session (FormLogin) 방식으로 로그인,로그아웃, 중복 로그인 방지 구현,crsf 토큰
@Configuration
@EnableWebSecurity
public class SecurityConfig{
@Bean
BCyrptPasswordEncoder bCryptPasswordEncoder(){
reutrn new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) thorws Exception{
http
.authorizeHttpRequest((auth)-> auth
.requestMatchers("/login","/join","joinProc").permitAll()
.requestMathcers("/").hasAnyRole("USER")
.requestMatchers("/my/**").hasAnyRole("ADMIN","USER")
.anyRequest().authenticated();
);
http
.formLogin((auth)-> auth.loginPage("/login")
.lgoinProcessingUrl("/loginProc")
.permitAll()
);
http
.crsf((auth)->auth.disalbe());
return http.build();
}
}
SecurityFilterChain은 인증, 인가 과정을 담당하는 클래스이다. 간략히 말하면SpringSecurity가 제공하는 필터들의 모음을 어떻게 작동할지 설정하는 부분이다. 아래는 요청이FilterChain에 도달하는 대강의 모식도이다.
SecurityFilterChain메서드를 정의하고BCyrptPasswordEncoder는 비밀번호를 암호화 하는데 사용하는 메서드이다. 여담으로 메서드체이닝으로 한 번에 http를
Http 요청 -> WebApplication Server ->필터1 -> 필터2 -> Servlet -> 컨트롤러

@Controller
public class AdminController(){
@GetMapping("/admin")
public String adminP(){
return "admin"
}
}
@Controller
public class MainController{
@GetMapping("/")
public String mainP(){
return "main"
}
}
메인 하고 어드민은 바디로 받지 않고 간단히 뷰에 해당 페이지 구분만 해준다
public interface UserRepository extends JPARepository<UserEntity,Integer>
JPA 레퍼지토리를 상속 받는데 첫 번째 파라메터는 받을 객체, 두 번쨰 파라메터는 구분할 아이디의 변수타입명을 참조형으로 넣는다
@Data
@Entity
public class UserEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String role;
}
JoinService
@Service
@AllArgsConsturctor
pbulic class JoinService{
private final UserRepository userRepository;
private final BCryptPasswordEncoder bcyrptPasswordEncoder;
public void joinProcess(JoinDto joinDto){
UserEntity user = new UserEntity;
user.setUsername(joinDto.getUsername());
user.setPassword(bCryptPasswordEncoder.ecode(joinDto.getPasword()));
user.setRole("ADMIN");
userRepository.save(user);
}
}
JointDTO 를 통해 username, password 를 get 한뒤 UserEntity에 set 해서 repository에 save 해준다.
JoinDTO
@Data
public class JoinDto{
private String username;
private String passwrod;
}
JoinController
@Controller public class JoinController{
@GetMapping("/join")
public String joinP(){
return "joinP"
}
@PostMapping("/join")
public String joinProcess(JoinDto joinDto){
joinservice.joinProcess(joinDto);
return "redirect:/login";
}
}
UserEntity
@Entity
public class UserEntity{
private Integer id;
private String password;
@Column(unique=true)
private String name;
private String role;
}
UserEntity 내에
@Column(unique=true)속성 추가
UserRepository
public interface UserRepository extends JPARepository<UserEntity, Integer>{
boolean existsByUsername(String username);
UserEntity findByUserName(String username);
}
JoinService
@Service
@AllArgsConsturctor
pbulic class JoinService{
private final UserRepository userRepository;
private final BCryptPasswordEncoder bcyrptPasswordEncoder;
public void joinProcess(JoinDto joinDto){
boolean isUser= userRepository.findByUsername(joinDto.getUsername())
if(isUser){ retunrn;}
UserEntity user = new UserEntity;
user.setUsername(joinDto.getUsername());
user.setPassword(bCryptPasswordEncoder.ecode(joinDto.getPasword()));
user.setRole("ADMIN");
userRepository.save(user);
}
}
isUser를 통해 중복 검사 로직joinService에 추가
CustomerUserDetailService
@Service
@RequiredArgsConstructor
pulbic CustomerUserDetailService implements UserDetailService{
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity= userRepository.findByusername(username);
if(userEntity!=null){
return new CustomerUserDetails(userdata);
}
return null;
}
}
UserDetailService는 DB에서 사용자의 정보를 가져오는 역할을 한다. 보통은 UserDetails로 반환값을 갖는다. 사용하려면loadByUsername메서드를 오버라이드 해서 사용해야 하며 이 메서드를 사용하기 위해 레퍼지토리를 주입 받고 미리 구현해놓은findByUsername을 통해서userEntity를 찾은 뒤에UserDetails로 반환할 때UserEntity를 넣으면 된다.
UserDetail
해당 인터페이스는 사용자의 세부 정보를 제공하는데 권한, 아이디, 비밀번호, 유저 잠금상태, 만료, 사용가능 여부 등 아주 다양한 속성을 담는다. 일단 오버라이드 후 안에 로직들을 천천히 채워나가면 된다.
public class CustomUserDetails implements UserDetails {
private UserEntity userEntity;
public CustomUserDetails(UserEntity userEntity) {
this.userEntity = userEntity;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return userEntity.getRole();
}
});
return collection;
}
@Override
public String getPassword() {
return userEntity.getPassword();
}
@Override
public String getUsername() {
return userEntity.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
권한 부여 부분외 부분을 유의하고 나머지는 유저 엔티티 주입 후 아이디와 패스워드를 get 하고 그 외 속성들은 전부 true로 설정 해준다.
@Controller
public class MainController{
public String mainP(Model model){
String id = SecurityContextHolader.getContext().getAuthentication().getName();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrandAuthority> iter = authorities.iterator();
GrandAuthority auth = iter.next();
String role = auth.getAuthoritiy();
model.addAttribute("id", id);
model.addAttribute("role",role);
return "main";
}
}
getAuthentication()현재 인증 객체를 가져온다
getAuthorities()인증 객체에서 권한 정보 컬렉션을 가져온다
iterator()반복자
next()반복자를 이용한 첫 번째 권한 정보 객체를 가져온다(여러 권한이 있는 경우를 고려하지 않음)
getAuthority()권한 정보 객체 이름을 가져온다.
SecurityContextHolder에서 사용자의 이름을 얻고,getAuthentication()한 후에 권한을 컬렉션 객체로 가져오는데 여기서 컬렉션으로 가져오는 이유는 객체가 여러 권한을 가지고 있다고 상정하기 때문이다. (권장되는 방식) 예제이므로 두 번째 권한은 표시하지 않는다고 상정한 후iterater한후next()로 첫 번째 권한만 보여준다
SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig{
@Bean
BCyrptPasswordEncoder bCryptPasswordEncoder(){
reutrn new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) thorws Exception{
http
.authorizeHttpRequest((auth)-> auth
.requestMatchers("/login","/join","joinProc").permitAll()
.requestMathcers("/").hasAnyRole("USER")
.requestMatchers("/my/**").hasAnyRole("ADMIN","USER")
.anyRequest().authenticated();
);
http
.formLogin((auth)-> auth.loginPage("/login")
.lgoinProcessingUrl("/loginProc")
.permitAll()
);
// http.crsf((auth)->auth.disalbe())
// 주석 처리를 하게 되면 enable 하게 되어 로그인시 토큰을 전달 해주어야 한다.
.sessionManagement(
(auth) -> auth
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).sessionFixation((sessionFixation)->sessionFixation.newSession())
.maximumSession(1)
.maxSessionPreventsLogin(true)
)
http
.logout((auth)->auth.logoutUrl("/logout")
.logoutSuccessUrl("/"));
return http.build();
}
}
sessionManagement스프링 시큐리티에서 세션 관리 메서드
세션 생성 정책
sessionCreationPolicy세션 생성 정책
SessionCreationPolicy.If_REQUIRED필요한 경우에만 세션을 생성
세션 고정 방지
sessionFixation세션공격 방지 설정 메서드
newSession로그인 시 마다 새로운 세션 생성
최대 세션 수
maximumSession사용자별 최대 세션 수
중복 로그인
maxSessionPreventLogin메서드는 최대 세션 수 초과시 로그인 차단 여부 설정true로그인 차단false세션 중 하나를 삭제 후 로그인 허용
sessionManagement를 통해서 로그인시 매번 새 세션을 발급 받도록 하고 세션은 최대 1개로 정해 중복로그인 방지를 한다. 로그아웃 URL 을 추가했다. crsf 를enable했으니mustache프론트 단에서
<input type="hidden name="_crsf" value="{{_crsf.token}}" />
위 해당 코드를 추가 해줘야 한다. ( 폼 로그인 코드 안 )
LogoutController
@Controller
public class LogOutController{
@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) throws IOEException{
Authentication authentication = SecurityContextHolder().getContext().getAuthentication()
if(authentication != null){
new SecurityContextLogoutHandler().logout(request,response,authentication);
}
return "redirect:/";
}
}
여기까지 구현 완료. 아래 부터는 부수적인 설명
권장되지 않는 방식(DB와 연결되지 않아도 작동하며 주로 작은 프로젝트에서 쓰인다)
@Bean
public UserDetailService userDetailService(){
UserDetail user1 = User.builder()
.username("user1")
.password(bCryptPasswordEncoder().encode("1234"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailManager(user1);
}
builder()패턴 방식으로UserDetail타입 객체를 직접buidl()하고InMemoryUserDetailManager()에 user을 직접 담아서return한다.`
SecurityConfig
@Bean
public RoleHierarchy roleHierarchy(){
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_MANAGER\n" + "ROLE_MANAGER > ROLE_USER");
return hierarchy;
}
가장 높은 계층 권한을 가진 ADMIN은 MANAGER USER 계층 권한에 자연스레 접근 할 수 있다.
RoleHierarchyImpl 은 RoleHierarchy 인터페이스의 구현체이다.http
.authorizeHttpRequest(
(auth)-> auth
.requestMatchers("/login","/join").permitAll()
.requestMatchers("/").hasAnyRole("USER");
.reqestMatchers("/manager").hasAnyRole("MANAGER");
.requestMatcher("/admin").hasAnyRole("ADMIN")
.anyRequest().authenticated()
)
계층 권한을 구현 해놓으면
hasAnyRole()부분에 구구절절 권한 목록들을 나열하지 않아도 해당 계층의 위 계층들은 자연스레 접근이 가능 해진다. 구현 해야할 코드가 매우 간단해진다.
sessionstorage를 구현할 코드는 비교적 간단하지만 JWT 토큰을 쓰게 되면 직접 구현해야될 부분들이 더 많아진다. 가령AutehnticationProvider,FilterUserDetailServiceTokenUtilsJWTUtil등등.. 다음 포스팅은 JWT를 이용한 기본적인 회원가입 로직을 복습 할 예정이다. ***