Thymeleaf + Spring Security integration basics
https://www.thymeleaf.org/doc/articles/springsecurity.html
https://spring.io/guides/gs/securing-web/
스프링 부트 사용버전
1. 소스 : https://github.com/braverokmc79/Springboot-JPA-Blog (jsp, thymeleaf 둘다 사용)
2. 소스 : https://github.com/braverokmc79/spring-boot-jpa-member-join (thymeleaf )
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.2</version> </parent>
1 .pom.xml
<!-- ============================- --> <!-- 시큐리티 라이브러리 시작 --> <!-- spring-boot-starter-security 추가 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- 유효성 검증을 위해 spring-boot-starter-validation 추가 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- 시큐리티 테스틀 위해 추가 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <!-- jsp 사용시 로그인 상태 유무 권한 설정할 수 있게 추가--> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> </dependency> <!-- thymeleaf 에서 로그인 상태 유무 권한 설정할 수 있게 추가 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency> <!-- 시큐리티 라이브러리 끝 --> <!-- ============================- -->
스프링 시큐리티는
위 라이브러리 등록후 아래와 같은 두개의 핵심적인 클래스만 생성하면 SecurityConfig, PrincipalDetailService
거의 완료 되었다고 볼수 있다.
1. 소스 : https://github.com/braverokmc79/Springboot-JPA-Blog
1. SecurityConfig.java
package com.cos.blog.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; //빈등록 :스프링 컨테이너에서 객체를 관리할 수 있게 하는것 import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import com.cos.blog.config.auth.PrincipalDetailService; @Configuration //빈등록 (IOC 관리) @EnableWebSecurity //시큐리티 필터가 등록이 된다. @EnableGlobalMethodSecurity(prePostEnabled = true) //특정 주소로 접근을 하면 권한 및 인증을 미리 체크하겠다는 뜻. public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private PrincipalDetailService principalDetailService; @Bean //loC 가 된다. public BCryptPasswordEncoder encodePWD() { return new BCryptPasswordEncoder(); } /** * * Spring Secureity 에서 인증은 AuthenticationManager 를 통해 이루어지며 * AuthenticationManagerBuilder 가 AuthenticationManager 를 생성합니다. * userDetailsService 를 구현하고 있는 객체로 PrincipalDetailService를 지정해 주며, * 비밀번호 암호화를 위해 passwordEncoder 를 지정해줍니다. //시큐리티가 대신 로그인해주는데 password 를 가로채기를 하는데 //해당 password 가 뭘로 해쉬가 되어 회원가입이 되었는지 알아야 //같은 해쉬로 암호화해서 DB에 있는 해쉬랑 비교할 수 있음. * */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD()); } @Override protected void configure(HttpSecurity http) throws Exception { http //.csrf().disable() //csrf 토큰 비활성화(테스트시 걸어두는 것이 좋다) .authorizeRequests() //시큐리티 처리에 HttpServletRequest 를 이용한다는 것을 의미 .mvcMatchers("/", "/auth/**", "/js/**", "/css/**", "/images/**") .permitAll() .anyRequest().authenticated(); //위설정한 경로를 제외한 나머지 경로들은 모든 인증을 요구하도록 설정 http .formLogin() .loginPage("/auth/loginForm") //로그인 페이지 URL 을 설정 .loginProcessingUrl("/auth/loginProc") //스프링 시큐리티가 해당 주소로 요청오는 로그인을 가로채서 대신 로그인 해준다. 적지 않으면 .loginPage("/auth/loginForm") 값이 기본값 처리 .usernameParameter("username") //로그인시 사용할 파라미터 이름으로 username (또는 email) 여기서는 username 을 지정 .passwordParameter("password") //없을시 기본값 :password .defaultSuccessUrl("/") // 로그인 성공시 이동할 URL 을 설정 .failureUrl("/auth/loginForm/error") // 로그인 실패시 이동할 URL 을 설정 .and() .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/auth/logout")) // 임의로그아웃 URL 을 설정하면당 스프링시큐리티가 해당 url 로 로그아웃 처리 . 기본 값은 logout .logoutSuccessUrl("/"); //로그아웃 성공시 이동할 URL 을 설정 } /** *static 디렉터리의 하위 파일은 인증을 무시하도록 설정 */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/img/**"); } }
2.PrincipalDetailService
package com.cos.blog.config.auth; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.cos.blog.model.User; import com.cos.blog.repository.UserRepository; @Service //Bean 등록 @Transactional public class PrincipalDetailService implements UserDetailsService{ @Autowired private UserRepository userRepository; /**스프링이 로그인 요청을 가로챌 때, username, passwrod 변수 2개를 가로채는데 password 부분처리는 알아서 함. username (email) 이 DB 에 있는지만 확인해주면 됨. return */ /**방법 1 컨트롤에서 다음과 같은 방법으로 시큐리티 세션정보를 가져온다. Principal principal @GetMapping("/user/updateForm") public String updateUserForm(Principal principal, Model model) { User userInfo =userService.getByUsername(principal.getName()); model.addAttribute("userInfo" ,userInfo); return "user/updateUserForm"; } */ /** @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //이메일경우 예 // User principal=userRepository.findByEmail(email); // if(principal==null){ // throw new UsernameNotFoundException(email); // } // UserDetails result= org.springframework.security.core.userdetails.User.builder() // .username(principal.getEmail()) // .password(principal.getPassword()) // .roles(principal.getRole().toString()) // .build(); User principal=userRepository.findByUsername(username).orElseThrow(()->{ return new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다. :" + username); }); UserDetails result= org.springframework.security.core.userdetails.User.builder() .username(principal.getUsername()) .password(principal.getPassword()) .roles(principal.getRole().toString()) .build(); return result; } */ /**방법2 - 커스텀 로그인 방법 : UserDetails 상속받은 PrincipalDetails 생성후 로그인 처리 방법 컨트롤에서 다음과 같이 시큐리티 세션정보를 가져올수 있다. @AuthenticationPrincipal PrincipalDetails principal @GetMapping("/user/updateForm") public String updateUserForm(@AuthenticationPrincipal PrincipalDetails principal, Model model) { model.addAttribute("userInfo" ,principal.getUser()); return "user/updateUserForm"; } */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User principal=userRepository.findByUsername(username).orElseThrow(()->{ return new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다. :" + username); }); if(principal == null) { return null; } return new PrincipalDetails(principal); } }
PrincipalDetails
import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.Map; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.user.OAuth2User; import com.cos.blog.model.User; import groovy.transform.ToString; import lombok.Data; import lombok.NoArgsConstructor; @NoArgsConstructor @Data @ToString public class PrincipalDetails implements UserDetails, OAuth2User{ private static final long serialVersionUID = 1L; private User user; private Map<String, Object> attributes; //일반 로그인 public PrincipalDetails(User user) { this.user=user; } // OAuth 로그인 public PrincipalDetails(User user, Map<String, Object> attributes) { this.user = user; this.attributes = attributes; } @Override public Map<String, Object> getAttributes() { return attributes; } @Override public String getName() { return String.valueOf(user.getId()); } // 권한 (원래 권한이 여러개 있을 수 있으므로 Collection 루프 돌려야 함) @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> authorities = new ArrayList<>(); // collect.add(new GrantedAuthority() { // @Override // public String getAuthority() { // return memberDto.getMemberRole(); // "ROLE_" + @가 되어야 함 // } // }); //스프링부트 2.5 이상 ROLE 슬러시를 붙이면 안된다. 자동으로 붙여준다. authorities.add( () -> { return user.getRole().toString();}); return authorities; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUsername(); } // 계정 만료(false) 여부 @Override public boolean isAccountNonExpired() { return true; } // 계정 정지(false) 여부 @Override public boolean isAccountNonLocked() { return true; } // 계정 신용(false) 여부 @Override public boolean isCredentialsNonExpired() { return true; } // 계정 활성(false) 여부 (예 : 최종 로그인 후 일정기간 경과 여부 -> 휴면계정) @Override public boolean isEnabled() { return true; } }
다음 아래부터 추가적으로 내용 참조.
spring-boot-starter-security 추가하면 스프링 시큐리티에서 기본적으로 로그인 페이지로 이동함.
아이디 user , 비밀번호는 콘솔창에 출력
2. SecurityConfig 추가
다음 클래스를 추가하면 더이상 인증 요청하지 않으며
커스텀 로그인 처리
import com.macaronics.service.MemberService; import groovy.lang.GrabConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; /** *WebSecurityConfigurerAdapter 상속받는 클래스에 @EnableWebSecurity 어노테이션을 *선언하면 SpringSecurityFilterChain 이 자동으로 포함 *WebSecurityConfigurerAdapter를 상송받아서 메소드 오버라이딩을 통해 보안 설정을 커스터마이징할 수 있음 * *http 요청에 대한 보안 으로 페이지 권한 설정, 로그인 페이지 설정, 로그아웃 메소드 등에 대한 설정. * * */ @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired MemberService memberService; @Override protected void configure(HttpSecurity http) throws Exception{ 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 을 설정 http.authorizeRequests() //시큐리티 처리에 HttpServletRequest 를 이용한다는 것을 의미. .mvcMatchers("/", "/members/**","/board/**", "/images/**").permitAll() //permitAll() 을 통해 모든 사용자가 인증(로그인 없이 해당 경로 접근 ) .mvcMatchers("/admin/**").hasRole("ADMIN") // admin 으로 시작하는 경로는 해당 계정이 ADMIN Role 일 경우에만 접근이 가능 .anyRequest().authenticated(); //위설정한 경로를 제외한 나머지 경로들은 모든 인증을 요구하도록 설정 http.exceptionHandling() .authenticationEntryPoint(new CustomAuthenticationEntryPoint()); //인증되지 않은 사용자가 리소스에 접근하였을 때 수행되는 핸들러를 등록 } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } /** * * Spring Secureity 에서 인증은 AuthenticationManager 를 통해 이루어지며 * AuthenticationManagerBuilder 가 AuthenticationManager 를 생성합니다. * userDetailsService 를 구현하고 있는 객체로 memberService를 지정해 주며, * 비밀번호 암호화를 위해 passwordEncoder 를 지정해줍니다. */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception{ auth.userDetailsService(memberService).passwordEncoder(passwordEncoder()); } /** *static 디렉터리의 하위 파일은 인증을 무시하도록 설정 */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/img/**"); } }
3. Role
public enum Role { //역할은 자동으로 삽입되므로 'ROLE_'로 시작하면 안 됩니다 GUEST , //손님 USER , //일반 사용자 MANAGER , //중간 관리자 ADMIN ; //촤고 관리자 }
4. DTO
MemberFormDto
import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; import lombok.Getter; import lombok.Setter; @Getter @Setter public class MemberFormDto { @NotBlank(message = "이름은 필수 입력 값입니다.") private String name; @NotEmpty(message = "이메일은 필수 입력 값입니다.") @Email(message = "이메일 형식을 맞춰주세요.") private String email; @NotEmpty(message = "비밀번호는 필수 입력 값입니다.") //@Length(min = 4, max = 16, message = "비밀번호는 4자 이상, 16자 이하로 입력해 주세요.") @Pattern(regexp="(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}", message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8자 ~ 20자의 비밀번호여야 합니다.") private String password; @NotEmpty(message = "주소는 필수 입력 값입니다.") private String address; }
5. entity
Member.java
import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import org.hibernate.annotations.Comment; import org.springframework.security.crypto.password.PasswordEncoder; import com.macaronics.constant.Role; import com.macaronics.dto.MemberFormDto; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Entity @Table(name="member") @Getter @Setter @ToString public class Member { @Id @Column(name="member_id") @GeneratedValue(strategy = GenerationType.AUTO) @Comment("회원아이디") private Long id; @Comment("회원이름") private String name; @Column(unique = true) @Comment("이메일") private String email; @Comment("비밀번호") private String password; @Comment("주소") private String address; /**Enum 순서가 바뀔 경우 문제가 발생할 수 있으므로 * EnumType.STRING 옵션을 사용해서 String */ @Enumerated(EnumType.STRING) private Role role; public static Member createMember(MemberFormDto memberFormDto, PasswordEncoder passwordEncoder, Role state){ Member member=new Member(); member.setName(memberFormDto.getName()); member.setEmail(memberFormDto.getEmail()); member.setAddress(memberFormDto.getAddress()); String password=passwordEncoder.encode(memberFormDto.getPassword()); member.setPassword(password); member.setRole(state); return member; } }
6. Repository
MemberRepository
import org.springframework.data.jpa.repository.JpaRepository; import com.macaronics.entity.Member; public interface MemberRepository extends JpaRepository { Member findByEmail(String email); }
7. Service
MemberService
import com.macaronics.entity.Member; import com.macaronics.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import javax.transaction.Transactional; /** * @Transactional * :비즈니스 로직을 담당하는 서비스 계층 클래스에 @Transactional 어노테이션을 선언합니다. * 로직을 처리하닥 에러가 발생하였다면, 변경된 데이터를 로직을 수행하기 이전 상태로 콜백 시켜줍니다. * * * @RequiredArgsConstructor * :빈을 주입하는 방법으로 @Autowired 어노테이션을 이용하거나, 필드 주입(Setter 주입), 생성자 주입을 이용 * 하는 방법이 있습니다. @RequiredArgsConstructor 어노테이션은 final 이나 @NonNull 이 붙은 필드에 * 생성자를 생성해줍니다. 빈에 생성자가 1개이고 생성자의 파라미터 타입이 빈으로 등록이 가능하다면 @Autowired * 어노테이션 없이 의존성 주입이 가능합니다. * * * * * 로그인 * * UserDetailsService * UserDetailsService 인터페이스는 데이터베이스에서 회원 정보를 가져오는 역할을 담당 * * loadUserByUsername() 메소드가 존재하며, 회원정보를 조회하여 사용자의 정보와 권한을 * 갖는 UserDetails 인터페이스를 반환 * * * * * */ @Service @Transactional @RequiredArgsConstructor public class MemberService implements UserDetailsService { private final MemberRepository memberRepository; public Member saveMember(Member member){ validateDuplicateMember(member); return memberRepository.save(member); } private void validateDuplicateMember(Member member){ Member findMember=memberRepository.findByEmail(member.getEmail()); if(findMember!=null){ throw new IllegalStateException("이미 가입된 회원입니다."); } } /** * UserDetailsService 인터페이스의 loadUserByUsername() 메소드를 오버라이딩합니다. 로그인할 유저의 email을 파라미터로 전달받습니다. UserDetail을 구현하고 있는 User 객체를 반환해줍니다. User 객체를 생성하기 위해서 생성자로 회원의 이메일, 비밀번호, role 을 파라미터로 넘겨 줍니다. * */ @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { Member member=memberRepository.findByEmail(email); if(member==null){ throw new UsernameNotFoundException(email); } UserDetails result= User.builder() .username(member.getEmail()) .password(member.getPassword()) .roles(member.getRole().toString()) .build(); System.out.println(" UserDetail 정보 :" + result.toString()); // UserDetail 정보 :org.springframework.security.core.userdetails.User // [Username=test@gmail.com, // Password=[PROTECTED], Enabled=true, // AccountNonExpired=true, credentialsNonExpired=true, // AccountNonLocked=true, Granted Authorities=[ROLE_USER]] return result; } }
8.Controller
MemberController
import com.macaronics.constant.Role; import com.macaronics.dto.MemberFormDto; import com.macaronics.entity.Member; import com.macaronics.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import javax.validation.Valid; @RequestMapping("/members") @Controller @RequiredArgsConstructor public class MemberController { private final MemberService memberService; private final PasswordEncoder passwordEncoder; @GetMapping(value = "/join") public String memberForm(Model model){ model.addAttribute("memberFormDto", new MemberFormDto()); return "member/memberForm"; } @PostMapping(value = "/join") public String memberForm(@Valid MemberFormDto memberFormDto, BindingResult bindingResult, Model model){ if(bindingResult.hasErrors()){ return "member/memberForm"; } try{ Member member=Member.createMember(memberFormDto, passwordEncoder, Role.USER); memberService.saveMember(member); }catch (IllegalStateException e){ model.addAttribute("errorMessage", e.getMessage()); return "member/memberForm"; } return "redirect:/"; } @GetMapping(value = "/login") public String loginMember(){ return "/member/memberLoginForm"; } @GetMapping(value = "/login/error") public String loginError(Model model){ model.addAttribute("loginErrorMsg", "아이디 또는 비밀번호를 확인해 주세요."); return "/member/memberLoginForm"; } }
9. View
header.html
http://www.thymeleaf.org/extras/spring-security
<!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 bg-dark color-white"> <ul class="nav nav-dark"> <li class="nav-item"> <a class="nav-link" href="/">홈</a> </li> <li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')"> <a class="nav-link" href="/admin/">관리자페이지</a> </li> <li class="nav-item" sec:authorize="isAuthenticated()"> <a class="nav-link" href="/board">게시판</a> </li> <li class="nav-item" sec:authorize="isAnonymous()"> <a class="nav-link" href="/members/login">로그인</a> </li> <li class="nav-item" sec:authorize="isAnonymous()"> <a class="nav-link" href="/members/join">회원가입</a> </li> <li class="nav-item" sec:authorize="isAuthenticated()"> <a class="nav-link" href="/members/logout">로그아웃</a> </li> </ul> </nav> </div> </html>
<div sec:authorize="isAuthenticated()"> 이 콘텐츠는 인증된 사용자에게만 표시됩니다. </div> <div sec:authorize="hasRole('ROLE_ADMIN')"> 이 콘텐츠는 관리자에게만 표시됩니다. </div> <div sec:authorize="hasRole('ROLE_USER')"> 이 콘텐츠는 사용자에게만 표시됩니다. </div>
The sec:authentication 기록된 사용자 이름과 역할을 표시하는 데 사용됩:
Logged user: <span sec:authentication="name">Bob</span> Roles: <span sec:authentication="principal.authorities">[ROLE_USER, ROLE_ADMIN]</span>
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="script"> <script> $(document).ready(function(){ var errorMessage='[[${errorMessage}]]'; if(errorMessage!=null){ console.log(errorMessage); } }); </script> </th:block> <div layout:fragment="content"> <form action="/members/join" role="form" method="post" th:object="${memberFormDto}"> <div class="form-group"> <p class="fieldError">[[${errorMessage}]]</p> </div> <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="text" 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">전송</button> </div> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> </form> </div> </html>
memberLoginForm.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> .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> <button class="btn btn-primary">로그인</button> <button type="button" class="btn btn-primary" onclick="location.href='/members/join'">회원가입</button> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> </form> </div> </html>
10.컨트롤에서 세션을 어떻게 찾는지?
@GetMapping(value={"", "/"}) public String index(Principal principal) { //컨트롤에서 세션을 어떻게 찾는지? if(principal!=null) { System.out.println("로그인 사용자 아이디 : " +principal.getName()); } return "index"; }
소스 :
https://github.com/braverokmc79/spring-boot-jpa-member-join
참조:
Spring Security 회원가입 / 로그인 구현
https://blog.javabom.com/minhee/session/spring-security-1/spring-security
[Spring Boot] 간단한 로그인 기능 구현 - Spring Security
https://hooongs.tistory.com/233
백견불여일타 스프링 부트 쇼핑몰 프로젝트 with JPA
댓글 ( 4)
댓글 남기기