-> UserCotroller
@RequestMapping(value = "verify-email", method = RequestMethod.GET)
public ModelAndView getVerifyEmail(
ModelAndView modelAndView,
) {
return modelAndView;
}
-> UserEmailVerifyResult enum 생성
public enum UserEmailVerifyResult {
EXPIRED,
FAILURE,
ILLEGAL,
SUCCESS
}
-> UserEmailVerifyVo 생성
public class UserEmailVerifyVo extends UserEmailVerificationCodeEntity implements IResult<UserEmailVerifyResult> {
private UserEmailVerifyResult result;
@Override
public UserEmailVerifyResult getResult() {
return result;
}
@Override
public void setResult(UserEmailVerifyResult result) {
this.result = result;
}
} //getter, setter
-> UserController
@RequestMapping(value = "verify-email", method = RequestMethod.GET) public ModelAndView getVerifyEmail( ModelAndView modelAndView, UserEmailVerifyVo userEmailVerifyVo, @RequestParam(name="c", required = true) String code, @RequestParam(name="s", required = true) String salt ) { userEmailVerifyVo.setResult(null); userEmailVerifyVo.setIndex(0); userEmailVerifyVo.setCode(code); userEmailVerifyVo.setSalt(salt); return modelAndView; }
- emailVerificationTemplate에 parametername을 그냥 c랑 s로 해놨기 때문에 c랑 s에 해당하는 값이 code와 salt에 자동으로 들어가지 않는다.
=> @RequestParam 사용해서 수동으로 넣어줘야한다.
-> UserService verifyEmail 메서드 생성
=> 이메일 인증을 위한 메서드
public void verifyEmail(UserEmailVerifyVo userEmailVerifyVo) { if (userEmailVerifyVo.getCode() == null || userEmailVerifyVo.getSalt() == null || !userEmailVerifyVo.getCode().matches("^([0-9a-z]{128})$") || !userEmailVerifyVo.getSalt().matches("^([0-9a-z]{256})$")) { userEmailVerifyVo.setResult(UserEmailVerifyResult.ILLEGAL); return; }
- 이 정규식은 이메일을 인증할 때만 사용하기 때문에 따로 밖으로 뺴지 않는다.
-> IUserMapper 수정 및 추가
int insertUserEmailVerificationCode(UserEmailVerificationCodeEntity userEmailVerificationCodeEntity);
- 수정
UserEmailVerificationCodeEntity selectUserEmailVerificationCode ( @Param(value = "code") String code, @Param(value = "salt") String salt);
UserEntity selectUserByIndex( @Param(value = "index") int index);
- 추가
-> UserMapper.xml select추가
<select id="selectUserEmailVerificationCode"
resultType="dev.jwkim.bbsbasic.entities.UserEmailVerificationCodeEntity">
SELECT `index` AS `index`,
`created_at` AS `createdAt`,
`expires_at` AS `expiresAt`,
`expired_flag` AS `isExpired`,
`code` AS `code`,
`salt` AS `salt`,
`user_index` AS `userIndex`
FROM `spring3`.`user_email_verification_codes`
WHERE `code` = #{code}
AND `salt` = #{salt}
LIMIT 1
</select>
-> UserService
UserEmailVerificationCodeEntity userEmailVerificationCodeEntity = this.userMapper .selectUserEmailVerificationCode(userEmailVerifyVo); if (userEmailVerificationCodeEntity == null || userEmailVerificationCodeEntity.getIndex() == 0) { userEmailVerifyVo.setResult(UserEmailVerifyResult.FAILURE); return; } if(userEmailVerificationCodeEntity.isExpired() || userEmailVerificationCodeEntity.getExpiresAt().compareTo(new Date()) < 0) { userEmailVerifyVo.setResult(UserEmailVerifyResult.EXPIRED); return; }
- 만료일시가 현재시간보다 과거라면 더 이상 사용하지 못한다.
getExipresAt은 Date 타입으로 돌려주고 Date타입은 compareTo라는 메서드를 가진다. compareTo 메서드보다 앞에 있는게 더 작다면 -1이 나온다.
- dt1.compareTo(dt2) > 0 : dt1이 dt2보다 미래임
- dt1.compareTo(dt2) == 0 : dt1과 dt2는 같음
- dt1.compareTo(dt2) < 0 : dt1이 dt2보다 과거임.
- 했는 결과가 -1이면 < 0 조건문 성사
-> IUserMapper
<select id="selectUserByIndex"
parameterType="dev.jwkim.bbsbasic.entities.UserEntity"
resultType="dev.jwkim.bbsbasic.entities.UserEntity">
SELECT `index` AS `index`,
`email` AS `email`,
`password` AS `password`,
`nickname` AS `nickname`,
`address_postal` AS `addressPostal`,
`address_primary` AS addressPrimary,
`address_secondary` AS `addressSecondary`,
`email_verified_flag` AS `isEmailVerified`,
`deleted_flag` AS `isDeleted`,
`suspended_flag` AS `isSuspended`,
`admin_flag` AS `isAdmin`
FROM `spring3`.`users`
WHERE `index` = #{index}
LIMIT 1
</select>
-> UserService 수정
- 아래에 있던 애를 맨위로 끌고 올라와서
public static
붙인다. 이름도 다 변경- shift + fn + f6 => 전체수정으로 수정
-> UserService
UserEntity userEntity = this.userMapper.selectUserByIndex(userEmailVerificationCodeEntity.getUserIndex());
String saltA = userEntity.getEmail();
String saltB = userEntity.getPassword();
for (int i = 0; i< SALT_HASH_ITERATION_COUNT; i++) {
saltA = CryptoUtil.hash(CryptoUtil.Hash.SHA512, saltA);
saltB = CryptoUtil.hash(CryptoUtil.Hash.SHA512, saltB);
}
if(!userEmailVerificationCodeEntity.getSalt().equals(String.format("%s%s", saltA, saltB))) {
userEmailVerifyVo.setResult(UserEmailVerifyResult.FAILURE);
return;
}
salt가 올바른 값인지 확인을 할껀데
salt(만들었던 규칙)는 이메일과 비밀번호를 각각 SALT_HASH_ITERATION_COUNT 만큼 돌려서 hasing 한 후 합친 값이다.
주어진 salt의 hash에 일치하는 userEntity가져와서 그 userEntity가 가진 이메일과 비밀번호를 분리해서 SALT_HASH_ITERATION_COUNT 만큼 각각 돌려주고 만약 값이 (String.format("%s%s", saltA, saltB))
다르다면 FALIURE 을 돌려준다.
회원가입할 때 받은 이메일과 패스워드를 각각 hash해서 나온 saltA와 saltB를 합친게 salt 였다. (database에 들어가있음)
인증할때도 마찬가지로 주어진 코드와 salt를 이용해서 정상적인 값이라면 user을 받아올 수 있다. ( userIndex를 이용해서 userEntity 받아옴 ) 걔가 가진 이메일과 패스워드를 다시 hasing해서 나온 salt값과 방금 전달받은 salt값이 일치하는지 확인하는 과정이다. => : 보안절차
SALT_HASH_ITERATION_COUNT 그래서 얘를 맨위로 올렸다.
여기까지 왔다면 그 다음으로는 이메일 인증이 완료 되었으니깐 ( 이메일 인증을 받은 코드이니깐) userEmailVerificationCodeEntity의 isExpiredFlag를 true로 바꿔서 update해줘야하고 userEntity emailVarifiedFlag를 trun로 바꿔줘야한다.
update두번하고 SUCCESS로 뺀 다음 html로 받아서 alert로 띄우자.
지금까지..
getVerifyEmail
추가 verifyEmail
메소드 추가selectUserByEmailAndPassword
selectUserByIndex
select 추가-> IUserMapper
int updateUser(UserEntity userEntity);
int updateUserEmailVerificationCode(UserEmailVerificationCodeEntity userEmailVerificationCodeEntity);
-> UserMapper.xml추가
<update id="updateUser"
parameterType="dev.jwkim.bbsbasic.entities.UserEntity">
UPDATE `spring3`.`users`
SET `email` = #{email},
`pssword` = #{password},
`nickname` = #{nickname},
`address_postal` = #{addressPostal},
`address_primary` = #{addressPrimary},
`address_secondary` = #{addressSecondary},
`email_verified_flag` = #{isEmailVerified},
`deleted_flag` = #{isDeleted},
`suspended_flag` = #{isSuspended},
`adimn_flag` =#{isAdmin}
WHERE `index` = #{index}
LIMIT 1
</update>
<update id="updateUserEmailVerificationCode"
parameterType="dev.jwkim.bbsbasic.entities.UserEmailVerificationCodeEntity">
UPDATE `spring3`.`user_email_verification_codes`
SET `created_at` = #{createdAt},
`expires_at` = #{expiresAt},
`expired_flag` = #{isExpired},
`code` = #{code},
`salt` = #{salt},
`user_index` = #{userIndex}
WHERE `index` = #{index}
LIMIT 1
</update>
-> userService
userEntity.setEmailVerified(true);
userEmailVerificationCodeEntity.setExpired(true);
this.userMapper.updateUser(userEntity);
this.userMapper.updateUserEmailVerificationCode(userEmailVerificationCodeEntity);
// 자동으로 수정이 된다.
userEmailVerifyVo.setResult(UserEmailVerifyResult.SUCCESS);
이메일 인증을 했으니 userEntity.setEmailVerified(true);
그리고 사용을 했으니 바꿔준다.
userEmailVerificationCodeEntity.setExpired(true);
-> UserController
@RequestMapping(value = "verify-email", method = RequestMethod.GET)
public ModelAndView getVerifyEmail(
ModelAndView modelAndView,
UserEmailVerifyVo userEmailVerifyVo,
@RequestParam(name="c", required = true) String code,
@RequestParam(name="s", required = true) String salt
) {
userEmailVerifyVo.setIndex(0);
userEmailVerifyVo.setResult(null);
userEmailVerifyVo.setCode(code);
userEmailVerifyVo.setSalt(salt);
this.userService.verifyEmail(userEmailVerifyVo);
modelAndView.addObject("userEmailVerifyVo", userEmailVerifyVo);
modelAndView.setViewName("verifyEmail");
return modelAndView;
}
-> verifyEmail.html
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>이메일 인증</title>
<script th:if="${userEmailVerifyVo.getResult().name().equals('EXPIRED')}">
alert('해당 인증 링크는 이미 사용되었거나 만료되었습니다');
window.close();
</script>
<script th:if="${userEmailVerifyVo.getResult().name().equals('FAILURE')}">
alert('잘못된 접근입니다.');
window.close();
</script>
<script th:if="${userEmailVerifyVo.getResult().name().equals('SUCCESS')}">
alert('이메일 인증이 완료되었습니다.');
window.location.href = '/';
</script>
</head>
</html>
window.close();
: 이 페이지에 남지 않고 자동으로 탭을 닫아주는 것. ( 없으면 흰 화면에 남아있는다.)
회원가입 후 이메일 인증 후 잘 작동되는 지 확인해보자.
- 회원가입을 하게 되면 이 alert가 뜬다.
- 회원가입 직후 바로 로그인을 해보면 이메일 인증을 해달라고 alert가 뜬다.
이메일 인증 후에는
users
테이블의 해당회원email_verified_flag
가 1이 되어있어야한다.
- 다시 한번 이메일 인증하기버튼을 누르게 되면 위 사진 처럼 경고가 뜬다. => 만료 or 이미 사용되었다.
그리고user_email_verification_codes
테이블의 해당 코드expired_flag
가 1이 되어야한다.
우리가 만든 로직에서 요청에 대한 응답이 돌아오는 것 중에 결과 값이 발생하는 것이 3가지가 있다.
- 회원가입, 이메일 인증, 로그인 ( SUCCESS, FALIURE ...같은 결과 값 )
얘네들을 데이터베이스에 남기려고 한다.
-> system_activity_logs
system_exception_logs
테이블 생성
CREATE TABLE `spring3`.`system_activity_logs`
(
`index` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`created_at` DATETIME NOT NULL DEFAULT NOW(),
`client_ip` VARCHAR(50) NOT NULL,
`client_ua` VARCHAR(500) NOT NULL,
`request_url` VARCHAR(500) NOT NULL,
`result` VARCHAR(50) NOT NULL,
CONSTRAINT PRIMARY KEY (`index`)
);
- client_ip : 누군가가
- client_ua : 어떤 브라우저로
- request_url : 어떤 주소를 요청했는데
- result : 어떤 결과 값이 나왔다.
CREATE TABLE `spring3`.`system_exception_logs`
(
`index` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`created_at` DATETIME NOT NULL DEFAULT NOW(),
`message` VARCHAR(1000) NOT NULL,
`stacktrace` VARCHAR(10000) NOT NULL,
CONSTRAINT PRIMARY KEY (`index`)
);
- stacktrace : 오류터졌을 때 밑에 뜨는 것.
-> StandardController 추가
@Component public abstract class StandardController { protected final SystemService systemService; protected StandardController(SystemService systemService) { this.systemService = systemService; } }
생성자의 접근 제한자를
protected
로 두어 막는다.
- @Component : 어노테이션 계의 div같은 느낌.
spring이 인식가능한 범위내에 있는데 특정한 용도로 쓰일 것은 아니다.
얘가 실제 Controller로 쓰일 것이 아니기 때문이다.
-> SystemService 추가
@Service(value = "dev.dev.jwkim.bbsbasic.services.SystemService")
public class SystemService() {
}
-> StandardController 로 돌아와서
@Component public abstract class StandardController { protected final SystemService systemService; @Autowired protected StandardController(SystemService systemService) { this.systemService = systemService; } }
protected final SystemService systemService
추가 후@Autowired
해준다.- 여기서
@Component
를 빼보니@Autowired
에 경고등이 들어온다. Autowired를 사용하러면 Contoroller나 service나 뭐든 되야한다는 것이다.
정확히 StandardController의 종류를 구분할 수 없기때문에@Component
를 사용한다.
-> UserControlller 수정
public class UserController extends StandardController { private final UserService userService; @Autowired public UserController(SystemService systemService, UserService userService) { super(systemService); this.userService = userService; }
StandardController
를 상속받으니 UserController가 다 박살이 났는데 그것은 부모인 StandardController의 생성자에 SystemService를 넘겨줘야하기 때문이다. 부모객체가 먼저 객체화가 이루어져야하기 때문에 자식이 부모 생성자를 호출해줘야한다. =>super(systemService);
-> SystemService putActivityLog
메서드 생성
생성하고 내용작성 전에
-> SystemActivityLogEntity 먼저 추가 (getter,setter)
public class SystemActivityLogEntity {
private long index;
private Date createdAt;
private String clientIp;
private String clientUa;
private String requestUrl;
private String result;
public void putActivityLog(HttpServletRequest request, IResult<? extends Enum<?>> iResult) {
Date createdAt = new Date();
String clientIp = request.getRemoteAddr();
String clientUa = request.getHeader("User-Agent");
String requestUrl = request.getRequestURI();
String result = iResult.getResult().name();
SystemActivityLogEntity systemActivityLogEntity = new SystemActivityLogEntity();
systemActivityLogEntity.setCreatedAt(createdAt);
systemActivityLogEntity.setClientIp(clientIp);
systemActivityLogEntity.setClientUa(clientUa);
systemActivityLogEntity.setRequestUrl(requestUrl);
systemActivityLogEntity.setResult(result);
}
-> ISystemMapper 생성
@Mapper
public interface ISystemMapper {
int insertActivityLog(SystemActivityLogEntity systemActivityLogEntity);
}
-> SystemMapper 생성
<mapper namespace="dev.jwkim.bbsbasic.mappers.ISystemMapper">
<insert id="insertActivityLog"
parameterType="dev.jwkim.bbsbasic.entities.SystemActivityLogEntity">
INSERT INTO `spring3`.`system_activity_logs` (`created_at`, `client_ip`, `client_ua`, `request_url`, `result`)
VALUES (#{createdAt},
#{clientIp},
SUBSTRING(#{clientUa}, 1, 500),
SUBSTRING(#{requestUrl}, 1, 500),
#{result})
</insert>
</mapper>
- SUBSTRING 함수 : 문자열 자르기
clientUa와 requestUrl는 클라이언트마음대로 조작이 가능하기 때문에 500자를 넘어선다면 오류가 터져서 insert가 되지 않는다. 그것을 막기 위해 mariaDB에 있는 SUBSTRING 함수를 사용한다. 만약에 초과를 한다면 짜르겠다는 뜻이다.SUBSTRING의 예시 String str = "ABCDEFGHI"; str.substring(2,5); => 결과값BCDEF
-> SystemService추가
-> UserController
HttpServletRequest request, this.systemService.putActivityLog(request, userLoginVo); this.systemService.putActivityLog(request, userRegisterVo); this.systemService.putActivityLog(request, userEmailVerifyVo); 추가
회원가입 진행 후
system_activity_logs
에 이렇게 떠야한다.
로그인을 성공했다면 result = SUCCESS : log가 쌓이는 겋이다.