Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

JAN's History

Spring Security Session 권한처리 본문

JWT

Spring Security Session 권한처리

JANNNNNN 2024. 6. 14. 19:33

  1. Http Request로 username과 password를 통한 post요청이 옵니다.
  2. AuthenicationFliter가 UsernamePasswordAuthentication에게 이 요청을 전달하면, HttpServlet에서 username과 password를 추출해서 UsernamePasswordAuthenticationToken 객체를 생성합니다. 
  3. UsernamePasswordAuthenticationToken 은 AuthenticationManager에게 넘겨지고, AuthenticationManager는 해당 UsernamePasswordAuthenticationToken 을 인증할 수 있는 AuthenticationProvider을 찾아서 인증을 진행합니다.
    • 이때 인증 과정이 실패하면 SecurityContextHolder를 비우고, remember-me가 설정되어 있다면, RememberMeService.loginFail을 실행합니다. 마지막으로 AuthenticationFailureHandler를 실행시킵니다.
  4. AuthenticationProvider는 인증과정에 성공하게 되면 Session 정보에 오브젝트를 저장합니다.
  5. 이때 Session 에 저장될 때에는 오브젝트로 UserDetailsService만 저장이 가능합니다. 그리하여 보통은 오버라이드하여 PrincipalDetailsService implements UserDetailsService를 합니다.
  6. 그리고 UserDetailsService에는 UserDetails만 저장이 가능하므로, 이 때에도 PrincipalDetails implements UserDetails 를 합니다.
  7. 그리고 저장된 객체를 다시 AuthenticationProvider에게 돌려줍니다.
  8. 9. 이를 다시 AuthenicationFliter에게 돌려주면

  10. SecurityContextHolder에 정상적으로 저장이 됩니다.

 

0. 미리 알아두자

  • 인증(Authentication): 해당 사용자가 본인이 맞는지를 확인하는 절차
  • 인가(Authorization): 인증된 사용자가 요청한 자원에 접근 가능한지를 결정하는 절차

1. pom.xml 의존성 설정

<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
</dependency>

스프링 시큐리티를 사용할 것이기 때문에 spring-security 의존성을 설정해줍니다.

2. SecurityConfig class 설정

@Configuration
@EnableWebSecurity //스프링 시큐리티 필터가 스프링 필터체인에 등록이 된다.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean //해당 메서드의 리턴되는 오브젝트를 IoC로 등록해준다.
    public BCryptPasswordEncoder encodePwd(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.authorizeRequests()
                .antMatchers("/user/**").authenticated() //로그인(=인증된) 한 사람만 들어올 수 있다.
                .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')") //두가지 중 하나만 가져야 들어올 수 있음
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
                .anyRequest().permitAll() //위의 3가지 url이 아니면 모두 들어올 수 있음
                .and()
                .formLogin() //위 3가지 url을 들어가려면 login페이지로 이동하게 함
                .loginPage("/loginForm")
                .loginProcessingUrl("/login") //login주소가 호출이 되면 시큐리티가 낚아채서 대신 로그인을 진행해준다.즉, controller에 /login을 안만들어도됨
                .defaultSuccessUrl("/"); //로그인 성공하면 디폴트 url
    }
}
  • http.csrf().disable();: CSRF(Cross-Site Request Forgery) 공격을 방지하기 위해 Spring Security의 CSRF 보호를 비활성화합니다

 

  • http.authorizeRequests(): HTTP 요청에 대한 접근 권한을 구성하기 위한 메서드 호출을 시작합니다.
  • .antMatchers("/user/**").authenticated(): "/user/**" 패턴의 URL에 대해 인증된 사용자만 접근할 수 있도록 설정합니다. 즉, 로그인한 사용자만 해당 URL에 접근할 수 있습니다.
  • .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')"): "/manager/**" 패턴의 URL에 대해 "ROLE_ADMIN" 또는 "ROLE_MANAGER" 권한을 가진 사용자만 접근할 수 있도록 설정합니다. 즉, 이 URL에 접근하려면 관리자 또는 매니저 권한이 필요합니다.
  • .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')"): "/admin/**" 패턴의 URL에 대해 "ROLE_ADMIN" 권한을 가진 사용자만 접근할 수 있도록 설정합니다. 즉, 이 URL에 접근하려면 관리자 권한이 필요합니다.
  • .anyRequest().permitAll(): 위에서 설정한 URL 패턴 이외의 모든 요청은 권한 없이 허용됩니다. 즉, 인증되지 않은 사용자도 이러한 요청을 할 수 있습니다.
  • .and(): 이전 규칙을 종료하고 새로운 보안 규칙을 시작합니다.
  • .formLogin(): 폼 기반 인증을 활성화합니다. 이것은 사용자가 로그인 폼을 통해 인증하는 데 사용됩니다.
  • .loginPage("/loginForm"): 사용자가 로그인할 때 사용할 사용자 정의 로그인 페이지를 지정합니다. "/loginForm"은 사용자 정의 로그인 페이지의 URL입니다.
  • .loginProcessingUrl("/login"): 로그인 프로세스가 처리되는 URL을 지정합니다. 즉, "/login" 주소로 요청이 오면 Spring Security가 로그인 처리를 수행합니다.
  • .defaultSuccessUrl("/"): 로그인 성공 후 리다이렉트할 기본 URL을 지정합니다. 여기서는 "/"로 지정되어 있으므로 로그인 성공 후 홈 페이지로 이동합니다.

3. PrincipalDetails Setting

form 로그인은 Spring Security Session 기반 로그인입니다. 인증된 사용자의 정보는 아래 과정에 따라 Security 내부의 자체적인 Session에서 관리됩니다.

  1. POST "/login" 을 낚아채 로그인 진행
  2. 로그인이 완료되면 시큐리티 자체 session을 만들어 저장 (key: Security ContextHolder)
  3. 오브젝트 타입 : Authentication
  4. Authentication 안에 User(UserDetails) 객체를 가짐
  5. Security Session -> Authentication -> UserDetails

그러니 Principal/Authentication 객체로 현재 인증된 사용자 정보를 꺼내 쓰려면 UserDetails 인터페이스를 상속받는 User 클래스가 있어야합니다. 저는 User 대신 `PrincipalDetails`를 만들었습니다.

//시큐리티가 /login주소 요청이 오면 낚아채서 로그인을 진행시킨다.
//로그인 진행이 완료가되면 session을 만들어준다.(Security ContextHolder에 세션정보저장함)
//Security ContextHolder에 저장될 수 있는 오브젝트는 Authentication 타입 객체임!
//Authentication 안에 User정보가 있어야함.
//User오브젝트의 타입은 UserDetails타입의 객체여야한다.
//즉, Security Session에 들어갈 수 있는 객체가 Authentication 객체여야하고, 이 안에는 UserDetails만 들어갈 수 있다.

public class PrincipalDetails implements UserDetails {

    private User user;
    public PrincipalDetails(User user){
        this.user=user;
    }

    //해당 User의 권한을 리턴하는 곳!
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            //user.getRole은 string타입이라서 바로 리턴할 수 없기 때문에
            //collect를 오버라이드 해서 던져줘야함.
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });
        return collect;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

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

    @Override
    public boolean isEnabled() {
        //우리 사이트에서 1년동안 회원이 로그인을 안하면 휴먼계정으로 변환되기 했다면?
        //user.getLoginDate();를 들고와서
        //현재 시간 - 로그인 시간을 해서 1년을 초과하면
        //return false하면 된다.
        return true;
    }
}

 

4. PrincipalDetailsService Setting

  1. POST "/login" 요청이 오면 UserDetailsService(인터페이스) 타입으로 IoC되어 있는 loadUserByUsername 함수 실행
  2. 들어온 아이디값(username)을 가지고 존재하는 사용자인지 체크
  3. 사용자가 존재하면 User 정보를 담은 UserDetails 객체를 반환
//시큐리티 설정에서 loginProcesingUrl("/login");
//login요청이 오면 자동으로 UserDetailsService 타입으로 IoC되어잇는
//loadUserByUsername 함수가 실행된다.
@Service
public class PrincipalDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;


    //시큐리티 session - Authentication Type만 저장 가능 <- UserDetails만 저장 가능
    //Authentication(내부 UserDetails) => 시큐리티 session(Authentication(내부 UserDetails))
    //loadUserByUsername를 오버라이드 해주면 알아서 session안에 (Authentication(내부 UserDetails)를 넣어줌!
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User userEntity = userRepository.findByUsername(username);
        if(username != null) {
            return new PrincipalDetails(userEntity);
        }
        return null;
    }
}

5. Create joinForm.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>로그인 페이지</title>
</head>
<body>
<h1>회원가입 페이지</h1>
<hr/>
<form action="/join" method="POST">
  <input type="text" name="username" placeholder="Username"/> <br/>
  <input type="password" name="password" placeholder="Password"/> <br/>
  <input type="email" name="email" placeholder="Email"/> <br/>
  <button>회원가입</button>
</form>
</body>
</html>

6. Create UserController

@Controller
public class UserContoller{

@PostMapping("/join")
    public String join(User user){
        System.out.println(user);
        user.setRole("ROLE_USER");
        String rawPassword = user.getPassword();
        String encPassword = bCryptPasswordEncoder.encode(rawPassword);
        user.setPassword(encPassword); //password 암호화 진행

        userRepository.save(user);
        return "redirect:/loginForm";
    }
}

7. 성공적으로 저장!