Github >> https://github.com/jinseong205/Sample_JWT
백엔드
(빌드.그레이들)
종속성 추가
plugins {
id 'java'
id 'org.springframework.boot' version '2.5.6'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.jinseong'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-security' /* Spring Security */
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' /* JPA */
implementation group: 'com.oracle.database.jdbc', name: 'ojdbc8', version: '21.8.0.0' /* Oracle ADW Connect */
implementation group: 'com.oracle.ojdbc', name: 'osdt_core', version: '19.3.0.0'
implementation group: 'com.oracle.database.security', name: 'osdt_cert', version: '21.8.0.0'
implementation group: 'com.oracle.database.security', name: 'oraclepki', version: '21.8.0.0'
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2' /* JWT */
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
}
tasks.named('test') {
useJUnitPlatform()
}
(사용자.자바)
멤버십 모델 만들기
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Builder
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100, unique = true)
private String email;
@Column(nullable = false, length = 100, unique = true)
private String password;
@Enumerated(EnumType.STRING)
@ColumnDefault("USER")
private Role role;
}
(UserDetailImpl.java)
이것은 인증을 포함하는 UserDetail 인터페이스를 구현하는 클래스입니다.
public class UserDetailsImpl implements UserDetails {
private static final long serialVersionUID = 1L;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserDetailsImpl(String email, String password, Collection<? extends GrantedAuthority> authorities) {
this.email = email;
this.password = password;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}
@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;}
public static UserDetailsImpl build(User user) {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(user.getRole().name()));
return new UserDetailsImpl(
user.getEmail(),
user.getPassword(),
authorities
);
}
}
(UserDetailsServiceImpl.java)
인증에 필요한 UserDetailService는 인터페이스의 loadUserByName 메소드를 통해 DB에 접근하여 사용자 정보에 접근한다.
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User Not Found with email: " + email));
return UserDetailsImpl.build(user);
}
}
(JwtUtils.java)
JWT 토큰을 만들고 유효성 검사를 진행합니다.
package com.jinseong.backend.auth;
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@Component
public class JwtUtils {
@Value("${jwt.secret.key}")
private String secret;
public String generateToken(String email) {
Date now = new Date();
Date expiration = new Date(now.getTime() + 3600 * 1000);
return Jwts.builder()
.setSubject(email)
.setIssuedAt(now)
.setExpiration(expiration)
//.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getEmailFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateToken(String token) {
try {
//Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
}
(JwtAuthenticationFilter.java)
클라이언트 요청 시 JWT 인증을 수행하기 위해 설치되고 UsernamePasswordAuthenticationFilter보다 먼저 작동하는 사용자 정의 필터입니다.
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private JwtUtils jwtUtils;
private UserDetailsServiceImpl userDetailsService;
public JwtAuthenticationFilter(JwtUtils jwtUtils, UserDetailsServiceImpl userDetailsService) {
this.jwtUtils = jwtUtils;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
String email = jwtUtils.getEmailFromToken(token);
if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
if (jwtUtils.validateToken(token)) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request, response);
}
}
(SecurityConfig.java)
보안 설정
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private JwtUtils jwtUtils;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtUtils, userDetailsService), UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
(UserController.java)
@Slf4j
@RestController
@RequestMapping("/api/auth")
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {
@Autowired
private AuthService authService;
@PostMapping("/signup")
public ResponseEntity<?> signup(@RequestBody User user) {
authService.signup(user);
return ResponseEntity.ok(new MessageVO("User registered successfully!"));
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody User user) {
String token = authService.login(user);
return ResponseEntity.ok(new JwtVO(token));
}
}
(UserService.java)
@Slf4j
@Service
public class AuthService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtUtils jwtUtils;
public void signup(User user) {
User saveUser = new User();
saveUser.setEmail(user.getEmail());
saveUser.setPassword(passwordEncoder.encode(user.getPassword()));
userRepository.save(saveUser);
}
public String login(User user) {
Optional<User> userOptional = userRepository.findByEmail(user.getEmail());
User findUser = userOptional.orElseThrow(() -> new UsernameNotFoundException("User not found!"));
if (!passwordEncoder.matches(user.getPassword(), findUser.getPassword())) {
throw new BadCredentialsException("Incorrect password!");
}
return jwtUtils.generateToken(user.getEmail());
}
}
프런트 엔드
(첨부파일)
function App() {
return (
<>
<Routes>
<Route path="/" element={<Home/>} />
<Route path="/login" element= {<Login/>} />
<Route path="/signup" element= {<Signup/>} />
</Routes>
</>
);
}
export default App;
(auth.Service.js)
const API_URL = 'http://localhost:8080/api/auth/';
const register = (email, password) => {
const user = {
email: email,
password: password
};
return axios.post(API_URL + 'signup', user);
};
const login = (email, password) => {
const user = {
email: email,
password: password
};
return axios.post(API_URL + 'login', user)
.then((response) => {
if (response.data.accessToken) {
localStorage.setItem('user', JSON.stringify(response.data));
}
return response.data;
});
};
const logout = () => {
localStorage.removeItem('user');
};
const getCurrentUser = () => {
return JSON.parse(localStorage.getItem('user'));
};
export default {
register,
login,
logout,
getCurrentUser
};
(LoginForm.js)
const LoginForm = () => {
const (email, setEmail) = useState("");
const (password, setPassword) = useState("");
const (loading, setLoading) = useState(false);
const (message, setMessage) = useState("");
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
setMessage("");
setLoading(true);
try {
await authService.login(email, password);
navigate("/");
window.location.reload();
} catch (error) {
const resMessage =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
setLoading(false);
setMessage(resMessage);
}
};
return (
<div className="col-md-12">
<div className="card card-container">
<Link to="/" style={{ textDecoration: "none" }}><h1>JWT Sample</h1></Link>
<form onSubmit={handleLogin}>
<div className="form-group">
<label htmlFor="email">E-Mail</label>
<input
type="text"
className="form-control"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
className="form-control"
name="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<div className="form-group">
<button
className="btn btn-primary btn-block"
disabled={loading}
>
{loading && (
<span className="spinner-border spinner-border-sm"></span>
)}
<span>Login</span>
</button>
</div>
{message && (
<div className="form-group">
<div className="alert alert-danger" role="alert">
{message}
</div>
</div>
)}
</form>
</div>
</div>
);
};
export default LoginForm;
(SingUpForm.js)
const SignupForm = () => {
const navigate = useNavigate();
const (email, setEmail) = useState("");
const (password, setPassword) = useState("");
const (successful, setSuccessful) = useState(false);
const (message, setMessage) = useState("");
const handleSignup = (e) => {
e.preventDefault();
authService.register(email, password).then(
(response) => {
setMessage(response.data.message);
setSuccessful(true);
navigate("/login");
},
(error) => {
setMessage(error.response.data.message);
setSuccessful(false);
}
);
};
return (
<div className="col-md-12">
<div className="card card-container">
<Link to="/" style={{ textDecoration: "none" }}><h1>JWT Sample</h1></Link>
<form onSubmit={handleSignup}>
<div className="form-group">
<label htmlFor="email">E-Mail</label>
<input
type="text"
className="form-control"
id="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
name="email"
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
className="form-control"
id="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
name="password"
/>
</div>
<div className="form-group">
<button className="btn btn-primary btn-block">Sign Up</button>
</div>
{message && (
<div className="form-group">
<div
className={
successful ? "alert alert-success" : "alert alert-danger"
}
role="alert"
>
{message}
</div>
</div>
)}
</form>
</div>
</div>
);
};
export default SignupForm;
(Home.js)
import React from 'react';
import { Link } from "react-router-dom";
const Home = () => {
return (
<div>
<Link to="/" style={{ textDecoration: "none" }}><h1>JWT Sample</h1></Link>
<p>Please login or signup to continue</p>
<Link to="/login" style={{ textDecoration: "none" }}>로그인</Link>
<Link to="/signup" style={{ textDecoration: "none" }}>회원가입</Link>
</div>
);
};
export default Home;
