diff --git a/build.gradle.kts b/build.gradle.kts index 7312af5..e65464d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation ("org.slf4j:jcl-over-slf4j") // implementation ("org.springframework.boot:spring-boot-starter-batch") implementation ("org.springframework.boot:spring-boot-starter-quartz") + implementation ("com.google.code.gson:gson:2.11.0") implementation ("org.apache.tomcat.embed:tomcat-embed-jasper") implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive") diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/configs/GlobalEnvironment.kt b/src/main/kotlin/kr/lunaticbum/back/lun/configs/GlobalEnvironment.kt index ee74148..2de417a 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/configs/GlobalEnvironment.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/configs/GlobalEnvironment.kt @@ -19,6 +19,8 @@ class GlobalEnvironment : EnvironmentAware { @Value("\${weather.api.key}") var weatherApiKey: String? = "" + private val pad = "%7C%2A-%2A%7C" + fun padding(key : String) = pad.plus(key).plus(pad) override fun setEnvironment(environment: Environment) { println ("telegramBotKey $telegramBotKey") diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/configs/SecurityConfig.kt b/src/main/kotlin/kr/lunaticbum/back/lun/configs/SecurityConfig.kt index 3a8fcb4..070bff3 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/configs/SecurityConfig.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/configs/SecurityConfig.kt @@ -3,11 +3,8 @@ package kr.lunaticbum.back.lun.configs import com.fasterxml.jackson.databind.ObjectMapper import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse -import kr.lunaticbum.back.lun.model.Role import kr.lunaticbum.back.lun.utils.LogService -import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory.disable import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.security.servlet.PathRequest import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod @@ -18,6 +15,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.core.AuthenticationException +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.web.AuthenticationEntryPoint import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.access.AccessDeniedHandler @@ -46,6 +44,7 @@ class SecurityConfig { http.authorizeHttpRequests { logService.log(it.toString()) it.requestMatchers(HttpMethod.POST,"/user/**").permitAll() +// it.requestMatchers(HttpMethod.POST,"/user/**").permitAll() // it.requestMatchers("/", "/user/**").permitAll() // .requestMatchers(".ajax").permitAll() // it.requestMatchers("/", "/user/joinUser.api").permitAll() @@ -100,4 +99,9 @@ class SecurityConfig { writer.write(json) writer.flush() } + + @Bean + fun bCryptPasswordEncoder(): BCryptPasswordEncoder { + return BCryptPasswordEncoder() + } } \ No newline at end of file diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/UserController.kt b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/UserController.kt index b5ca32b..1ff2b70 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/controllers/UserController.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/controllers/UserController.kt @@ -3,6 +3,7 @@ package kr.lunaticbum.back.lun.controllers import com.google.gson.Gson import jakarta.servlet.http.HttpServletRequest import kr.lunaticbum.back.lun.configs.GlobalEnvironment +import kr.lunaticbum.back.lun.model.RequestModel import kr.lunaticbum.back.lun.model.ResponceResult import kr.lunaticbum.back.lun.model.User import kr.lunaticbum.back.lun.model.UserManager @@ -10,6 +11,7 @@ import kr.lunaticbum.back.lun.utils.LogService import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType import org.springframework.http.ResponseEntity +import org.springframework.security.core.userdetails.UserDetails import org.springframework.web.bind.annotation.* import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.servlet.ModelAndView @@ -19,7 +21,12 @@ import java.util.* @RestController @RequestMapping("user") class UserController { - + val EncTypeKey = "enc" + val EncType00 = "T0" + val EncType11 = "T3" + val EncType10 = "T2" + val EncType01 = "T1" + val ApiKeyWordKey = "keyword" @Autowired lateinit var globalEvv : GlobalEnvironment @@ -33,17 +40,86 @@ class UserController { fun hello(httpServletRequest: HttpServletRequest): ModelAndView { logService.log("onJoin") val vm = ModelAndView("/content/user/join") - when(System.currentTimeMillis() % 5L) { - 0L -> vm.modelMap.put("ENC","T4") - 1L -> vm.modelMap.put("ENC","T3") - 2L -> vm.modelMap.put("ENC","T2") - else -> vm.modelMap.put("ENC","T0") - } - logService.log("${vm.toString()}") +// when(System.currentTimeMillis() % 5L) { +// 0L -> vm.modelMap.put(EncTypeKey,"T4") +// 1L -> vm.modelMap.put(EncTypeKey,"T3") +// 2L -> vm.modelMap.put(EncTypeKey,"T2") +// else -> vm.modelMap.put(EncTypeKey,"T0") +// } + vm.modelMap.put(EncTypeKey,EncType11) + vm.modelMap.put(ApiKeyWordKey,"JOIN") + return vm + } + + @GetMapping("/login") + fun userLogin(httpServletRequest: HttpServletRequest): ModelAndView { + logService.log("onJoin") + val vm = ModelAndView("/content/user/login") +// when(System.currentTimeMillis() % 5L) { +// 0L -> vm.modelMap.put(EncTypeKey,"T4") +// 1L -> vm.modelMap.put(EncTypeKey,"T3") +// 2L -> vm.modelMap.put(EncTypeKey,"T2") +// else -> vm.modelMap.put(EncTypeKey,"T0") +// } + vm.modelMap.put(EncTypeKey,EncType11) + vm.modelMap.put(ApiKeyWordKey,"LOGIN") return vm } + @ResponseBody + @PostMapping("/login.ajax") + fun login(httpServletRequest: HttpServletRequest, @RequestBody jsonString: String) : ResponseEntity { + logService.log("${httpServletRequest.requestURI}") + logService.log(jsonString) + var lResultCode = 0 + var lResultMsg = "Suscces" + var u : UserDetails? = null + val decodedBytes: ByteArray = Base64.getDecoder().decode(jsonString) + String(decodedBytes).let { + Gson().fromJson(it,RequestModel::class.java)?.let { model -> + logService.log(Gson().toJson(model)) + model.data?.let { jsonString -> + try { + val reqString = jsonString.split(globalEvv.padding(model.getKeyword())) + val nb = arrayListOf() + val na = arrayListOf() + reqString[0].replace(globalEvv.padding(model.getKeyword()),"").split("").toList().let { na.addAll(it) } + reqString[1].replace(globalEvv.padding(model.getKeyword()),"").split("").toList().let { nb.addAll(it) } + var max = nb.size + na.size + var fullData = arrayListOf() + for (idx in 0..max) { if (idx % 2 == 0) { if (nb.size > 0) { fullData.add(nb.removeLast()) } } else { if (na.size > 0) { fullData.add(na.removeLast()) } } } + logService.log(fullData.joinToString("")) + val target = Gson().fromJson(fullData.joinToString(""), User::class.java) ?: User() + var user = userManager.findById(target.user_id!!)?.block() + if (user == null && ((target.user_id?.length ?: 0) > 3 == true)) { + user = userManager.findByEmail(target.user_id!!)?.block() + } + if (user != null) { + if(userManager.isCorrectUser(user,target.user_pw!!)){ + + } else { + lResultMsg = "is wrong infomation id or passord" + lResultCode = 7100 + } + } else { + lResultMsg = "not founding user[can't find same id,email.. ]" + lResultCode = 7100 + } + } catch (e: Exception) { + e.printStackTrace() + lResultMsg = "unknown exception" + lResultCode = 7999 + } + } + } + } + val responce = ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(ResponceResult().apply { + this.resultCode = lResultCode + this.resultMsg = lResultMsg + }) + return responce + } @ResponseBody @@ -51,43 +127,48 @@ class UserController { fun joinUser(httpServletRequest: HttpServletRequest, @RequestBody jsonString: String) : ResponseEntity { logService.log("${httpServletRequest.requestURI}") logService.log(jsonString) + var lResultCode = 0 + var lResultMsg = "Suscces" var u : User? = null val decodedBytes: ByteArray = Base64.getDecoder().decode(jsonString) - String(decodedBytes)?.let { jsonString -> - val reqString = jsonString.split("_||L_") - var nb = arrayListOf() - (reqString[0].replace("_||L_","").split("")).toList().let { - nb.addAll(it) - } - logService.log(nb.toString()) - var na = arrayListOf() - reqString[1].replace("_||L_","").split("").toList().let { - na.addAll(it) - } - - logService.log(na.toString()) - var max = nb.size + na.size - var fullData = arrayListOf() - for (idx in 0..max) { - if (idx % 2 == 0) { - if (nb.size > 0) { - fullData.add(nb.removeLast()) - } - } else { - if (na.size > 0) { - fullData.add(na.removeLast()) + String(decodedBytes).let { + Gson().fromJson(it,RequestModel::class.java)?.let { model -> + logService.log(Gson().toJson(model)) + model.data?.let { jsonString -> + try { + val reqString = jsonString.split(globalEvv.padding(model.getKeyword())) + val nb = arrayListOf() + val na = arrayListOf() + reqString[0].replace(globalEvv.padding(model.getKeyword()),"").split("").toList().let { na.addAll(it) } + reqString[1].replace(globalEvv.padding(model.getKeyword()),"").split("").toList().let { nb.addAll(it) } + var max = nb.size + na.size + var fullData = arrayListOf() + for (idx in 0..max) { if (idx % 2 == 0) { if (nb.size > 0) { fullData.add(nb.removeLast()) } } else { if (na.size > 0) { fullData.add(na.removeLast()) } } } + logService.log(fullData.joinToString("")) + val user = Gson().fromJson(fullData.joinToString(""), User::class.java) ?: User() + if (user.checkValid() == false) { + lResultCode = 7009 + lResultMsg = "user insert Fail Reason : Not Correct Data" + }else if (userManager.findById(user!!.user_id!!)?.block() != null) { + lResultCode = 7001 + lResultMsg = "user insert Fail Reason : already has Same Id" + }else if (userManager.findByEmail(user!!.user_email!!)?.block() != null ) { + lResultCode = 7002 + lResultMsg = "user insert Fail Reason : already has Same Email" + } else { + u = userManager.save(user).block() + } + } catch (e: Exception) { + e.printStackTrace() + lResultMsg = "unknown exception" + lResultCode = 7999 } } } - logService.log(fullData.joinToString("")) - var user = Gson().fromJson(fullData.joinToString(""), User::class.java) - if (user.checkValid()) { - u = userManager.save(user).block() - } } val responce = ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(ResponceResult().apply { - resultCode = if (u != null) 0 else 8245 - resultMsg = if (u != null) "OK" else "User Insert Fail" + this.resultCode = lResultCode + this.resultMsg = lResultMsg }) return responce } diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/model/RequestModel.kt b/src/main/kotlin/kr/lunaticbum/back/lun/model/RequestModel.kt new file mode 100644 index 0000000..ba4d8d7 --- /dev/null +++ b/src/main/kotlin/kr/lunaticbum/back/lun/model/RequestModel.kt @@ -0,0 +1,12 @@ +package kr.lunaticbum.back.lun.model + +import lombok.Getter + +@Getter +class RequestModel { + var type : String? = null + var key : String? = null + var data : String? = null + + fun getKeyword() = key ?: "" +} \ No newline at end of file diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/model/User.kt b/src/main/kotlin/kr/lunaticbum/back/lun/model/User.kt index 16bbec7..dc1d066 100644 --- a/src/main/kotlin/kr/lunaticbum/back/lun/model/User.kt +++ b/src/main/kotlin/kr/lunaticbum/back/lun/model/User.kt @@ -1,8 +1,5 @@ package kr.lunaticbum.back.lun.model -import jdk.jfr.Description -import kotlinx.coroutines.reactive.awaitFirst -import kotlinx.coroutines.reactive.awaitFirstOrElse import kr.lunaticbum.back.lun.utils.LogService import lombok.* import org.springframework.beans.factory.annotation.Autowired @@ -13,6 +10,7 @@ import org.springframework.data.mongodb.repository.Query import org.springframework.data.mongodb.repository.ReactiveMongoRepository import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Repository import org.springframework.stereotype.Service import reactor.core.publisher.Mono @@ -47,7 +45,7 @@ class User { (user_email?.split("@")?.get(0)?.length ?: 0) > 1 && (user_email?.split("@")?.get(1)?.length ?: 0) > 1 && (user_email?.split(".")?.get(1)?.length ?: 0) > 1 - ) { + ) { if (user_id.equals("lun_admin") || user_id.equals("lunaticbum")){ isAdmin = "Y" } else { @@ -58,6 +56,26 @@ class User { } return false } + + /** + * 비밀번호를 암호화 + * @param passwordEncoder 암호화 할 인코더 클래스 + * @return 변경된 유저 Entity + */ + fun hashPassword(passwordEncoder: PasswordEncoder): User { + this.user_pw = passwordEncoder.encode(this.user_pw) + return this + } + + /** + * 비밀번호 확인 + * @param plainPassword 암호화 이전의 비밀번호 + * @param passwordEncoder 암호화에 사용된 클래스 + * @return true | false + */ + fun checkPassword(plainPassword: String?, passwordEncoder: PasswordEncoder): Boolean { + return passwordEncoder.matches(plainPassword, this.user_pw) + } } @Getter @@ -72,14 +90,18 @@ enum class Role(key : String, description: String) { @Repository interface UserRepository : ReactiveMongoRepository { - @Query("{id :?0}") - override fun findById(id: String): Mono + @Query("{user_id :?0}") + override fun findById(user_id: String): Mono + + @Query("{user_email :?0}") + fun findByEmail(user_email: String): Mono fun save(user: User): Mono } interface UserService { fun findById(id: String): Mono? + fun findByEmail(id: String): Mono? } @Service @@ -90,7 +112,12 @@ class UserManager : UserService , UserDetailsService { @Autowired private lateinit var userRepository: UserRepository + @Autowired + private lateinit var bCryptPasswordEncoder: PasswordEncoder + override fun findByEmail(id: String): Mono? { + return userRepository.findByEmail(id) + } override fun findById(id: String): Mono? { return userRepository.findById(id) @@ -100,15 +127,19 @@ class UserManager : UserService , UserDetailsService { fun save(user: User): Mono { println("saved user before ${user}") + user.hashPassword(bCryptPasswordEncoder) return userRepository.save(user) -// .subscribe( { println("saved user after ${it}") },{e -> e.printStackTrace()},{ -// println("saved user comp") -// }) } + fun isCorrectUser(user: User, password : String) : Boolean { + return user.checkPassword(password,bCryptPasswordEncoder) + } + + override fun loadUserByUsername(username: String?): UserDetails { var user = findById(username!!)?.blockOptional(Duration.ofMillis(5000L))?.get() ?: User() logService.log("username ${username}") + user.hashPassword(bCryptPasswordEncoder) return org.springframework.security.core.userdetails.User.builder().username(user.user_id ?: "").password(user.user_pw).roles(if ("Y".equals(user.isAdmin)) Role.ADMIN.name else {Role.USER.name}).build() } } \ No newline at end of file diff --git a/src/main/kotlin/kr/lunaticbum/back/lun/utils/StringUtils.kt b/src/main/kotlin/kr/lunaticbum/back/lun/utils/StringUtils.kt new file mode 100644 index 0000000..578fbef --- /dev/null +++ b/src/main/kotlin/kr/lunaticbum/back/lun/utils/StringUtils.kt @@ -0,0 +1,52 @@ +//package kr.lunaticbum.back.lun.utils +// +// +//import javax.crypto.Cipher +//import org.apache.commons.codec.DecoderException; +//import org.apache.commons.codec.binary.Base64; +//import org.apache.commons.codec.binary.Hex; +// +//@Throws(Exception::class) +//fun AesECBEncode(plainText: String): String { +// //Cipher 객체 인스턴스화(Java에서는 PKCS#5 = PKCS#7이랑 동일) +// +// val c: Cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") +// +// +// //Cipher 객체 초기화 +// c.init(Cipher.ENCRYPT_MODE, secretKey) +// +// +// //Encrpytion/Decryption +// val encrpytionByte: ByteArray = c.doFinal(plainText.toByteArray(charset("UTF-8"))) +// +// +// //Hex Encode +// return Hex.encodeHexString(encrpytionByte) +// +// +// //Base64 Encode +//// return Base64.encodeBase64String(encrpytionByte); +//} +// +// +////AES ECB PKCS5Padding 복호화(Hex | Base64) +//@Throws(Exception::class) +//fun AesECBDecode(encodeText: String): String { +// //Cipher 객체 인스턴스화(Java에서는 PKCS#5 = PKCS#7이랑 동일) +// +// val c: Cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") +// +// +// //Cipher 객체 초기화 +// c.init(Cipher.DECRYPT_MODE, secretKey) +// +// +// //Decode Hex +// val decodeByte: ByteArray = Hex.decodeHex(encodeText.toCharArray()) +// +// +// //Decode Base64 +//// byte[] decodeByte = Base64.decodeBase64(encodeText); +// return String(c.doFinal(decodeByte), charset("UTF-8")) +//} \ No newline at end of file diff --git a/src/main/resources/static/js/common.js b/src/main/resources/static/js/common.js new file mode 100644 index 0000000..3d1bf0a --- /dev/null +++ b/src/main/resources/static/js/common.js @@ -0,0 +1,67 @@ +function divider(key) { + return merge(padding(),key,padding()) +} +function padding() { + return "%7C%2A-%2A%7C"; +} +function merge(...args) { + return args.join(""); +} +function unformat(type , data, key) { + var even = []; + var odd = []; + data.split("").forEach(function (v,idx,full) {if(idx % 2 === 0) {even.push(v)} else {odd.push(v)}}); + switch (type) { + case "T0": return merge(odd.join(""),divider(key),even.join("")); + case "T1": return merge(odd.reverse().join(""),divider(key),even.join("")); + case "T2": return merge(odd.join(""),divider(key),even.reverse().join("")); + default: return merge(odd.reverse().join(""),divider(key),even.reverse().join("")); + } +} +function checkDebug(){ + var debug = false + try { + debug = typeof v8debug === 'object' + || /--debug|--inspect/.test(process.execArgv.join(' ')); + alert(debug) + } catch (e) { + + } + + try { + const inspector = require('inspector'); + debug = inspector.url() !== undefined; + alert(debug) + } catch (e) { + + } +} + +function post(target,type, data, key,callBackResult) { + var httpRequest; + /* 통신에 사용 될 XMLHttpRequest 객체 정의 */ + httpRequest = new XMLHttpRequest(); + /* httpRequest의 readyState가 변화했을때 함수 실행 */ + httpRequest.onreadystatechange = () => { + /* readyState가 Done이고 응답 값이 200일 때, 받아온 response로 name과 age를 그려줌 */ + if (httpRequest.readyState === XMLHttpRequest.DONE) { + if (httpRequest.status === 200) { + callBackResult(httpRequest.response) + } else { + alert('Request Error!'); + } + } + } + httpRequest.open('POST', target, true); + httpRequest.setRequestHeader("Content-Type", "text/plain"); + var odd = [] + var even = [] + var dataStr = JSON.stringify(data) + var src = dataStr.split("") + src.forEach(function (s,i,a) {if (i % 2 === 0) {even.push(s)} else {odd.push(s)}}) + httpRequest.send(btoa(JSON.stringify({ + 'data': unformat(type,data,key), + 'key':key, + 'type':type, + }))); +} diff --git a/src/main/resources/static/js/sha256.js b/src/main/resources/static/js/sha256.js new file mode 100644 index 0000000..b7d0fe6 --- /dev/null +++ b/src/main/resources/static/js/sha256.js @@ -0,0 +1,517 @@ +(function () { + 'use strict'; + + var ERROR = 'input is invalid type'; + var WINDOW = typeof window === 'object'; + var root = WINDOW ? window : {}; + if (root.JS_SHA256_NO_WINDOW) { + WINDOW = false; + } + var WEB_WORKER = !WINDOW && typeof self === 'object'; + var NODE_JS = !root.JS_SHA256_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node; + if (NODE_JS) { + root = global; + } else if (WEB_WORKER) { + root = self; + } + var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && typeof module === 'object' && module.exports; + var AMD = typeof define === 'function' && define.amd; + var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined'; + var HEX_CHARS = '0123456789abcdef'.split(''); + var EXTRA = [-2147483648, 8388608, 32768, 128]; + var SHIFT = [24, 16, 8, 0]; + var K = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ]; + var OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer']; + + var blocks = []; + + if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) { + Array.isArray = function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + } + + if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) { + ArrayBuffer.isView = function (obj) { + return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer; + }; + } + + var createOutputMethod = function (outputType, is224) { + return function (message) { + return new Sha256(is224, true).update(message)[outputType](); + }; + }; + + var createMethod = function (is224) { + var method = createOutputMethod('hex', is224); + if (NODE_JS) { + method = nodeWrap(method, is224); + } + method.create = function () { + return new Sha256(is224); + }; + method.update = function (message) { + return method.create().update(message); + }; + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createOutputMethod(type, is224); + } + return method; + }; + + var nodeWrap = function (method, is224) { + var crypto = require('crypto') + var Buffer = require('buffer').Buffer; + var algorithm = is224 ? 'sha224' : 'sha256'; + var bufferFrom; + if (Buffer.from && !root.JS_SHA256_NO_BUFFER_FROM) { + bufferFrom = Buffer.from; + } else { + bufferFrom = function (message) { + return new Buffer(message); + }; + } + var nodeMethod = function (message) { + if (typeof message === 'string') { + return crypto.createHash(algorithm).update(message, 'utf8').digest('hex'); + } else { + if (message === null || message === undefined) { + throw new Error(ERROR); + } else if (message.constructor === ArrayBuffer) { + message = new Uint8Array(message); + } + } + if (Array.isArray(message) || ArrayBuffer.isView(message) || + message.constructor === Buffer) { + return crypto.createHash(algorithm).update(bufferFrom(message)).digest('hex'); + } else { + return method(message); + } + }; + return nodeMethod; + }; + + var createHmacOutputMethod = function (outputType, is224) { + return function (key, message) { + return new HmacSha256(key, is224, true).update(message)[outputType](); + }; + }; + + var createHmacMethod = function (is224) { + var method = createHmacOutputMethod('hex', is224); + method.create = function (key) { + return new HmacSha256(key, is224); + }; + method.update = function (key, message) { + return method.create(key).update(message); + }; + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createHmacOutputMethod(type, is224); + } + return method; + }; + + function Sha256(is224, sharedMemory) { + if (sharedMemory) { + blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + this.blocks = blocks; + } else { + this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + } + + if (is224) { + this.h0 = 0xc1059ed8; + this.h1 = 0x367cd507; + this.h2 = 0x3070dd17; + this.h3 = 0xf70e5939; + this.h4 = 0xffc00b31; + this.h5 = 0x68581511; + this.h6 = 0x64f98fa7; + this.h7 = 0xbefa4fa4; + } else { // 256 + this.h0 = 0x6a09e667; + this.h1 = 0xbb67ae85; + this.h2 = 0x3c6ef372; + this.h3 = 0xa54ff53a; + this.h4 = 0x510e527f; + this.h5 = 0x9b05688c; + this.h6 = 0x1f83d9ab; + this.h7 = 0x5be0cd19; + } + + this.block = this.start = this.bytes = this.hBytes = 0; + this.finalized = this.hashed = false; + this.first = true; + this.is224 = is224; + } + + Sha256.prototype.update = function (message) { + if (this.finalized) { + return; + } + var notString, type = typeof message; + if (type !== 'string') { + if (type === 'object') { + if (message === null) { + throw new Error(ERROR); + } else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) { + message = new Uint8Array(message); + } else if (!Array.isArray(message)) { + if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) { + throw new Error(ERROR); + } + } + } else { + throw new Error(ERROR); + } + notString = true; + } + var code, index = 0, i, length = message.length, blocks = this.blocks; + while (index < length) { + if (this.hashed) { + this.hashed = false; + blocks[0] = this.block; + this.block = blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + } + + if (notString) { + for (i = this.start; index < length && i < 64; ++index) { + blocks[i >>> 2] |= message[index] << SHIFT[i++ & 3]; + } + } else { + for (i = this.start; index < length && i < 64; ++index) { + code = message.charCodeAt(index); + if (code < 0x80) { + blocks[i >>> 2] |= code << SHIFT[i++ & 3]; + } else if (code < 0x800) { + blocks[i >>> 2] |= (0xc0 | (code >>> 6)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else if (code < 0xd800 || code >= 0xe000) { + blocks[i >>> 2] |= (0xe0 | (code >>> 12)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | ((code >>> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); + blocks[i >>> 2] |= (0xf0 | (code >>> 18)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | ((code >>> 12) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | ((code >>> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } + } + } + + this.lastByteIndex = i; + this.bytes += i - this.start; + if (i >= 64) { + this.block = blocks[16]; + this.start = i - 64; + this.hash(); + this.hashed = true; + } else { + this.start = i; + } + } + if (this.bytes > 4294967295) { + this.hBytes += this.bytes / 4294967296 << 0; + this.bytes = this.bytes % 4294967296; + } + return this; + }; + + Sha256.prototype.finalize = function () { + if (this.finalized) { + return; + } + this.finalized = true; + var blocks = this.blocks, i = this.lastByteIndex; + blocks[16] = this.block; + blocks[i >>> 2] |= EXTRA[i & 3]; + this.block = blocks[16]; + if (i >= 56) { + if (!this.hashed) { + this.hash(); + } + blocks[0] = this.block; + blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + } + blocks[14] = this.hBytes << 3 | this.bytes >>> 29; + blocks[15] = this.bytes << 3; + this.hash(); + }; + + Sha256.prototype.hash = function () { + var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6, + h = this.h7, blocks = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc; + + for (j = 16; j < 64; ++j) { + // rightrotate + t1 = blocks[j - 15]; + s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3); + t1 = blocks[j - 2]; + s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10); + blocks[j] = blocks[j - 16] + s0 + blocks[j - 7] + s1 << 0; + } + + bc = b & c; + for (j = 0; j < 64; j += 4) { + if (this.first) { + if (this.is224) { + ab = 300032; + t1 = blocks[0] - 1413257819; + h = t1 - 150054599 << 0; + d = t1 + 24177077 << 0; + } else { + ab = 704751109; + t1 = blocks[0] - 210244248; + h = t1 - 1521486534 << 0; + d = t1 + 143694565 << 0; + } + this.first = false; + } else { + s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10)); + s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7)); + ab = a & b; + maj = ab ^ (a & c) ^ bc; + ch = (e & f) ^ (~e & g); + t1 = h + s1 + ch + K[j] + blocks[j]; + t2 = s0 + maj; + h = d + t1 << 0; + d = t1 + t2 << 0; + } + s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10)); + s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7)); + da = d & a; + maj = da ^ (d & b) ^ ab; + ch = (h & e) ^ (~h & f); + t1 = g + s1 + ch + K[j + 1] + blocks[j + 1]; + t2 = s0 + maj; + g = c + t1 << 0; + c = t1 + t2 << 0; + s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10)); + s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7)); + cd = c & d; + maj = cd ^ (c & a) ^ da; + ch = (g & h) ^ (~g & e); + t1 = f + s1 + ch + K[j + 2] + blocks[j + 2]; + t2 = s0 + maj; + f = b + t1 << 0; + b = t1 + t2 << 0; + s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10)); + s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7)); + bc = b & c; + maj = bc ^ (b & d) ^ cd; + ch = (f & g) ^ (~f & h); + t1 = e + s1 + ch + K[j + 3] + blocks[j + 3]; + t2 = s0 + maj; + e = a + t1 << 0; + a = t1 + t2 << 0; + this.chromeBugWorkAround = true; + } + + this.h0 = this.h0 + a << 0; + this.h1 = this.h1 + b << 0; + this.h2 = this.h2 + c << 0; + this.h3 = this.h3 + d << 0; + this.h4 = this.h4 + e << 0; + this.h5 = this.h5 + f << 0; + this.h6 = this.h6 + g << 0; + this.h7 = this.h7 + h << 0; + }; + + Sha256.prototype.hex = function () { + this.finalize(); + + var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, + h6 = this.h6, h7 = this.h7; + + var hex = HEX_CHARS[(h0 >>> 28) & 0x0F] + HEX_CHARS[(h0 >>> 24) & 0x0F] + + HEX_CHARS[(h0 >>> 20) & 0x0F] + HEX_CHARS[(h0 >>> 16) & 0x0F] + + HEX_CHARS[(h0 >>> 12) & 0x0F] + HEX_CHARS[(h0 >>> 8) & 0x0F] + + HEX_CHARS[(h0 >>> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] + + HEX_CHARS[(h1 >>> 28) & 0x0F] + HEX_CHARS[(h1 >>> 24) & 0x0F] + + HEX_CHARS[(h1 >>> 20) & 0x0F] + HEX_CHARS[(h1 >>> 16) & 0x0F] + + HEX_CHARS[(h1 >>> 12) & 0x0F] + HEX_CHARS[(h1 >>> 8) & 0x0F] + + HEX_CHARS[(h1 >>> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] + + HEX_CHARS[(h2 >>> 28) & 0x0F] + HEX_CHARS[(h2 >>> 24) & 0x0F] + + HEX_CHARS[(h2 >>> 20) & 0x0F] + HEX_CHARS[(h2 >>> 16) & 0x0F] + + HEX_CHARS[(h2 >>> 12) & 0x0F] + HEX_CHARS[(h2 >>> 8) & 0x0F] + + HEX_CHARS[(h2 >>> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] + + HEX_CHARS[(h3 >>> 28) & 0x0F] + HEX_CHARS[(h3 >>> 24) & 0x0F] + + HEX_CHARS[(h3 >>> 20) & 0x0F] + HEX_CHARS[(h3 >>> 16) & 0x0F] + + HEX_CHARS[(h3 >>> 12) & 0x0F] + HEX_CHARS[(h3 >>> 8) & 0x0F] + + HEX_CHARS[(h3 >>> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] + + HEX_CHARS[(h4 >>> 28) & 0x0F] + HEX_CHARS[(h4 >>> 24) & 0x0F] + + HEX_CHARS[(h4 >>> 20) & 0x0F] + HEX_CHARS[(h4 >>> 16) & 0x0F] + + HEX_CHARS[(h4 >>> 12) & 0x0F] + HEX_CHARS[(h4 >>> 8) & 0x0F] + + HEX_CHARS[(h4 >>> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F] + + HEX_CHARS[(h5 >>> 28) & 0x0F] + HEX_CHARS[(h5 >>> 24) & 0x0F] + + HEX_CHARS[(h5 >>> 20) & 0x0F] + HEX_CHARS[(h5 >>> 16) & 0x0F] + + HEX_CHARS[(h5 >>> 12) & 0x0F] + HEX_CHARS[(h5 >>> 8) & 0x0F] + + HEX_CHARS[(h5 >>> 4) & 0x0F] + HEX_CHARS[h5 & 0x0F] + + HEX_CHARS[(h6 >>> 28) & 0x0F] + HEX_CHARS[(h6 >>> 24) & 0x0F] + + HEX_CHARS[(h6 >>> 20) & 0x0F] + HEX_CHARS[(h6 >>> 16) & 0x0F] + + HEX_CHARS[(h6 >>> 12) & 0x0F] + HEX_CHARS[(h6 >>> 8) & 0x0F] + + HEX_CHARS[(h6 >>> 4) & 0x0F] + HEX_CHARS[h6 & 0x0F]; + if (!this.is224) { + hex += HEX_CHARS[(h7 >>> 28) & 0x0F] + HEX_CHARS[(h7 >>> 24) & 0x0F] + + HEX_CHARS[(h7 >>> 20) & 0x0F] + HEX_CHARS[(h7 >>> 16) & 0x0F] + + HEX_CHARS[(h7 >>> 12) & 0x0F] + HEX_CHARS[(h7 >>> 8) & 0x0F] + + HEX_CHARS[(h7 >>> 4) & 0x0F] + HEX_CHARS[h7 & 0x0F]; + } + return hex; + }; + + Sha256.prototype.toString = Sha256.prototype.hex; + + Sha256.prototype.digest = function () { + this.finalize(); + + var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, + h6 = this.h6, h7 = this.h7; + + var arr = [ + (h0 >>> 24) & 0xFF, (h0 >>> 16) & 0xFF, (h0 >>> 8) & 0xFF, h0 & 0xFF, + (h1 >>> 24) & 0xFF, (h1 >>> 16) & 0xFF, (h1 >>> 8) & 0xFF, h1 & 0xFF, + (h2 >>> 24) & 0xFF, (h2 >>> 16) & 0xFF, (h2 >>> 8) & 0xFF, h2 & 0xFF, + (h3 >>> 24) & 0xFF, (h3 >>> 16) & 0xFF, (h3 >>> 8) & 0xFF, h3 & 0xFF, + (h4 >>> 24) & 0xFF, (h4 >>> 16) & 0xFF, (h4 >>> 8) & 0xFF, h4 & 0xFF, + (h5 >>> 24) & 0xFF, (h5 >>> 16) & 0xFF, (h5 >>> 8) & 0xFF, h5 & 0xFF, + (h6 >>> 24) & 0xFF, (h6 >>> 16) & 0xFF, (h6 >>> 8) & 0xFF, h6 & 0xFF + ]; + if (!this.is224) { + arr.push((h7 >>> 24) & 0xFF, (h7 >>> 16) & 0xFF, (h7 >>> 8) & 0xFF, h7 & 0xFF); + } + return arr; + }; + + Sha256.prototype.array = Sha256.prototype.digest; + + Sha256.prototype.arrayBuffer = function () { + this.finalize(); + + var buffer = new ArrayBuffer(this.is224 ? 28 : 32); + var dataView = new DataView(buffer); + dataView.setUint32(0, this.h0); + dataView.setUint32(4, this.h1); + dataView.setUint32(8, this.h2); + dataView.setUint32(12, this.h3); + dataView.setUint32(16, this.h4); + dataView.setUint32(20, this.h5); + dataView.setUint32(24, this.h6); + if (!this.is224) { + dataView.setUint32(28, this.h7); + } + return buffer; + }; + + function HmacSha256(key, is224, sharedMemory) { + var i, type = typeof key; + if (type === 'string') { + var bytes = [], length = key.length, index = 0, code; + for (i = 0; i < length; ++i) { + code = key.charCodeAt(i); + if (code < 0x80) { + bytes[index++] = code; + } else if (code < 0x800) { + bytes[index++] = (0xc0 | (code >>> 6)); + bytes[index++] = (0x80 | (code & 0x3f)); + } else if (code < 0xd800 || code >= 0xe000) { + bytes[index++] = (0xe0 | (code >>> 12)); + bytes[index++] = (0x80 | ((code >>> 6) & 0x3f)); + bytes[index++] = (0x80 | (code & 0x3f)); + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff)); + bytes[index++] = (0xf0 | (code >>> 18)); + bytes[index++] = (0x80 | ((code >>> 12) & 0x3f)); + bytes[index++] = (0x80 | ((code >>> 6) & 0x3f)); + bytes[index++] = (0x80 | (code & 0x3f)); + } + } + key = bytes; + } else { + if (type === 'object') { + if (key === null) { + throw new Error(ERROR); + } else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) { + key = new Uint8Array(key); + } else if (!Array.isArray(key)) { + if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) { + throw new Error(ERROR); + } + } + } else { + throw new Error(ERROR); + } + } + + if (key.length > 64) { + key = (new Sha256(is224, true)).update(key).array(); + } + + var oKeyPad = [], iKeyPad = []; + for (i = 0; i < 64; ++i) { + var b = key[i] || 0; + oKeyPad[i] = 0x5c ^ b; + iKeyPad[i] = 0x36 ^ b; + } + + Sha256.call(this, is224, sharedMemory); + + this.update(iKeyPad); + this.oKeyPad = oKeyPad; + this.inner = true; + this.sharedMemory = sharedMemory; + } + HmacSha256.prototype = new Sha256(); + + HmacSha256.prototype.finalize = function () { + Sha256.prototype.finalize.call(this); + if (this.inner) { + this.inner = false; + var innerHash = this.array(); + Sha256.call(this, this.is224, this.sharedMemory); + this.update(this.oKeyPad); + this.update(innerHash); + Sha256.prototype.finalize.call(this); + } + }; + + var exports = createMethod(); + exports.sha256 = exports; + exports.sha224 = createMethod(true); + exports.sha256.hmac = createHmacMethod(); + exports.sha224.hmac = createHmacMethod(true); + + if (COMMON_JS) { + module.exports = exports; + } else { + root.sha256 = exports.sha256; + root.sha224 = exports.sha224; + if (AMD) { + define(function () { + return exports; + }); + } + } +})(); \ No newline at end of file diff --git a/src/main/resources/static/js/user.js b/src/main/resources/static/js/user.js new file mode 100644 index 0000000..10c9199 --- /dev/null +++ b/src/main/resources/static/js/user.js @@ -0,0 +1,90 @@ + +function onclickJoin(type, keyword) { + let user_id = document.getElementById('user_id') + let user_pw = document.getElementById('user_pw') + let user_pw_check = document.getElementById('user_pw_check') + let user_name = document.getElementById('user_name') + let user_email = document.getElementById('user_email') + var fields = [user_id,user_pw, user_pw_check, user_name, user_email] + var hasValues = true + const spPattern = /[~!@#$%<>^&*]/; //특수문자 + const korean = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/; //한글 + const eng = /[a-zA-Z]/; //영어 + const numbers = /[0-9]/; //숫자 + const email = /[A-za-z0-9\-][A-Za-z0-9_.\-]+@[A-za-z0-9\-][A-Za-z0-9\-]+\.[A-za-z0-9\-][A-za-z0-9\-]+/; + fields.forEach(function (field , idx , all) { + if ((field.value.length > 7 || + (field===user_name && user_name.value.length > 2) || + (field===user_id && user_id.value.length > 6)) && + hasValues) { + const text = field.value + switch (field) { + case user_id : + if (korean.test(text)) { + hasValues = false + alert("id를 확인 해보슈."); + } + break; + case user_pw : + if ( + korean.test(text) || + false === numbers.test(text) || + false === eng.test(text) || + false === spPattern.test(text) + ) { + hasValues = false + alert("pw 한글 노노 영문 숫자 특문(~!@#$%<>^&*) 섞으셈."); + } + break + case user_email : if(false === email.test(field.value)) { + hasValues = false + alert("email를 확인 해보슈."); + } + break + } + } else if (hasValues) { + hasValues = false + switch (field) { + case user_id : alert("id를 확인 해보슈.");break + case user_pw : alert("pw를 확인 해보슈.");break + case user_pw_check : alert("pw를 확인 해보슈.");break + case user_name : alert("name를 확인 해보슈.");break + case user_email : alert("email를 확인 해보슈.");break + } + } + }) + if (hasValues) { + let data = { + 'user_id': user_id.value, + 'user_pw': user_pw.value, + 'user_email': user_email.value, + 'user_name': user_name.value + } + if (user_pw.value === user_pw_check.value) { + if(confirm(JSON.stringify(data) + "\n해당 내용으로\n유저 등록 하실??")) { + post("joinUser.ajax",type,JSON.stringify(data),keyword, function (resultData) { + alert(resultData) + }) + } else { + + } + } else { + alert("비번이 다름요") + } + } +} + + + + +function onclickLogin(type, keyword) { + let user_id = document.getElementById('user_id') + let user_pw = document.getElementById('user_pw') + let data = { + 'user_id': user_id.value, + 'user_pw': user_pw.value, + } + post("login.ajax",type,JSON.stringify(data),keyword, function (resultData) { + alert(resultData) + }) +} \ No newline at end of file diff --git a/src/main/resources/templates/content/user/join.html b/src/main/resources/templates/content/user/join.html index f17d82b..21be579 100644 --- a/src/main/resources/templates/content/user/join.html +++ b/src/main/resources/templates/content/user/join.html @@ -6,11 +6,17 @@ Spring Boot - + + + - +
@@ -26,20 +32,9 @@ 이름 - 생년월일 - 이메일 - - @ - - - - + +
diff --git a/src/main/resources/templates/content/user/login.html b/src/main/resources/templates/content/user/login.html new file mode 100644 index 0000000..5000439 --- /dev/null +++ b/src/main/resources/templates/content/user/login.html @@ -0,0 +1,34 @@ + + + + + Spring Boot + + + + + + + + + +
+ + + + + + +
아이디
비밀번호
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/head.html b/src/main/resources/templates/fragments/head.html new file mode 100644 index 0000000..27386a3 --- /dev/null +++ b/src/main/resources/templates/fragments/head.html @@ -0,0 +1,4 @@ + +
+
+ \ No newline at end of file