Spring Boot +

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>
            &nbsp;
            <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>
            &nbsp;
            <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>
                        &nbsp;
                        <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>
                        &nbsp;
                        <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>
            &nbsp;
            <Link to="/signup" style={{ textDecoration: "none" }}>회원가입</Link>
            
        </div>
    );
};

export default Home;