UserRepository (추가)
package com.toyproject.bookmanagement.repository;
import org.apache.ibatis.annotations.Mapper;
import com.toyproject.bookmanagement.entity.Authority;
import com.toyproject.bookmanagement.entity.User;
@Mapper
public interface UserRepository {
// 이메일 중복확인
public User findUserByEmail(String email);
// 유저 등록
public int saveUser (User user);
public int saveAuthority(Authority authority);
}
UserMapper.xml (추가)
<insert id="saveUser"
parameterType="com.toyproject.bookmanagement.entity.User"
useGeneratedKeys="true"
keyProperty="userId">
insert into user_tb
values (0, #{email},#{password},#{name},#{provider})
</insert>
<insert id="saveAuthority" parameterType="com.toyproject.bookmanagement.entity.Authority">
insert into authority_tb
values (0, #{userId}, #{roleId})
</insert>
AuthenticationService(추가)
package com.toyproject.bookmanagement.service;
import org.springframework.stereotype.Service;
import com.toyproject.bookmanagement.dto.auth.SignupReqDto;
import com.toyproject.bookmanagement.entity.Authority;
import com.toyproject.bookmanagement.entity.User;
import com.toyproject.bookmanagement.exception.CustomException;
import com.toyproject.bookmanagement.exception.ErrorMap;
import com.toyproject.bookmanagement.repository.UserRepository;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class AuthenticationService {
private final UserRepository userRepository;
public void checkDuplicatedEmail(String email) {
User userEntity = userRepository.findUserByEmail(email);
if(userEntity != null) {
throw new CustomException("Duplicated Email",
ErrorMap.builder().put("email", "사용중인 이메일입니다.").build());
}
}
public void signup(SignupReqDto signupReqDto) {
User userEntity = signupReqDto.toEntity();
userRepository.saveUser(userEntity);
userRepository.saveAuthority(Authority.builder()
.userId(userEntity.getUserId())
.roleId(1)
.build());
}
}
회원가입
MySQL (DB 자동 저장) - 회원가입 완료
Register.js (추가)
const Register = () => {
const navigate = useNavigate();
const [registerUser, setRegisterUser] = useState( { email:"",password:"",name:"" } );
const [errorMessages, setErrorMessages] = useState( { email: "", password: "" , name: "" } );
const onChangeHandle = (e) => {
const { name, value } = e.target;
setRegisterUser( { ...registerUser, [name]:value } );
}
const registeSubmit = async() => {
const data = {
...registerUser
}
const option = {
headers: {
"Content-Type" : "application/json"
}
}
try{
await axios.post("http://localhost:8080/auth/signup", JSON.stringify(data), option);
setErrorMessages({email: "", password: "" , name: ""}); //빈값 ( 로그인 성공 시, error 메시지 뜨지않음 )
alert("회원가입 성공!");
navigate("/login");
}catch(error){
setErrorMessages({email: "", password: "" , name: "",...error.response.data.errorData}); //객체 (error.response.data.errorData)
}
}
Login.js (수정)
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import React, { useState } from 'react';
import LoginInput from '../../components/UI/Login/LoginInput/LoginInput';
import { FiUser, FiLock } from 'react-icons/fi';
import { Link } from 'react-router-dom';
import { BsGoogle } from 'react-icons/bs';
import { SiNaver,SiKakao } from 'react-icons/si';
import axios from 'axios';
;
const container = css`
display: flex;
flex-direction: column;
align-items: center;
padding: 70px 30px;
`;
const logo = css`
margin: 50px 0px;
font-size: 34px;
font-weight: 600;
`;
const mainContainer = css`
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid #dbdbdb;
border-radius: 10px;
padding: 40px 20px;
width: 400px;
`;
const authForm = css`
width: 100%;
`;
const inputLabel = css`
margin-left: 5px;
font-size: 12px;
font-weight: 600;
`;
const forgotPassword = css`
display: flex;
justify-content: flex-end;
align-content: center;
margin-bottom: 45px;
width: 100%;
font-size: 12px;
font-weight: 600;
`;
const loginButton = css`
margin: 10px 0px ;
border: 1px solid #dbdbdb;
border-radius: 7px;
width: 100%;
height: 50px;
background-color: white;
font-weight: 900;
cursor: pointer;
&:hover {
background-color: #fafafa;
}
&:active {
background-color: #eee;
}
`;
const oauth2Container = css`
display: flex;
justify-content: center;
align-items: center;
margin: 20px;
width: 100%;
`;
const oauth2 = (provider) => css`
display: flex;
justify-content: center;
align-items: center;
margin: 0px 10px;
border: 1px solid ${provider === "google" ? "#0075ff" : provider === "naver" ? "#19ce60": "#ffdc00"};
border-radius: 50%;
width: 50px;
height: 50px;
font-size: ${provider === "kakao" ? "30px" : "20px"};
cursor: pointer;
&:hover {
background-color: ${provider === "google" ? "#0075ff" : provider === "naver" ? "#19ce60": "#ffdc00"};
}
`;
const signupMessage = css`
margin-top: 20px;
font-size: 14px;
font-weight: 600;
color: #777;
`;
const register = css`
margin-top: 10px;
font-weight: 600;
`;
const errorMsg = css`
margin-left: 5px;
margin-bottom: 20px;
font-size: 12px;
color:red;
`;
const Login = () => {
const [loginUser, setLoginUser] = useState({ email:"",password:"" ,name: ""});
const [errorMessages, setErrorMessages] = useState( { email: "", password: "" ,name: "" } );
const handleChange = (e) => {
const { name, value } = e.target;
setLoginUser({ ...loginUser, [name]:value });
}
const loginHandleSubmit = async() => {
const option = {
headers: {
"Content-Type" : "application/json"
}
}
try{
const response = await axios.post("http://localhost:8080/auth/login", JSON.stringify(loginUser),option);
setErrorMessages({email: "", password: "" , });
console.log(response.data);
}catch(error){
setErrorMessages({email: "", password: "", ...error.response.data.errorData});
}
}
return (
<div css= {container}>
<header>
<h1 css= { logo } >Login</h1>
</header>
<main css={ mainContainer }>
<div css={authForm}>
<label css={ inputLabel }>Email</label>
<LoginInput type="email" placeholder="Type your email" onChange={handleChange} name="email" >
<FiUser />
</LoginInput>
<div css={errorMsg}>{errorMessages.email}</div>
<label css={ inputLabel }>Password</label>
<LoginInput type="password" placeholder="Type your password" onChange={handleChange} name="password" >
<FiLock />
</LoginInput>
<div css={errorMsg}>{errorMessages.password}</div>
<div css= { forgotPassword }><Link to="/forgot/password">Forgot Password?</Link></div>
<button css={ loginButton } onClick={loginHandleSubmit}>LOGIN</button>
</div>
<div></div>
</main>
<div css = { signupMessage }>Or Sign Up Using</div>
<div css= {oauth2Container}>
<div css={ oauth2("google") }><BsGoogle /></div>
<div css={ oauth2("naver") }><SiNaver /></div>
<div css={ oauth2("kakao") }><SiKakao /></div>
</div>
<div css= { signupMessage }>Or Sign Up Using</div>
<footer>
<div css = { register }><Link to="/register">SIGN UP</Link></div>
</footer>
</div>
);
};
export default Login;
config
WebMvcConfig
package com.toyproject.bookmanagement.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// `/**` : 모든 요청
// .allowedMethods("*") : 모든 메서드에서 열어준다
// .allowedOrigins("http://localHost:3000") : 해당 port에서 오는 요청을
registry.addMapping("/**")
.allowedMethods("*")
.allowedOrigins("*"); // test시 , 모든 port 개방
// .allowedOrigins("http://localHost:3000");
}
}
service
AuthenticationService
package com.toyproject.bookmanagement.service;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
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 com.toyproject.bookmanagement.dto.auth.JwtRespDto;
import com.toyproject.bookmanagement.dto.auth.LoginReqDto;
import com.toyproject.bookmanagement.dto.auth.SignupReqDto;
import com.toyproject.bookmanagement.entity.Authority;
import com.toyproject.bookmanagement.entity.User;
import com.toyproject.bookmanagement.exception.CustomException;
import com.toyproject.bookmanagement.exception.ErrorMap;
import com.toyproject.bookmanagement.repository.UserRepository;
import com.toyproject.bookmanagement.security.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class AuthenticationService implements UserDetailsService {
private final UserRepository userRepository;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final JwtTokenProvider jwtTokenProvider;
public void checkDuplicatedEmail(String email) {
User userEntity = userRepository.findUserByEmail(email);
if(userEntity != null) {
throw new CustomException("Duplicated Email",
ErrorMap.builder().put("email", "사용중인 이메일입니다.").build());
}
}
public void signup(SignupReqDto signupReqDto) {
User userEntity = signupReqDto.toEntity();
userRepository.saveUser(userEntity);
userRepository.saveAuthority(Authority.builder()
.userId(userEntity.getUserId())
.roleId(1)
.build());
}
public JwtRespDto signin(LoginReqDto loginReqDto) {
//AuthenticationManagerBuilder가 알아보게 하기 위함 (입력한 email , password와 DB에 저장된 email, password를 비교)
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginReqDto.getEmail(), loginReqDto.getPassword()); //암호화 안된 비밀번호
// 이까지만 해도 로그인 성공
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
return jwtTokenProvider.generateToken(authentication);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepository.findUserByEmail(username);
if(userEntity == null) {
throw new CustomException("로그인 실패",ErrorMap.builder().put("email", "사용자 정보를 확인하세요").build());
}
return userEntity.toPrincipal();
}
}
dto > auth
JwtRespDto
package com.toyproject.bookmanagement.dto.auth;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
public class JwtRespDto {
private String grantType;
private String accessToken;
}
security
PrincipalUser
package com.toyproject.bookmanagement.security;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.toyproject.bookmanagement.entity.Authority;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class PrincipalUser implements UserDetails {
private static final long serialVersionUID = 3893676052625302075L;
private int userId;
private String email;
private String password;
private List<Authority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorrities= new ArrayList<>();
this.authorities.forEach(authority -> {
authorrities.add(new SimpleGrantedAuthority(authority.getRole().getRoleName()));
});
return authorrities;
}
@Override
public String getPassword() {
return password; //암호화 된 비밀번호
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
entity
User(추가)
package com.toyproject.bookmanagement.entity;
import java.util.ArrayList;
import java.util.List;
import com.toyproject.bookmanagement.security.PrincipalUser;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {
private int userId;
private String email;
private String password;
private String name;
private String provider;
private List<Authority> authorities;
public PrincipalUser toPrincipal() {
List<String> roles = new ArrayList<>();
authorities.forEach(authority ->{
roles.add(authority.getRole().getRoleName());
});
return PrincipalUser.builder()
.userId(userId)
.email(email)
.password(password)
.authorities(authorities)
.build();
}
}
MVN Token 관련 의존성 추가
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
security
JwtTokenProvider(핵심)
package com.toyproject.bookmanagement.security;
import java.security.Key;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import com.toyproject.bookmanagement.dto.auth.JwtRespDto;
import com.toyproject.bookmanagement.entity.Authority;
import com.toyproject.bookmanagement.exception.CustomException;
import lombok.extern.slf4j.Slf4j;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
@Slf4j
@Component
public class JwtTokenProvider {
private final Key key;
public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
}
public JwtRespDto generateToken(Authentication authentication) {
StringBuilder builder = new StringBuilder();
authentication.getAuthorities().forEach(authority -> {
builder.append(authority.getAuthority() +",");
});
builder.delete(builder.length() -1, builder.length()); // 마지막 쉼표 삭제
String authorities = builder.toString();
Date tokenExpiresDate = new Date(new Date().getTime()+ (1000 * 60 * 60 * 24)); // 현재시간 + 하루
String accessToken = Jwts.builder()
.setSubject(authentication.getName()) // 토큰의 이름 (email)
.claim("auth", authorities) // auth
.setExpiration(tokenExpiresDate) // 토큰 만료 시간
.signWith(key, SignatureAlgorithm.HS256) // 토큰 암호화
.compact();
return JwtRespDto.builder()
.grantType("Bearer")
.accessToken(accessToken)
.build();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
}catch(SecurityException | MalformedJwtException e) {
// Security 라이브러리에 오유가 있거나, JSON의 포맷이 잘못된 형식의 JWT가 들어왔을 때 예외
// SignatureException이 포함되어 있음
log.info("Invalid JWT Token", e);
}catch (ExpiredJwtException e) {
// 토큰의 유효기간이 만료된 경우의 예외
log.info("Expired JWT Token", e);
}catch (UnsupportedJwtException e) {
// jwt의 형식을 지키지 않은 경우 (Header.Payload.Signature)
log.info("Unsupported JWT Token", e);
}catch (IllegalArgumentException e) {
// jwt 토큰이 없을때
log.info("IllegalArgument JWT Token", e);
}catch (Exception e) {
log.info("JWT Token", e);
}
return false;
}
public Authentication getAuthentication(String accessToken) {
Claims claims = parseClaims(accessToken);
Object roles = claims.get("auth");
if(roles == null) {
throw new CustomException("권한 정보가 없는 토큰입니다.");
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
String[] rolesArray = roles.toString().split(",");
Arrays.asList(rolesArray).forEach(role -> {
authorities.add(new SimpleGrantedAuthority(role));
});
UserDetails userDetails = new User(claims.getSubject(),"",authorities);
return new UsernamePasswordAuthenticationToken(userDetails,"",authorities);
}
private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(accessToken)
.getBody();
}catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
controller
AuthenticationController (수정)
package com.toyproject.bookmanagement.controller;
import javax.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.toyproject.bookmanagement.aop.annotation.ValidAspect;
import com.toyproject.bookmanagement.dto.auth.LoginReqDto;
import com.toyproject.bookmanagement.dto.auth.SignupReqDto;
import com.toyproject.bookmanagement.dto.response.DataResponseDto;
import com.toyproject.bookmanagement.service.AuthenticationService;
import lombok.RequiredArgsConstructor;
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationService authenticationService;
@ValidAspect
@PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody LoginReqDto loginReqDto, BindingResult bindingResult) {
return ResponseEntity.ok().body(DataResponseDto.of(authenticationService.signin(loginReqDto)));
}
@ValidAspect
@PostMapping("/signup")
public ResponseEntity<?> signup(@Valid @RequestBody SignupReqDto signupReqDto, BindingResult bindingResult) {
authenticationService.checkDuplicatedEmail(signupReqDto.getEmail());
authenticationService.signup(signupReqDto);
return ResponseEntity.ok().body(true);
}
}
로그인 결과
Login.js
코드를 입력하세요
pages > Main
Main.js
코드를 입력하세요
components > Routes > AuthRoute
AuthRoute.js
코드를 입력하세요
npm install recoil 설치
index.js (수정 & 추가)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { RecoilRoot, atom, selector } from 'recoil';
export const authenticated = atom({
key: "authenticated",
default: false
})
export const authenticatedState = selector( {
key: "authenticatedState",
get: ({ get }) => {
const auth = get(authenticated);
return auth;
}
})
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<RecoilRoot>
<BrowserRouter>
<App />
</BrowserRouter>
</RecoilRoot>
</React.StrictMode>
);
reportWebVitals();
Login.js (추가)
코드를 입력하세요