- 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=) | 최소, 최대 길이 검사 |
이메일 형식인지 검사 | |
@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일 경우에만 접근 가능하도록 설정한다.
'Portfolio, Project > Project(Programming)' 카테고리의 다른 글
Project (5-1) 상품 등록 및 수정 (2) (0) | 2023.08.09 |
---|---|
Project (5-1) - 상품 등록 및 수정 (1) (0) | 2023.08.08 |
Project - (4) 연관관계 매핑 (0) | 2023.08.07 |
Project (2) - Thymeleaf (0) | 2023.08.02 |
Project - (1) Spring Data JPA (0) | 2023.08.01 |