이전 포스팅에서 security 기본 개념에 대해 알아봤습니다.
Spring Security에 대해 궁금하다면 아래 포스팅을 참고하면 될 것 같습니다.
예전에는 회원가입 및 로그인을 구현할 때 전통적인 방식으로 구현했지만 요즘은 사용하지 않는 추세이고, Spring Security를 사용합니다.
오늘은 Security를 사용해서 회원가입과 로그인 기능 구현해 봅시다.
gradle
implementation 'org.springframework.boot:spring-boot-starter-mustache'
implementation 'org.springframework.boot:spring-boot-starter-security'
템플릿으로는 머스테치를 사용하겠습니다.
mustache 템플릿 문법은 다른 템플릿 엔진보다 쉽고, View의 역할과 서버의 역할이 명확하게 구분할 수 있습니다. mustache 템플릿에 대한 포스팅은 나중에 진행할 예정입니다.
MemberDTO
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MemberDTO {
private Long idx; // 번호
private Long num; // rownum
private String userid;
private String pwd;
private String username; // 이름
private String userphone; // 전화번호
private String useraddr; //주소
private String zipcode;
private String basicaddr;
private String isEnabled; // 사용 가능 여부
private Integer failCnt; // 실패 횟수
private String usergrade; // 사용자 등급 권한 > user - manager - admin
private String delete_yn; // 삭제 여부
private String create_user; // 생성자
private Object create_date; // 생성일자
private String update_user; //수정자
private Object update_date; //수정일자
}
SecurityConfig
Spring Security에서 SecurityFilterChain을 생성해서 @Bean 등록해 줍니다.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig{
@Autowired
private PrincipalDetailsService principalDetails;
// security 로그인 실패 핸들러 DI
@Autowired
private AuthenticationFailureHandler customFailHandler;
@Autowired
private BCryptPasswordEncoder encodePwd;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
return http
.csrf() // csrf 선언 = 어디에 쓸꺼야 ?
.ignoringAntMatchers("/uploadSummernoteImageFile")
.ignoringAntMatchers("/user/**")
.ignoringAntMatchers("/auth/**")
.ignoringAntMatchers("/admin/**")
// csrf token Post 403 error Exception filter
.and()
.authorizeRequests()
.antMatchers("/include/**","/temcss/**","temjs/**").permitAll()
.antMatchers("/").authenticated()
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.antMatchers("/manager/**")
.access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin()// 나는 폼 로그인을 사용할거야 !
.loginPage("/auth/loginForm")
// 근데 난 로그인 폼이 있어 이 경로야 !
.usernameParameter("userid")
// 근데 난 username이라고 선언 안해. 별도로 name 선언 했어
.passwordParameter("pwd")
// 동일해
.loginProcessingUrl("/auth/login") // security login
// 로그인 요청 들어왔어 !! true / false ?
.failureHandler(customFailHandler)
// login Fail // false
.defaultSuccessUrl("/") // true
.and()
.logout()
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
// HTTP session reset
.and()
.build();
}
- @EnableWebSecurity
- @Configuration에 @EnableWebSecurity를 추가해 Spring Security 설정 클래스임을 알려줍니다.
- @EnableGlobalMethodSecurity(prePostEnabled = true)
- 특정 주소로 접근하면 권한 및 인증을 미리 체크하기 위해 사용
- BCryptPasswordEncoder
- BCryptPasswordEncoder는 Spring Security에서 제공하는 비밀번호 암호화입니다.
- SecurityFilterChain filterChain(HttpSecurity http)
- Security에서 모든 인증처리가 진행되며, HttpSecurity를 통해 인증, 인가, CSRF 설정, 세션 관리, 로그아웃 설정, HTTP 요청 및 응답 필터를 설정합니다. Spring Security의 핵심 구성 요소 중 하나로, 웹 애플리케이션의 보안 설정을 정의하는 데 중요한 역할을 합니다.
- .csrf().ignoringAntMatchers()
- Spring Security에서는 csrf token 없이 요청하면 해당 요청을 막기 때문에 경로 설정을 했습니다.
- .authorizeRequests()
- HttpServletRequest에 따라 접근을 제한합니다.
- antMatchers() 메서드로 특정 경로를 지정하며, permitAll(),hasRole() 메소드로 권한에 따른 접근 설정을 합니다.
- .anyRequest().authenticated()
- 그 외의 경로는 인증된 사용자만이 접근 가능하도록 설정합니다.
- formLogin()
- form 기반으로 인증합니다.
- "/login" 경로로 접근하면, Spring Security에서 제공하는 로그인 Form을 사용할 수 있습니다.
- .loginPage("/auth/loginForm")
- 기본으로 제공되는 form을 사용하지 않고, 커스텀 로그인 폼을 사용하기 위해 loginPage() 메서드를 사용했습니다.
- .usernameParameter("userid")
- Spring Security에서는 네임 법칙이 있다 보니 username이 default 입니다. 따라서 별도로 name을 userid로 지정했습니다. 패스워드도 동일합니다.
- .loginProcessingUrl("/auth/login")
- Spring Security가 해당 주소로 오는 요청을 낚아채서 수행합니다. 즉, 인터셉터한다고 보면 됩니다.
- .failureHandler(customFailHandler)
- 로그인 요청이 들어와서 조회해 본 결과 값이 false 일 때, 실패했을 때 처리해야 하는 핸들러가 있어야 합니다. 이 부분은 별도로 포스팅할 예정입니다.
- .defaultSuccessUrl("/")
- 로그인 성공 시 이동되는 Path 값입니다.
- logout()
- SecurityFilterChain을 사용하면 자동으로 적용되는 로그아웃 메서드이며, 기본적으로 "/logout"에 접근하면 HTTP Session을 제거해 줍니다. 저는 사용하지 않지만, 제공되는 기능이므로 명시적으로 작성했습니다.
- .logoutSuccessUrl("/")
- 로그아웃 성공 시 이동되는 페이지입니다.
- .invalidateHttpSession(true)
- HTTP Session을 초기화하는 작업입니다.
Session 정보를 저장하는 DTO 클래스 생성
@Getter
public class MemberSessionDTO implements Serializable {
private Long idx;
private String userid;
private String username;
private String pwd;
private String usergrade;
// Entity -> DTO
public MemberSessionDTO(MemberDTO dto) {
this.idx = dto.getIdx();
this.userid =dto.getUserid();
this.pwd = dto.getPwd();
this.username = dto.getUsername();
this.usergrade = dto.getUsergrade();
}
}
MemberSessionDTO의 역할은 인증된 사용자 정보를 Session에 저장하기 위한 클래스입니다.
Entity 클래스에 직접 Session을 저장하려면 직렬화를 해야 하는데, Entity 클래스에 직렬화를 해준다면 추후에 다른 Entity와 연관관계를 맺을 시 직렬화 대상에 다른 Entity까지 포함될 수 있어 성능 이슈, 부수 효과 우려가 있습니다.
UserDetails , UserDetailsService 클래스 생성
@Data
public class PrincipalDetails implements UserDetails {
private MemberDTO user;
public PrincipalDetails(MemberDTO user) {
System.out.println("Details user data = " + user.toString());
this.user=user;
}
// 해당 member 권한을 리턴하는 곳
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>(); // 부모타입 collection 사용
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getUsergrade(); // object type -> String 변환
}
});
System.out.println("collect = " + collect);
return collect;
}
@Override
public String getPassword() {
System.out.println("user.getpwd = " + user.getPwd());
return user.getPwd();
}
@Override
public String getUsername() {
System.out.println("user.getuserid = " + user.getUserid());
return user.getUserid();
}
// 계정 만료 여부
// true : 만료 안됨
// false : 만료
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정 잠김 여부
// true : 잠기지 않음
// false : 잠김
@Override
public boolean isAccountNonLocked() {
return true;
}
// 비밀번호 만료 여부
// true : 만료 안됨
// false : 만료
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 사용자 활성화 여부
// true : 만료 안됨
// false : 만료
@Override
public boolean isEnabled() {
return true;
}
}
UserDetails 추상메서드를 들고 있는 UserDetails를 @Override 해서 사용할 수 있습니다. 각 용도는 주석을 참고해 주시면 될 거 같습니다.
Spring Security가 로그인 요청을 가로채서 로그인을 진행-완료되면 UserDetails 타입의 오브젝트를 Spring Security의 고유한 세션저장소에 저장을 해줍니다. 즉, UserDetails 타입의 PrincipalDetails에 저장이 됩니다. 물론 user 객체도 포함되어야 합니다.
@RequiredArgsConstructor
@Service
public class PrincipalDetailsService implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(MemberController.class);
private final MemberRepository memberRepository;
private final HttpSession session;
// security session(내부 Authentication(UserDetails))
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("loadUserByUsername 진입 완료");
System.out.println("userid = " + username);
// Map<String, Object> userCheck = userData(username);
Map<String, Object> param = new HashMap<>();
param.put("userid", username);
param.put("failCntCheck", true);
logger.info("맵 = {}" , param);
MemberDTO user = memberRepository.findByUserName(param);
if (user == null) {
throw new UsernameNotFoundException("해당 사용자가 존재하지 않습니다. : " + username);
}
session.setAttribute("user", new MemberSessionDTO(user));
return new PrincipalDetails(user);
}
UserDetailsService는 DaoAuthenticationProvider와 협력하는 인터페이스입니다.
DaoAuthenticationProvider는 요청받은 유저의 ID, Password와 저장된 ID, Password의 검증하는 책임을 갖고 있는데요. 그래서 저장된 데이터를 갖고 오기 위해 UserDetailsService와 협력합니다.
즉, DB에 있는지 확인을 하고 Security Session에 유저 정보를 저장하기 위해 "return UserDetails(user)" 해줍니다.
Service
@Service
@RequiredArgsConstructor
public class MemberService {
private static final Logger logger = LoggerFactory.getLogger(MemberService.class);
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final MemberRepository memberRepository;
@Transactional
public void save(MemberDTO dto) {
String match = "[^0-9]";
String cleanMatch = dto.getUserphone().replaceAll(match, "");
dto.setUserphone(cleanMatch);
System.out.println("전화번호: " + dto.getUserphone());
dto.setPwd(bCryptPasswordEncoder.encode(dto.getPwd()));
memberRepository.save(dto);
}
회원가입 전에 추가 구성으로 save() 메서드를 구현하고 사용자 비밀번호를 해쉬 암호화 후 repository에 저장합니다.
"String match = "[^0-9]";"
match의 역할은 핸드폰 번호가 "010-1234-5678" 번호가 들어오면 중간에 있는 하이픈(-)을 제거하고 DB에 저장하기 위한 정규표현식입니다.
Repository , Mapper
@Mapper
public interface MemberRepository {
MemberDTO findByUserName(Map<String, Object> param);
void save(MemberDTO dto);
<mapper namespace="com.example.task.boradTable.domain.MemberRepository">
<select id="findByUserName" parameterType="hashMap" resultType="com.example.task.boradTable.web.dto.MemberDTO">
SELECT
*
FROM
userinfo_tbl
WHERE
USERID = #{userid}
and DELETE_YN = 'Y'
<if test="failCntCheck">
<![CDATA[ and failCnt < 5 ]]>
</if>
</select>
<insert id="save">
INSERT
INTO userinfo_tbl
(IDX,USERID,PWD,USERNAME,USERPHONE,ZIPCODE,BASICADDR,USERADDR)
VALUES
(nextval(user_sq),#{userid},#{pwd},#{username},#{userphone},#{zipcode},#{basicaddr},#{useraddr})
</insert>
</mapper>
Controller
@Controller
@RequiredArgsConstructor
@Log4j2
public class IndexController {
private static final Logger logger = LoggerFactory.getLogger(MemberController.class);
private final MemberService memberservice;
private final BoardService boardservice;
private final CheckUserIdValidator checkUserIdValidator;
@InitBinder // 바인딩, 검증
public void validatorBinder(WebDataBinder binder) {
binder.addValidators(checkUserIdValidator);
}
// localhost8090/
// localhost8090
@GetMapping({ "", "/" })
public String index(Model model, @LoginUser MemberSessionDTO user) {
logger.info("Controller session userid ={}", user.getUsername());
if (user != null) {
memberservice.resetCnt(user.getUserid());
String userRole = memberservice.userRole(user.getUserid());
Stream<String> checkRole = Arrays.stream(userRole.split(","));
List<String> userRoleCheckFiler = checkRole
.filter(role -> "ROLE_ADMIN".equals(user.getUsergrade()) || "ROLE_MANAGER".equals(user.getUsergrade()))
.collect(Collectors.toList());
logger.info("userRoleCheck = {}" , userRoleCheckFiler);
logger.info("user id session = {}",user.getUserid());
List<MemberDTO> memberLists = memberservice.selectList();
List<MemberDTO> filterMembers = memberLists.stream().filter(member -> "Y".equals(member.getDelete_yn()))
.collect(Collectors.toList());
List<BoardDTO> list = boardservice.boardList();
List<BoardDTO> filterboard = list.stream().filter(board -> "Y".equals(board.getDelete_yn()))
.collect(Collectors.toList());
model.addAttribute("member", filterMembers);
model.addAttribute("boardList", filterboard);
model.addAttribute("role", userRoleCheckFiler);
model.addAttribute("user", user.getUserid());
}
return "index";
}
@GetMapping("/auth/loginForm")
public String loginForm(@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "exception", required = false) String exception,
Model model,
@LoginUser MemberSessionDTO user) {
model.addAttribute("error", error);
model.addAttribute("exception", exception);
if(user != null) {
model.addAttribute("user", user.getUsername());
}
return "loginForm";
}
@GetMapping("/auth/joinForm")
public String join(Model model, MemberDTO dto) {
Long seq = memberservice.maxseq();
dto.setIdx(seq);
System.out.println(dto.getIdx());
model.addAttribute("idx", seq);
return "join";
}
@PostMapping("/auth/join")
public String join(@Valid MemberDTO dto, Errors errors, Model model, HttpServletResponse response)
throws Exception {
logger.info("dto = {}", dto);
if (errors.hasErrors()) {
// 회원가입 실패 시 데이터 유지
model.addAttribute("memberDto", dto);
// 유효성 통과 못한 필드, 메시지 핸들링
Map<String, String> validatorResult = memberservice.validateHandling(errors);
for (String key : validatorResult.keySet()) {
model.addAttribute(key, validatorResult.get(key));
}
return "join";
}
memberservice.save(dto);
ScriptUtils.alertAndMovePage(response, "회원가입 성공! 로그인 페이지로 이동합니다.", "/auth/loginForm");
return "";
}
"/auth/**" 경로에 대한 권한 없이 접근 가능하도록 설정해 뒀기에 각 url에 /auth/를 붙여주었습니다.
Controller에 불러오는 데이터가 많은데, 지금은 경로만 확인해 보시면 될 거 같습니다.
// MemberSessionDTO user
if(user != null) {
model.addAttribute("user", user.getUsername());
}
PrincipalDetailsService에서 세션 정보를 저장하고 IndexController 클래스에 가져와 각각 model에 담았습니다.
Mustache(View)
[ header.mustache ]
<link rel="stylesheet" href="/temcss/user.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="/include/css/bootstrap.css">
<link rel="stylesheet" href="/include/style.css">
<div class="maintitle" style="height: 200px; padding-top: 50px;" align="center">
<h6 class="text-while" style="font-size: 60px;">커뮤니티 사이트</h6>
</div>
<nav class="navbar navbar-expand-sm navbar-inverse navbar-fixed-top navbar-dark">
<div class="container">
{{#user}}
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav" style="width: 100%;">
{{#role}}
<li class="nav-item">
<a class="nav-link" href="#" onclick="memberList()">사용자 목록</a>
</li>
{{/role}}
<li class="nav-item">
<a class="nav-link" href="#" onclick="boardList()">게시글 목록</a>
</li>
</ul>
<div class="sessionmenu" style="width: 100%;">
<ul class="navbar-nav" style="float: right;">
<li style="margin-top: 10px;">
<span class="navbar-text me-3">{{user}}님 안녕하세요!</span>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout">Logout</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/">home</a>
</li>
</ul>
</div>
{{/user}}
{{^user}}
<ul class="nav navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/auth/loginForm">Login</a>
</li>
</ul>
</div>
{{/user}}
</div>
</nav>
[ footer.mustache ]
<script src="/include/jquery-3.4.1.min.js"></script>
<script src="/temjs/user.js" ></script>
<script src="/temjs/board.js" ></script>
<script src="/include/js/bootstrap.js"></script>
[ loginForm.mustache ]
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<link rel="stylesheet" href="/temcss/user.css">
<link rel="stylesheet" href="/include/css/bootstrap.css">
<link rel="stylesheet" href="/include/style.css">
<body>
<div id="posts_list">
<div class="container">
<table border="3" class="mainLogo">
<tr>
<td>
<h1>시큐리티 게시판</h1>
</td>
</tr>
<tr>
<td>
<h3>홍길동</h3>
</td>
</tr>
</table>
</div>
<br>
<div class="container col-md-6">
<form id="form1" name="form1" method="POST" >
<input type="hidden" name="_csrf" value="{{_csrf.token}}"/>
<div class="form-group">
<label>ID</label>
<input class="form-control" id="userid" name="userid" maxlength="16" onkeydown="noSpaceForm(this);" placeholder="ID를 입력해주세요." >
<span id="useridError" class="error" style="color: red;"></span>
</div>
<br>
<div class="form-group">
<label>PASS WORD</label>
<input type="password" class="form-control" id="pwd" name="pwd" maxlength="16" onkeydown="noSpaceForm(this);" placeholder="Pass Word를 입력해주세요." onkeyup="pwdenterkey()">
<span id="pwdError" class="error" style="color: red;"></span>
</div>
<span>
{{#error}}
<p id="valid" class="alert alert-danger">{{exception}}</p>
{{/error}}
</span>
<br>
<button type="button" class="form-control btn btn-primary bi bi-lock-fill" onclick="logincheck()">Login</button>
<br>
<div>
<p>회원이 아니세요? <a href="/auth/joinForm">회원가입하기</a></p>
</div>
</form>
</div>
</div>
{{>layout/footer}}
</body>
</html>
[ join.mustache ]
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>JOIN</title>
</head>
<body>
{{>layout/header}}
<div id="posts_list">
<div class="container col-md-8">
<form method="post" modelAttribute="memberDto" name="userJoin" id="userJoin">
<!-- mustache는 csrf 토큰을 제공해주지 않는다. yml 파일에 설정 함. -->
<input type="hidden" name="_csrf" value="{{_csrf.token}}"/>
<div class="form-group">
{{#idx}}
<label>번호</label>
<input type="text" name="idx" id="idx" value="{{idx}}" class="form-control" readonly/>
{{/idx}}
</div>
<div class="form-group">
<label>아이디</label>
<input type="text" name="userid" id="userid" value="{{#memberDto}}{{memberDto.userid}}{{/memberDto}}" onkeydown="noSpaceForm(this);" maxlength="16" class="form-control" placeholder="아이디를 입력해주세요"/>
{{#valid_userid}} <span id="valid">{{valid_userid}}</span> {{/valid_userid}}
</div>
<div class="form-group">
<label>비밀번호</label>
<input type="password" name="pwd" id="pwd" value="{{#memberDto}}{{memberDto.pwd}}{{/memberDto}}" onkeydown="noSpaceForm(this);" maxlength="16" class="form-control" placeholder="비밀번호를 입력해주세요"/>
{{#valid_pwd}} <span id="valid">{{valid_pwd}}</span> {{/valid_pwd}}
</div>
<div class="form-group">
<label>이름</label>
<input type="text" name="username" id="username" value="{{#memberDto}}{{memberDto.username}}{{/memberDto}}" onkeydown="noSpaceForm(this);" maxlength="6" class="form-control" placeholder="이름을 입력해주세요"/>
{{#valid_username}} <span id="valid">{{valid_username}}</span> {{/valid_username}}
</div>
<div class="form-group">
<label>전화번호</label>
<input type="text" name="userphone" id="userphone" value="{{#memberDto}}{{memberDto.userphone}}{{/memberDto}}" onkeydown="noSpaceForm(this);" maxlength="13" class="form-control" placeholder="전화번호는 하이푼(-) 포함하여 입력해주세요"/>
{{#valid_userphone}} <span id="valid">{{valid_userphone}}</span> {{/valid_userphone}}
</div>
<div class="form-group">
<label>우편번호</label><br>
<input type="text" name="zipcode" id="zipcode" value="{{#memberDto}}{{memberDto.zipcode}}{{/memberDto}}" placeholder="우편번호 찾기" readonly>
<button type="button" class=" btn-outline-success" onclick="daumZipcode()">ZipCode</button>
</div>
<div class="form-group">
<label>일반주소</label><br>
<input type="text" name="basicaddr" id="basicaddr" value="{{#memberDto}}{{memberDto.basicaddr}}{{/memberDto}}" readonly>
</div>
<div class="form-group">
<label>상세주소</label>
<input type="text" name="useraddr" id="useraddr" value="{{#memberDto}}{{memberDto.useraddr}}{{/memberDto}}" maxlength="50" class="form-control" placeholder="주소를 입력해주세요"/>
{{#valid_useraddr}} <span id="valid">{{valid_useraddr}}</span> {{/valid_useraddr}}
</div>
<br>
<br>
<div style=" margin-left: 40%;">
<button type="button" class="btn btn-primary bi bi-person" onclick="joinJs()"> 가입</button>
<button type="button" class="btn btn-info bi bi-arrow-return-left" onclick="loginForm()">뒤로가기</button>
</div>
</form>
</div>
</div>
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
{{>layout/footer}}
</body>
</html>
[ index.mustache ]
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
{{>layout/header}}
<div id="result">
<div class="container"
style="padding-left: 50px; padding-right: 50px;">
<div id="result"
style="display: flex; width: 100%; flex-direction: column;">
<div class="content">
<div class="header"
style="display: flex; height: auto; padding-top: 50px;">
<div class="container" style="flex-basis: 50%;">
<div class="container">
<div
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h3>회원리스트</h3>
<button class="btn" type="button" onclick="memberList()"
style="float: right;">
<i class="bi bi-plus-lg"></i>
</button>
</div>
<table class="table table-hover">
<thead>
<tr>
<th>번호</th>
<th>이름</th>
<th>우편번호</th>
<th>가입일자</th>
</tr>
</thead>
<tbody>
{{#member}}
<tr onclick="detail({{idx}})">
<td>{{num}}</td>
<td>
<input type="hidden" id="idx" name="idx" value="{{idx}}">
{{username}}</td>
<td>{{zipcode}}</td>
<td>{{create_date}}</td>
</tr>
{{/member}}
</tbody>
</table>
</div>
</div>
<div class="container" style="flex-basis: 50%;">
<div class="container">
<div
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h3>자유게시판</h3>
<button class="btn" type="button" onclick="boardList()"
style="float: right;">
<i class="bi bi-plus-lg"></i>
</button>
</div>
<table class="table table-hover">
<thead>
<tr>
<th style="width: 10%;">번호</th>
<th style="width: 20%;">작성자</th>
<th style="width: 50%;">제목</th>
<th style="width: 20%;">작성일</th>
</tr>
</thead>
<tbody>
{{#boardList}}
<tr onclick="boarddetail({{idx}})">
<td>
{{num}}
<input type="hidden" id="idx" name="idx" value="{{idx}}">
</td>
<td>{{create_user}}</td>
<td>{{subject}}</td>
<td>{{create_date}}
</tr>
{{/boardList}}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{>layout/footer}}
</body>
</html>
Java Script
function logincheck() {
const userid = $('#userid').val();
const pwd = $('#pwd').val();
if(userid == ""){
//alert("ID를 입력해주세요.");
$('#useridError').text('원활한 진행을 위해 ID를 입력해주세요..!!');
$('#userid').focus();
return false;
}else{
$('#useridError').text('');
}
if(pwd == ""){
$('#pwdError').text('회원님 정보 조회를 위한 Password를 입력해주세요..!!');
$('#pwd').focus();
return false;
}else {
$('#pwdError').text('');
}
document.form1.action="/auth/login";
document.form1.submit();
}
function pwdenterkey() {
if (window.event.keyCode == 13) {
if(userid == ""){
//alert("ID를 입력해주세요.");
$('#useridError').text('원활한 진행을 위해 ID를 입력해주세요..!!');
$('#userid').focus();
return false;
}else{
$('#useridError').text('');
}
if(pwd == ""){
$('#pwdError').text('회원님 정보 조회를 위한 Password를 입력해주세요..!!');
$('#pwd').focus();
return false;
}else {
$('#pwdError').text('');
}
document.form1.action="/auth/login";
document.form1.submit();
}
}
function joinJs() {
const userid = $('#userid').val();
if (userid == "") {
alert("아이디는 필수 입력해주세요.");
$('#userid').focus();
return;
}
const pwd = $('#pwd').val();
if (pwd == "") {
alert("비밀번호를 입력해주세요.");
$('#pwd').focus();
return;
}
const username = $('#username').val();
if (username == "") {
alert("이름을 입력해주세요.");
$('#username').focus();
return;
}
const userphone = $('#userphone').val();
if (userphone == "") {
alert("전화번호를 입력해주세요.");
$('#userphone').focus();
return;
}
const useraddr = $('#useraddr').val();
if (useraddr == "") {
alert("주소를 입력해주세요.");
$('#useraddr').focus();
return;
}
const zipcode = $('#zipcode').val();
if (zipcode == "") {
alert("ZipCode 버튼을 눌러서 주소 조회를 해주세요.");
$('#zipcode').focus();
return;
}
/* const usergrade = $('#usergrade').val();
if (usergrade == "usergrade") {
alert("등급을 설정해주세요.");
$('#usergrade').focus();
return;
}*/
console.log("userid = " + userid);
console.log("pwd = " + pwd);
console.log("custname = " + username);
console.log("phone = " + userphone);
console.log("address = " + useraddr);
/* console.log("grade = " + usergrade);*/
document.userJoin.action = "/auth/join";
document.userJoin.submit();
}
function daumZipcode() {
new daum.Postcode({
oncomplete: function(data) {
var addr = '';
var extraAddr = '';
if (data.userSelectedType === 'R') {
addr = data.roadAddress;
} else {
addr = data.jibunAddress;
}
if(data.userSelectedType === 'R'){
if(data.bname !== '' && /[동|로|가]$/g.test(data.bname)){
extraAddr += data.bname;
}
if(data.buildingName !== '' && data.apartment === 'Y'){
extraAddr += (extraAddr !== '' ? ', ' + data.buildingName : data.buildingName);
}
if(extraAddr !== ''){
extraAddr = ' (' + extraAddr + ')';
}
document.getElementById("basicaddr").value = extraAddr;
} else {
document.getElementById("useraddr").value = '';
}
document.getElementById('zipcode').value = data.zonecode;
document.getElementById("basicaddr").value = addr;
document.getElementById("useraddr").focus();
}
}).open();
}
마치며
오늘은 Spring Security를 활용해서 로그인 및 회원가입 기능 구현에 대해 적어봤습니다.
다음 포스팅에서 뵙겠습니다.
'[ JAVA ] > JAVA Spring Security' 카테고리의 다른 글
[ Spring Boot ] Spring Security - Login Failure Handler Custom (1) | 2023.10.05 |
---|---|
[ Spring Boot ] Spring Security - 회원가입 Validation Check (2) | 2023.10.04 |
[ Spring Boot ] Spring Security - 기본 개념 및 예제 (0) | 2023.09.25 |
[ Spring Boot ] OAuth2 - NAVER 소셜 로그인 (0) | 2023.06.04 |
[ Spring Boot ] OAuth2 - Google 소셜 로그인 (1) | 2023.06.02 |