728x90
반응형
Config
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final AuthenticationSuccess authenticationSuccess;
private final AuthenticationFailure authenticationFailure;
private final LogoutExecute logoutExecute;
private final LogoutSuccess logoutSuccess;
private final UserDetailsService userDetailsService;
private final AuthenticationEntryException authenticationEntryException;
private final AccessDeniedHandlerException accessDeniedHandlerException;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
;
http.formLogin() // 로그인
.loginPage("/member/login")
.defaultSuccessUrl("/")
.failureUrl("/member/login?error=true")
.usernameParameter("userId")
.passwordParameter("password")
.loginProcessingUrl("/member/login")
.successHandler(authenticationSuccess)
.failureHandler(authenticationFailure)
.permitAll()
;
http.logout() // 로그아웃
.logoutUrl("/member/logout") // default post
.logoutSuccessUrl("/")
.invalidateHttpSession(true) // 세션 무효화
.deleteCookies("JSESSIONID")
.addLogoutHandler(logoutExecute)
.logoutSuccessHandler(logoutSuccess)
;
http.rememberMe() // 사용자 저장
.rememberMeParameter("idMaintain") // default 파라미터는 remember-me
.tokenValiditySeconds(604800) // 7일로 설정(default 14일)
.alwaysRemember(false)
.userDetailsService(userDetailsService)
;
http.sessionManagement()
.maximumSessions(1) // -1 무제한
.maxSessionsPreventsLogin(false) // true:로그인 제한 false(default):기존 세션 만료
.expiredUrl("/member/login") // 세션 만료
;
http.sessionManagement()
.sessionFixation().changeSessionId() // default 세션 공격 보호
;
http.exceptionHandling() // Exception 처리
.authenticationEntryPoint(authenticationEntryException) // 인증 예외
.accessDeniedHandler(accessDeniedHandlerException) // 인가 예외
;
return http.build();
}
/**
* 정적 자원 및 루트 페이지 ignore
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
.antMatchers("/", "/img/**", "/lib/**", "/member/**");
}
@Bean
public static PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
Controller
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/member")
@Controller
public class MemberController {
private final MemberService memberService;
@GetMapping("/login")
public String loginForm(@RequestParam(required = false) String error, Model model){
model.addAttribute("user", new LoginForm());
return "member/login";
}
@PostMapping("/login")
public String login(@ModelAttribute("login") @Valid LoginForm loginForm, BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request,
Model model){
if(bindingResult.hasErrors()) return "member/login";
Member loginMember = memberService.login(loginForm.getUserId(), loginForm.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail","아이디 또는 비밀번호가 맞지 않습니다.");
return "member/login";
}
model.addAttribute("user",loginForm);
return "redirect:" + redirectURL;
}
@GetMapping("/signup")
public String signupForm(Model model){
model.addAttribute("user", new MemberJoinForm());
return "member/signup";
}
@PostMapping("/signup")
public String signup(@Valid @ModelAttribute("user") MemberJoinForm memberJoinForm, BindingResult bindingResult){
log.info("user = {}",memberJoinForm.getUserId());
if(!memberJoinForm.getUserId().equals(memberJoinForm.getConfirmId())){ // 중복 확인 아이디와 입력 아이디가 다름
bindingResult.reject("notConfirm");
}
if(!memberJoinForm.getPassword().equals(memberJoinForm.getRePassword())){ // 비밀번호와 비밀번호 확인 값이 다름
bindingResult.reject("notPassword");
}
if(bindingResult.hasErrors()) {
log.info("bindingResult = {} " , bindingResult);
return "member/signup";
}
// 회원가입
memberService.memberJoin(memberJoinForm);
return "redirect:/member/login";
}
/**
* 아이디 중복 체크
*/
@ResponseBody
@GetMapping("/api/{userChkId}/exists")
public ResponseEntity<Boolean> checkUserIdDuplicate(@PathVariable String userChkId){
log.info("userChkId == {}", userChkId);
return ResponseEntity.ok(memberService.checkUserIdDuplicate(userChkId));
}
}
Service
@Slf4j
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
/**
* 회원가입
* @param member
* @return
*/
public String memberJoin(MemberJoinForm member) {
Member memberEntity = new Member();
memberEntity.memberJoin(member.getUserId(), member.getUserName(), passwordEncoder.encode(member.getPassword()), member.getEmail(), Role.USER);
memberRepository.save(memberEntity);
return memberEntity.getUserId();
}
/**
* 아이디 중복 체크
*/
public boolean checkUserIdDuplicate(String userChkId) {
return memberRepository.existsByUserId(userChkId);
}
/**
* @return null이면 로그인 실패
*/
public Member login(String userId, String password) {
return memberRepository.findByUserId(userId)
.filter(m -> passwordEncoder.matches(m.getPassword(),password))
.orElse(null);
}
}
signup.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/layout}">
<head>
<meta charset="utf-8">
<title>회원가입</title>
<link href="/css/signup.css" rel="stylesheet">
</head>
<div layout:fragment="content">
<script th:inline="javascript">
const log = console.log;
/**
* 아이디 중복 체크
*/
function idOverlapChked() {
let userId = document.getElementById('userId').value;
let url = `/member/api/${userId}/exists`;
if(userId === null || userId === ""){
document.getElementById("idOverlapMessage").innerHTML = '아이디를 입력해주세요.';
return;
}
ajax("GET", url, userId);
}
function ajax(method, url, data) {
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // 완료
if (xhr.status === 200) { // success
if(method === "GET"){ // 아이디 중복 확인
let template;
if(xhr.response === "true"){
template = '사용 불가능한 아이디입니다.';
}else{
template = '사용 가능한 아이디입니다.';
// document.getElementById("confirmId").innerText = userId;
document.getElementById("confirmId").value = data;
}
document.getElementById("idOverlapMessage").innerHTML = template;
}else{ // 회원가입 완료
alert("회원가입이 완료 되었습니다.");
location.href = "/member/login";
}
}
}
};
if(method === "GET"){
xhr.open("GET", url, false);
xhr.send();
}else{
xhr.open('POST', url, true);
xhr.responseType = "json";
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
}
}
</script>
<section class="bg-light">
<div class="container py-4">
<div class="row align-items-center justify-content-between">
<a class="navbar-brand h1 text-center" href="/">
<span class="text-dark h4">dyShop</span> <span class="text-primary h4">회원가입</span>
</a>
</div>
<form th:action="@{/member/signup}" th:object="${user}" method="post" id="memberForm">
<div th:if="${#fields.hasGlobalErrors()}">
<p th:each="err : ${#fields.globalErrors()}" th:text="${err}">글로벌 오류 메시지</p>
</div>
<div class="form-group">
<label for="userId" class="form-label mt-4" >아이디</label><br>
<input type="text" class="form-control" id="userId" th:field="*{userId}" th:errorclass="is-invalid">
<button class="btn btn-primary" type="button" id="idOverlapChk" th:onclick="|idOverlapChked()|">중복확인</button>
<p th:if="${#fields.hasErrors('userId')}" class="is-invalid" th:errors="*{userId}">아이디 오류 삐뽀삐뽀</p>
<label class="is-valid" id="idOverlapMessage"></label>
<input type="hidden" id="confirmId" th:field="*{confirmId}"/>
<p th:if="${#fields.hasErrors('confirmId')}" class="is-invalid" th:errors="*{confirmId}">아이디 중복확인 오류 삐뽀삐뽀</p>
</div>
<div class="form-group has-success">
<label class="form-label mt-4" for="password">비밀번호</label>
<input type="password" class="form-control is-valid" id="password" th:field="*{password}" th:errorclass="is-invalid">
<p th:if="${#fields.hasErrors('password')}" class="is-invalid" th:errors="*{password}">비밀번호 오류 삐뽀삐뽀</p>
</div>
<div class="form-group has-danger">
<label class="form-label mt-4" for="inputInvalid">비밀번호 재확인</label>
<input type="password" class="form-control" id="inputInvalid" th:field="*{rePassword}" th:errorclass="is-invalid">
<p th:if="${#fields.hasErrors('rePassword')}" class="is-invalid" th:errors="*{rePassword}">비밀번호 오류 삐뽀삐뽀</p>
</div>
<div class="form-group">
<label for="exampleInputEmail4" class="form-label mt-4">이름</label>
<input type="text" class="form-control" id="exampleInputEmail4" th:field="*{userName}" th:errorclass="is-invalid">
<p th:if="${#fields.hasErrors('userName')}" class="is-invalid" th:errors="*{userName}">이름 오류 삐뽀삐뽀</p>
</div>
<div class="form-group">
<label for="sex" class="form-label mt-4">성별</label><br>
<select class="form-select" id="sex" th:field="*{sex}" th:errorclass="is-invalid">
<option value="">선택</option>
<option value="남자">남자</option>
<option value="여자">여자</option>
</select>
</div>
<p th:if="${#fields.hasErrors('sex')}" class="is-invalid" th:errors="*{sex}">성별 오류 삐뽀삐뽀</p>
<div class="form-group">
<label for="email" class="form-label mt-4">본인 확인 이메일</label>
<input type="email" class="form-control" id="email" aria-describedby="emailHelp" placeholder="선택입력" th:field="*{email}" th:errorclass="is-invalid">
</div>
<p th:if="${#fields.hasErrors('email')}" class="is-invalid" th:errors="*{email}">email 오류 삐뽀삐뽀</p>
<br>
<div class="d-grid gap-2">
<!-- <button class="btn btn-primary btn-lg" type="button" th:onclick="|joinMember()|">가입하기</button>-->
<button class="btn btn-primary btn-lg" type="submit">가입하기</button>
</div>
</form>
</div>
</section>
</div>
</html>
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/layout}">
<head>
<meta charset="utf-8">
<title>로그인</title>
<link href="/css/login.css" rel="stylesheet">
</head>
<section class="text-center text-lg-start">
<div layout:fragment="content">
<div class="card mb-3 login-1">
<div class="row g-0 d-flex align-items-center">
<div class="col-lg-4 d-none d-lg-flex">
<img src="https://mdbootstrap.com/img/new/ecommerce/vertical/004.jpg" alt="Trendy Pants and Shoes"
class="w-100 rounded-t-5 rounded-tr-lg-0 rounded-bl-lg-5" />
</div>
<div class="col-lg-8">
<div class="card-body py-5 px-md-5">
<form style="width:50%;" th:action="@{/member/login}" th:object="${user}" method="post">
<div th:if="${#fields.hasGlobalErrors()}">
<p th:each="err : ${#fields.globalErrors()}" th:text="${err}">글로벌 오류 메시지</p>
</div>
<!-- userId input -->
<div class="form-outline mb-4">
<input type="text" id="userId" class="form-control" th:field="*{userId}" />
<label class="form-label" for="userId">아이디 입력</label>
<p th:if="${#fields.hasErrors('userId')}" class="is-invalid" th:errors="*{userId}">아이디 오류 삐뽀삐뽀</p>
</div>
<!-- Password input -->
<div class="form-outline mb-4">
<input type="password" id="password" class="form-control" th:field="*{password}" />
<label class="form-label" for="password">비밀번호 입력</label>
<p th:if="${#fields.hasErrors('password')}" class="is-invalid" th:errors="*{password}">비밀번호 오류 삐뽀삐뽀</p>
</div>
<div class="row mb-4">
<div class="col d-flex justify-content-center">
<div class="form-check">
<input class="form-check-input" type="checkbox" th:field="*{idMaintain}" id="idMaintain" />
<!-- <input type="hidden" value="on" id="_idMaintain"/> -->
<label class="form-check-label" for="idMaintain"> 로그인 상태 유지 </label>
</div>
</div>
<div class="col">
<a href="#!">아이디/비밀번호 찾기</a>
</div>
</div>
<!-- Submit button -->
<button th:type="submit" class="btn btn-primary btn-block mb-4">로그인</button>
<!-- Submit button -->
<button type="button" onclick="location.href='/member/signup'" class="btn btn-primary btn-block mb-4">회원가입</button>
</form>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Section: Design Block -->
</html>
728x90
반응형
'Spring > Spring Security' 카테고리의 다른 글
[Spring Security] ajax login (0) | 2023.02.15 |
---|---|
[Spring Security] GET 로그아웃 처리 (0) | 2022.11.03 |
[Spring Secururity ERROR] 스프링 빈 순환 참조 에러 The dependencies of some of the beans i (1) | 2022.10.30 |
[Spring Security] Spring Security 설정하기 (0) | 2022.10.29 |
[Spring Security] 유저별 권한 설정 (0) | 2022.10.28 |