개발/Project(Spring-쇼핑몰)

Project (3) - 스프링 시큐리티

잇(IT) 2023. 8. 3. 23:24
728x90
- security dependency 추가
<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

1. 스프링 시큐리티를 추가하게되면 모든 요청은 인증을 필요로 한다.

 

- 스프링 시큐리티는 기본적으로 1.Loing 2. Logout 기능을 제공한다.

 

- 스프링 시큐리티 설정

- SecurityConfig

@Configuration
@EnableWebSecurity
//SpringSecurityFilterChain이 자동으로 포함된다.
//WebSecurityConfigurerAdapter를 상속받아서 메소드 오버라이딩을 통해 보안 설정을 커스터마이징할 수 있다.
public class SecurityConfig {

    @Autowired
    MemberService memberService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// http 요청에 대한 보안 설정을 한다.
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    //    // 비밀번호를 데이터베이스에 그대로 저장하는 경우, 비밀번호가 그대로 노출되기 때문에 해시 함수를 이용하여 비밀번호를 암호화하여 저장한다.

}

1. 위 클래스 메서드에 설정을 추가하지 않으면 요청에 인증을 요구하지 않는다.

2. PasswordEncoder : // 비밀번호를 데이터베이스에 그대로 저장하는 경우, 비밀번호가 그대로 노출되기 때문에 해시 함수를 이용하여 비밀번호를 암호화하여 저장한다.

 

- 회원 가입 기능 구현

- Role

1. 유저 관리자 구분을 위한 Role (enum)

public enum Role {
    USER, ADMIN
    //Role의 값으로 USER와 ADMIN 2개를 입력한다.
}

 

- MemberFormDto

1. 회원 가입 화면으로부터 넘어오는 가입정보를 담을 dto 생성

@Getter
@Setter
public class MemberFormDto {

    @NotBlank(message = "이름은 필수 입력 값입니다")
    private String name;

    @NotEmpty(message = "이메일은 필수 입력 값입니다.")
    @Email(message = "이메일 형식으로 입력해주세요.")
    private String email;

    @NotEmpty(message = "비밀번호는 필수 입력 값입니다.")
    @Length(min=8,max=16, message = "비밀번호는 8자 이상, 16자 이하로 입력해주세요")
    private String password;

    @NotEmpty(message = "주소는 필수 입력 값입니다.")
    private String address;
}

 

- Member

1. 회원 정보를 저장하는 Member 엔티티

@Entity
@Table(name="member")
@Getter @Setter
@ToString
public class Member {
    //회원 정보를 저장하는 Member 엔티티 생성

    @Id
    @Column(name = "member_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @Column(unique = true)
    //이메일은 유일해야 하기 때문에 unique 옵션을 true로 설정해준다.
    private String email;

    private String password;

    private String address;

    @Enumerated(EnumType.STRING)
    //Enum의 경우 순서로 저장하게 되면 Enum에 속한 데이터가 변경되면 순서가 바뀔 위험이 있기 때문에 String으로 처리한다.
    private Role role;

    public static Member createMember(MemberFormDto memberFormDto, PasswordEncoder passwordEncoder) {
        //Member 엔티티를 생성하는 메소드이다. Member 엔티티에 회원을 생성하는 메소드를 만들어서 관리하면 코드 변경시 한 곳만 수정하면 되는 이점이 있다.
        Member member = new Member();
        member.setName(memberFormDto.getName());
        member.setEmail(memberFormDto.getEmail());
        member.setAddress(memberFormDto.getAddress());
        String password = passwordEncoder.encode(memberFormDto.getPassword());
        //스프링 시큐리티 설정 클래스에 등록된 BCryptPasswordEncoder Bean을 파라미터로 넘겨서 비밀번호를 암호화 한다.
        member.setPassword(password);
        member.setRole(Role.ADMIN);
        return member;
    }
}

 

- MemberRepository

1. Member 엔티티를 데이터베이스에 저장할 수 있도록 MemberRepository를 만든다.

public interface MemberRepository extends JpaRepository<Member, Long> {

    Member findByEmail(String email);
    //회원 가입 시 중복된 회원이 있는지 검사하기 위해서 이메일로 회원을 검사할 수 있도록 쿼리 메소드를 작성한다.
}

1. 스프링 데이터 JPA를 이용하여 Email을 통해 회원을 검사할 수 있도록 쿼리 메소드를 작성한다.

 

- MemberService

@Service
@Transactional
//하나의 트랜잭션 안에서 동작하게되면 에러 발생 시 롤백이 가능해진다. 또한 데이터 일관성을 유지할 수 있다.
@RequiredArgsConstructor
//final이나 @NonNull이 붙은 필드에 생성자를 생성해준다.
public class MemberService implements UserDetailsService {
    //MemberService가 UserDetailsService를 구현한다.

    private final MemberRepository memberRepository;
    //@RequiredArgsConstructor 빈을 주입하는 방식, 빈에 생성자가 1개이고 생성자의 파라미터 타입이 빈으로
    //등록이 가능하다면 @Autowired 없이 의존성 주입이 가능하다.

    public Member saveMember(Member member) {
        validateDuplicateMember(member);
        return memberRepository.save(member);
    }

    private void validateDuplicateMember(Member member) {
        //이미 가입된 회원의 경우 IllegalStateException 예외를 발생시킨다.
        Member findMember = memberRepository.findByEmail(member.getEmail());
        if (findMember != null) {
            throw new IllegalStateException("이미 가입된 회원입니다.");
        }
    }
}

1. @Transactional을 사용하게 되면 트랜잭션 안에서 데이터의 일관성이 보장된다. 에러가 발생할 경우 콜백을 통해 데이터를 원래 상태로 돌릴 수 있다.

 

- MemberServiceTest

@SpringBootTest
@Transactional
//테스트 클래스에 @Transactional 어노테이션을 선언하면, 테스트 실행 후 롤백 처리가 된다.
@TestPropertySource(locations = "classpath:application-test.properties")
public class MemberServiceTest {

    @Autowired
    MemberService memberService;

    @Autowired
    PasswordEncoder passwordEncoder;

    public Member createMember() {
        MemberFormDto memberFormDto = new MemberFormDto();
        memberFormDto.setEmail("test@email.com");
        memberFormDto.setName("홍길동");
        memberFormDto.setAddress("서울시 마포구 합정동");
        memberFormDto.setPassword("1234");
        return Member.createMember(memberFormDto, passwordEncoder);
    }
    
    @Test
    @DisplayName("회원가입 테스트")
    public void saveMemberTest() {
        Member member = createMember();
        Member savedMember = memberService.saveMember(member);

        assertThat(member.getEmail()).isEqualTo(savedMember.getEmail());
        assertThat(member.getName()).isEqualTo(savedMember.getName());
        assertThat(member.getAddress()).isEqualTo(savedMember.getAddress());
        assertThat(member.getPassword()).isEqualTo(savedMember.getPassword());
        assertThat(member.getRole()).isEqualTo(savedMember.getRole());
    }
    @Test
    @DisplayName("중복 회원 가입 테스트")
    public void saveDuplicateMemberTest() {
        Member member1 = createMember();
        Member member2 = createMember();
        memberService.saveMember(member1);

        IllegalStateException e = assertThrows(IllegalStateException.class, () -> {
            memberService.saveMember(member2);
        });

        assertEquals("이미 가입된 회원입니다.", e.getMessage());
    }
}

1. 테스트 코드를 통해 회원 가입 테스트, 중복 회원 테스트를 해 볼 수 있다.

 

- MemberController

@RequestMapping("/members")
@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;
    private final PasswordEncoder passwordEncoder;

    @GetMapping(value = "/new")
    public String memberForm(Model model) {
        model.addAttribute("memberFormDto", new MemberFormDto());
        return "member/memberForm";
    }

 

- memberForm.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout=http://www.ultraq.net.nz/thymeleaf/layout
      layout:decorate="~{layouts/layout1}">

<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
    <style>
        .fieldError {
            color:#bd2130;
        }
    </style>
</th:block>

<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="scripts">
    <script th:inline="javascript">
        $(document).ready(function () {
            // 회원 가입 시 실패했다면 에러 메시지를 경고창을 이용해서 보여준다.
            var errorMessage = [[${errorMessage}]];
            if (errorMessage != null) {
                alert(errorMessage);
            }
        });
    </script>
</th:block>

<div layout:fragment="content">

    <form action="/members/new" role="form" method="post"  th:object="${memberFormDto}">
        <div class="form-group">
            <label th:for="name">이름</label>
            <input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력해주세요">
            <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="fieldError">Incorrect data</p>
        </div>
        <div class="form-group">
            <label th:for="email">이메일주소</label>
            <input type="email" th:field="*{email}" class="form-control" placeholder="이메일을 입력해주세요">
            <p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="fieldError">Incorrect data</p>
        </div>
        <div class="form-group">
            <label th:for="password">비밀번호</label>
            <input type="password" th:field="*{password}" class="form-control" placeholder="비밀번호 입력">
            <p th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="fieldError">Incorrect data</p>
        </div>
        <div class="form-group">
            <label th:for="address">주소</label>
            <input type="text" th:field="*{address}" class="form-control" placeholder="주소를 입력해주세요">
            <p th:if="${#fields.hasErrors('address')}" th:errors="*{address}" class="fieldError">Incorrect data</p>
        </div>
        <div style="text-align: center">
            <button type="submit" class="btn btn-primary" style="">Submit</button>
        </div>
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<!--CSRF 토큰은 실제 서버에서 허용한 요청이 맞는지 확인하기 위한 토큰이다. 사용자의 세션에 임의의 값을 저장하여
요청마다 그 값을 포함하여 전송하여 전송하면 서버에서 세션에 저장된 값과 요청이 온 값이 일치하는지 확인하여 CSRF를 방어한다.-->
    </form>

</div>

</html>

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout=http://www.ultraq.net.nz/thymeleaf/layout
      layout:decorate="~{layouts/layout1}">

- Thymeleaf 레이아웃 네임스페이스를 선언한다.

- ~{...}는 fragment 표현식에 해당한다. layouts/layout1 템플릿을 레이아웃 템플릿으로 사용하겠다는 의미이다.

 

<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
    <style>
        .fieldError {
            color:#bd2130;
        }
    </style>
</th:block>

- layout:fragment="css"는 fragment / css에 블록을 적용하여 사용하겠다는 의미이다.

- fieldError 클래스에 대해 해당 색상을 넣겠다는 의미다.

 

<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">

    <script th:inline="javascript">
        $(document).ready(function(){
            var errorMessage = [[${errorMessage}]];
            if(errorMessage != null){
                alert(errorMessage);
            }
        });
    </script>
</th:block>

- 회원 가입 시 실패하게 되면 에러 메시지를 경고창을 이용해서 보여준다.

 

<div layout:fragment="content">

    <form action="/members/new" role="form" method="post"  th:object="${memberFormDto}">
        <div class="form-group">
            <label th:for="name">이름</label>
            <input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력해주세요">
            <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="fieldError">Incorrect data</p>
        </div>

- /members/new URL로 post 방식으로 데이터를 전달한다.

- th:object를 통해 memberFormDto 객체에 바인딩 되어 데이터를 전달한다.

- th:for 속성을 사용하여 레이블을 연결할 입력 필드를 지정한다. 속성은 연결할 입력 필드의 id 값을 가르킨다. 레이블을 클릭하면 해당 입력 필드로 포커스가 이동한다.

- th:field를 통해 id, name, value값이 설정된다. 보통 id, name은 field명으로 생성되고 vlaue의 경우 memberFormDto.getName() 메서드가 호출되어 name 필드의 값을 가져온다.

 

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

- CSRF를 방어하기 위해 모든 POST 방식의 데이터 전송에는 CSRF 토큰 값이 있어야 한다.CSRF 토큰은 실제 서버에서 허용한 요청이 맞는지 확인하기 위한 토큰이다. 요청마다 그 값을 포함하여 전송하면 서버에서 세션에 저장된 값과 요청이 온 값이 일치하는지 확인하여 CSRF를 방어한다.

 

- MemberController

@RequestMapping("/members")
@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;
    private final PasswordEncoder passwordEncoder;

    @GetMapping(value = "/new")
    public String memberForm(Model model) {
        model.addAttribute("memberFormDto", new MemberFormDto());
        return "member/memberForm";
    }

    @PostMapping(value = "/new")
    public String newMember(@Valid MemberFormDto memberFormDto,
                            BindingResult bindingResult, Model model) {
//@Valid 어노테이션을 선언하고, 파라미터로 bindingResult 객체를 추가한다. 검사 후 결과는 bindingResult에 담아준다.
        if (bindingResult.hasErrors()) {
            //bindingResult.hasErrors()를 호출하여 에러가 있다면 회원 가입 페이지로 이동한다.
            return "member/memberForm";
        }

        try {
            Member member = Member.createMember(memberFormDto, passwordEncoder);
            memberService.saveMember(member);
        } catch (IllegalStateException e) {
            model.addAttribute("errorMessage", e.getMessage());
            return "member/memberForm";
        }
        return "redirect:/";
    }

 

 

- MainController

@Controller
public class MainController {

    @GetMapping(value = "/")
    public String main() {
        return "main";
    }
}

- 검증 validation
<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
어노테이션 설명
@NotEmpty Null 체크 및 문자열의 경우 길이 0인지 검사
@NotBlank Null 체크 및 문자열의 경우 길이 0 및 빈 문자열(" ") 검사
@Length(min=, max=) 최소, 최대 길이 검사
@Email 이메일 형식인지 검사
@Max(숫자) 지정한 값보다 작은지 검사
@Min(숫자) 지정한 값보다 큰지 검사
@Null 값이 NULL인지 검사
@NouNull 길이 NULL인지 아닌지 검사

 

- MemberFormDto

@Getter
@Setter
public class MemberFormDto {

    @NotBlank(message = "이름은 필수 입력 값입니다")
    private String name;

    @NotEmpty(message = "이메일은 필수 입력 값입니다.")
    @Email(message = "이메일 형식으로 입력해주세요.")
    private String email;

    @NotEmpty(message = "비밀번호는 필수 입력 값입니다.")
    @Length(min=8,max=16, message = "비밀번호는 8자 이상, 16자 이하로 입력해주세요")
    private String password;

    @NotEmpty(message = "주소는 필수 입력 값입니다.")
    private String address;
}

 

- MemberController

...

@PostMapping(value = "/new")
    public String newMember(@Valid MemberFormDto memberFormDto,
                            BindingResult bindingResult, Model model) {
//@Valid 어노테이션을 선언하고, 파라미터로 bindingResult 객체를 추가한다. 검사 후 결과는 bindingResult에 담아준다.
        if (bindingResult.hasErrors()) {
            //bindingResult.hasErrors()를 호출하여 에러가 있다면 회원 가입 페이지로 이동한다.
            return "member/memberForm";
        }

        try {
            Member member = Member.createMember(memberFormDto, passwordEncoder);
            memberService.saveMember(member);
        } catch (IllegalStateException e) {
            model.addAttribute("errorMessage", e.getMessage());
            return "member/memberForm";
        }
        return "redirect:/";
    }

- @Valid 어노테이션과 객체(memberFormDto) 뒤에 bindingResult를 붙여주게되면, bindingResult.hasError()를 호출하여 에러가 있다면 return 경로로 이동한다.


- 스프링 시큐리티 로그인/로그아웃

 

- UserDetailsService

1. UserDetailService 인터페이스는 데이터베이스에서 회원 정보를 가져오는 역할을 담당한다.

2. loadUserByUsername() 메소드가 존재하며, 회원 정보를 조회하여 사용자의 정보와 권한을 갖는 UserDeatils 인터페이스를 반환한다.

* 스프링 시큐리티에서 UserDetailService를 구현하고 있는 클래스를 통해 로그인 기능을 구현한다고 생각하면 된다.

 

- UserDetail

1. 스프링 시큐리티에서 회원의 정보를 담기 위해서 사용하는 인터페이스는 UserDetails이다. 이 인터페이스를 직접 구현하거나 스프링 시큐리티에서 제공하는 User클래스를 사용한다. User클래스는 UserDetails 인터페이스를 구현하고 있는 클래스다.

 

- MemberService

@Service
@Transactional
//하나의 트랜잭션 안에서 동작하게되면 에러 발생 시 롤백이 가능해진다. 또한 데이터 일관성을 유지할 수 있다.
@RequiredArgsConstructor
//final이나 @NonNull이 붙은 필드에 생성자를 생성해준다.
public class MemberService implements UserDetailsService {
    //MemberService가 UserDetailsService를 구현한다.

...

@Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        //UserDetailsService 인터페이스의 loadUserByUsername() 메소드를 오버라이딩한다.
        Member member = memberRepository.findByEmail(email);
		//로그인할 유저의 email을 파라미터로 전달받는다.
        if (member == null) {
            throw new UsernameNotFoundException(email);
        }

        return User.builder()
                .username(member.getEmail())
                .password(member.getPassword())
                .roles(member.getRole().toString())
                .build();
                //builder 패턴을 이용해 User 객체에 회원의 이메일, 패스워드, 비밀번호, role을 파라미터로 넘긴다.
    }
}

 

- SecurityConfig

@Configuration
@EnableWebSecurity
//SpringSecurityFilterChain이 자동으로 포함된다.
//WebSecurityConfigurerAdapter를 상속받아서 메소드 오버라이딩을 통해 보안 설정을 커스터마이징할 수 있다.
public class SecurityConfig {

    @Autowired
    MemberService memberService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // http 요청에 대한 보안을 설정한다.
        http.formLogin()
                .loginPage("/members/login")
                //로그인 페이지 URL을 설정한다.
                .defaultSuccessUrl("/")
                //로그인 성공 시 이동할 URL을 설정한다.
                .usernameParameter("email")
                //로그인 시 사용할 파라미터 이름으로 email을 지정한다.
                .failureUrl("/members/login/error")
                //로그인 실패 시 이동할 URL을 설정한다.
                .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/members/logout"))
                //로그아웃 URL을 설정한다.
                .logoutSuccessUrl("/");
                //로그아웃 성공 시 이동할 URL을 설정한다.
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    // 비밀번호를 데이터베이스에 그대로 저장하는 경우, 비밀번호가 그대로 노출되기 때문에 해시 함수를 이용하여 비밀번호를 암호화하여 저장한다.
}

- SecurityFilterChain은 스프링 시큐리티에서 사용되는 인터페이스로, 보안 필터들의 체인을 나타내는 역할을 한다. 각각의 보안 필터들은 요청에 대한 보안 처리를 담당하며, 보안 설정을 적용하여 인증과 권한 부여 등의 작업을 수행한다.

- HttpSecurity는 웹 기반 보안을 구성하는데 사용된다. HttpSecurity 객체는 SecurityFilterChain을 생성하고 구성하는 데 사용된다.

- HttpSecurity를 사용하여 로그인 페이지, 로그아웃 처리, 권한에 따른 접근 제어 등을 설정할 수 있다.

- 이렇게 구성된 SecurityFilterChain은 스프링 시큐리티의 필터 체인에 추가되어 웹 애플리케이션의 보안 설정이 적용된다.

 

- memberLoginForm

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
    <style>
        .error {
            color: #bd2130;
        }
    </style>
</th:block>

<div layout:fragment="content">

    <form role="form" method="post" action="/members/login">
        <div class="form-group">
            <label th:for="email">이메일주소</label>
            <input type="email" name="email" class="form-control" placeholder="이메일을 입력해주세요">
        </div>
        <div class="form-group">
            <label th:for="password">비밀번호</label>
            <input type="password" name="password" id="password" class="form-control" placeholder="비밀번호 입력">
        </div>
        <p th:if="${loginErrorMsg}" class="error" th:text="${loginErrorMsg}"></p>
<!--        loginErrorMsg가 있으면 해당 텍스트를 내보낸다.-->
        <button class="btn btn-primary">로그인</button>
        <button type="button" class="btn btn-primary" onClick="location.href='/members/new'">회원가입</button>
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
    </form>

</div>

</html>

- 스프링 시큐리티를 사용하면 /login 경로로 POST요청을 처리하는 로그인 처리를 따로 구현할 필요가 없다. 스프링 시큐리티는 기본적으로 /login 경로에 대한 로그인 처리를 자동으로 처리해준다.

- 스프링 시큐리티는 내부적으로 UsernamePasswordAuthenticationFilter라는 필터를 사용하여 로그인 처리를 담당한다. 이 필터는 POST 방식으로 /login 경로로 요청이 오면, 전달된 사용자 이름과 비밀번호를 확인하여 인증 작업을 처리한다. 또한 인증에 성송하면 사용자를 인증된 상태로 유지하고, 인증에 실패하면 로그인 페이지로 다시 리디렉션 한다.

 

- MemberController

@RequestMapping("/members")
@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;
    private final PasswordEncoder passwordEncoder;
    
...

@GetMapping(value = "/login")
    public String loginMember() {
        return "/member/memberLoginForm";
    }

    @GetMapping(value = "/login/error")
    public String loginError(Model model) {
        model.addAttribute("loginErrorMsg", "아이디 또는 비밀번호를 확인해주세요");
        return "/member/memberLoginForm";
    }
}

- POST 방식으로 전달하는 데이터의 경우 (/member/login) 스프링 시큐리티에서 알아서 처리하기 때문에 나머지 페이지에 대한 컨트롤러만 작성해주면 된다.

 

- header.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
      <!-- 스프링 시큐리티 태그를 사용하기 위해서 네임스페이스를 추가한다-->

<div th:fragment="header">
    <nav class="navbar navbar-expand-sm bg-primary navbar-dark">
        <button class="navbar-toggler" type="button" data-toggle="collapse"
                data-target="#navbarTogglerDemo03" aria-controls="navbarTogglerDemo03"
                aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <a class="navbar-brand" href="/">Shop</a>

        <div class="collapse navbar-collapse" id="navbarTogglerDemo03">
            <ul class="navbar-nav mr-auto mt-2 mt-lg-0">
                <li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
                    <a class="nav-link" href="/admin/item/new">상품 등록</a>
                </li>
                <li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
                    <a class="nav-link" href="/admin/items">상품 관리</a>
                </li>
                <li class="nav-item" sec:authorize="isAuthenticated()">
                    <a class="nav-link" href="/cart">장바구니</a>
                </li>
                <li class="nav-item" sec:authorize="isAuthenticated()">
                    <a class="nav-link" href="/orders">구매이력</a>
                </li>
                <li class="nav-item" sec:authorize="isAnonymous()">
                    <a class="nav-link" href="/members/login">로그인</a>
                </li>
                <li class="nav-item" sec:authorize="isAuthenticated()">
                    <a class="nav-link" href="/members/logout">로그아웃</a>
                </li>
            </ul>
            <form class="form-inline my-2 my-lg-0" th:action="@{/}" method="get">
                <input name="searchQuery" class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
                <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
            </form>
        </div>
    </nav>
</div>
</html>
<li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">

- 관리자 계정(ADMIN ROLE)으로 로그인한 경우 삼품 등록, 상품 관리 메뉴를 보여준다.

public enum Role {
    USER, ADMIN
    //Role의 값으로 USER와 ADMIN 2개를 입력한다.
}
return User.builder()
                .username(member.getEmail())
                .password(member.getPassword())
                .roles(member.getRole().toString())
                .build();

- 주어진 코드에서 .roles(member.getRole().toString()) 부분은 User 객체를 생성할 때 해당 사용자의 역할(Role)을 문자열로 변환하여 설정하는 것입니다. 그리고 해당 문자열은 Spring Security에서 권한을 표현하는 방식 중 하나인 ROLE_ 접두사를 추가한 문자열 형태로 사용됩니다.


- 페이지 권한 설정하기

 

- ItemForm.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

<div layout:fragment="content">

    <h1>상품등록 페이지입니다.</h1>
</div>
</html>

 

- ItemController.java

package com.shop.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ItemController {

    @GetMapping(value = "/admin/item/new")
    public String itemForm() {
        return "/item/itemForm";
    }
}

- ajax의 경우 http request header에 XMLHttpRequest라는 값이 세팅되어 요청이 오는데, 인증되지 않은 사용자가 ajax로 리소스를 요청할 경우 "Unauthorized" 에러를 발생시키고 나머지 경우는 로그인 페이지로 리다이렉트 시켜줍니다.

 

- SecurityConfig.java

http.authorizeRequests()
                .mvcMatchers("/css/**", "/js/**", "/img/**").permitAll()
                .mvcMatchers("/", "/members/**", "/item/**", "/images/**").permitAll()
                .mvcMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated();

        http.exceptionHandling()
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint());
                
        return http.build();
    }
http.authorizeRequests()

- http.authorizeRequests() : 시큐리티 처리에 HttpServletRequest를 이용한다는 것을 의미한다.

.mvcMatchers("/css/**", "/js/**", "/img/**").permitAll()
.mvcMatchers("/", "/members/**", "/item/**", "/images/**").permitAll()

- permitAll()을 통해 모든 사용자가 인증(로그인)없이 해당 경로에 접근할 수 있도록 설정한다. 또 정적 페이지 또한 인증(로그인)없이 경로에 접근 가능하도록 설정한다.

.mvcMatchers("/admin/**").hasRole("ADMIN")

- /admin으로 시작하는 경로는 해당 계정이 ADMIN Role일 경우에만 접근 가능하도록 설정한다.

728x90