강의를 보며 학습한 내용을 복습차 다시 정리 하였습니다. 프론트단 코드는 작성 제외 하였습니다. (
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
,Filter
UserDetailService
TokenUtils
JWTUtil
등등.. 다음 포스팅은 JWT를 이용한 기본적인 회원가입 로직을 복습 할 예정이다. ***