🙋🏻
- React + Spring Boot(Maven) + JWT를 사용해 간단한 로그인을 구현했습니다.
- JWT의 개념과 동작 과정을 우선적으로 공부하기 위해 DB 사용 없이 user의 id, pw가 하드코딩되어있습니다. (추후 DB 버전도 구현할 예정)
- 아래 참고 부분의 강의들을 수강하며 진행했습니다.
Full Code
- 🌷 Github
참고
Self-contained
: 자체 포함, 토큰 자체가 정보Session의 한계
Scale Out의 한계
REST API는 Stateless를 지향
alg
: 해쉬 알고리즘typ
: 타입sub
: 어떤 것에 대해 말하는지name
: user의 이름 등..iat
: 토큰 생성 시간exp
: 만료 시간auth0 JWT
를 이용해 issuer, expire
검증
- 기존 React는 3000 port, Spring boot는 8090 port 사용
- 연동을 위해서는 새로운 port가 필요 (4200 port)
"scripts": {
"start": "set PORT=4200 && react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
start
부분에 set PORT=4200 &&
추가npm start
npm add axios
axios.get('URL').then().catch()
axios.post('URL').then().catch()
axos({
key: value
key2: value2
})
axios.post("/sample", {
id: this.state.id, pw: this.state.pw
}).then(response => {console.log(response)});
- Making use of hard coded user values for User Authentication
- user_id, user_pw
java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
spring-boot-starter-security
와 jjwt
추가server.port=8090
jwt.secret=jwtsecretkey
io.jsonwebtoken.Jwts
라이브러리 사용@Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -2550185165626007488L;
public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
@Value("${jwt.secret}")
private String secret;
//retrieve username from jwt token
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
//retrieve expiration date from jwt token
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
//for retrieveing any information from token we will need the secret key
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
//check if the token has expired
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
//generate token for user
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
//while creating the token -
//1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
//2. Sign the JWT using the HS512 algorithm and secret key.
//3. According to JWS Compact Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
// compaction of the JWT to a URL-safe string
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
//.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.setExpiration(new Date(System.currentTimeMillis() + 5 * 1000))
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
//validate token
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
JWT_TOKEN_VALIDITY
= 5 60 60 => 5시간signWith
(알고리즘, 비밀키)@Service
public class JwtUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("user_id".equals(username)) {
return new User("user_id", "$2a$10$m/enYHaLsCwH2dKMUAtQp.ksGOA6lq7Fd2pnMb4L.yT4GyeAPRPyS",
new ArrayList<>());
} else {
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
}
@RestController
@CrossOrigin
public class JwtAuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private JwtUserDetailsService userDetailsService;
@RequestMapping(value = "/authenticate", method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception {
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
final UserDetails userDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}
private void authenticate(String username, String password) throws Exception {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
} catch (DisabledException e) {
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) {
throw new Exception("INVALID_CREDENTIALS", e);
}
}
}
public class JwtRequest implements Serializable {
private static final long serialVersionUID = 5926468583005150707L;
private String username;
private String password;
//need default constructor for JSON Parsing
public JwtRequest() { }
public JwtRequest(String username, String password) {
this.setUsername(username);
this.setPassword(password);
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
}
public class JwtResponse implements Serializable {
private static final long serialVersionUID = -8091879091924046844L;
private final String jwttoken;
public JwtResponse(String jwttoken) {
this.jwttoken = jwttoken;
}
public String getToken() {
return this.jwttoken;
}
}
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtUserDetailsService jwtUserDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// JWT Token is in the form "Bearer token". Remove Bearer word and get
// only the Token
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
System.out.println("JWT Token has expired");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
// Once we get the token validate it.
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
// if token is valid configure Spring Security to manually set
// authentication
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// After setting the Authentication in the context, we specify
// that the current user is authenticated. So it passes the
// Spring Security Configurations successfully.
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
Bearer (스페이스)
로 시작jwtTokenUtil.validateToken
을 통해 Token 유효성 검사@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -7858869558953243875L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private UserDetailsService jwtUserDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// configure AuthenticationManager so that it knows from where to load
// user for matching credentials
// Use BCryptPasswordEncoder
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// For CORS error
httpSecurity.cors().configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues());
// We don't need CSRF for this example
httpSecurity.csrf().disable()
// dont authenticate this particular request
.authorizeRequests().antMatchers("/authenticate").permitAll().
// all other requests need to be authenticated
anyRequest().authenticated().and().
// stateless session exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add a filter to validate the tokens with every request
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
{
"username" : "user_id",
"password" : "user_pw"
}
import axios from 'axios'
class AuthenticationService {
// send username, password to the SERVER
executeJwtAuthenticationService(username, password) {
return axios.post('http://localhost:8090/authenticate', {
username,
password
})
}
executeHelloService() {
console.log("===executeHelloService===")
return axios.get('http://localhost:8090/hello');
}
registerSuccessfulLoginForJwt(username, token) {
console.log("===registerSuccessfulLoginForJwt===")
localStorage.setItem('token', token);
localStorage.setItem('authenticatedUser', username);
// sessionStorage.setItem('authenticatedUser', username)
//this.setupAxiosInterceptors(this.createJWTToken(token))
this.setupAxiosInterceptors();
}
createJWTToken(token) {
return 'Bearer ' + token
}
setupAxiosInterceptors() {
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;
}
// config.headers['Content-Type'] = 'application/json';
return config;
},
error => {
Promise.reject(error)
});
}
logout() {
//sessionStorage.removeItem('authenticatedUser');
localStorage.removeItem("authenticatedUser");
localStorage.removeItem("token");
}
isUserLoggedIn() {
const token = localStorage.getItem('token');
console.log("===UserloggedInCheck===");
console.log(token);
if (token) {
return true;
}
return false;
}
getLoggedInUserName() {
//let user = sessionStorage.getItem('authenticatedUser')
let user = localStorage.getItem('authenticatedUser');
if(user===null) return '';
return user;
}
}
export default new AuthenticationService()
Bearer
를 추가해서 Token을 생성https://medium.com/swlh/handling-access-and-refresh-tokens-using-axios-interceptors-3970b601a5da
Axios는 자바스크립트에서 HTTP 통신을 위해 쓰이는 Promise 기반 HTTP Client이다.
Axios Interceptors는 모든 Request/Response가 목적지에 도달하기 전에 Request에 원하는 내용을 담아 보내거나 원하는 코드를 실행시킬 수 있다.
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;
}
// config.headers['Content-Type'] = 'application/json';
return config;
},
error => {
Promise.reject(error)
});
Interceptor을 사용해 header를 보내려 하는데 자꾸 CORS Error가 나온다.
만들어둔 WebSecurityConfig 파일에 아래의 라인을 추가하면 해결된다
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.cors().configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues());
....
}
loginClicked() {
AuthenticationService
.executeJwtAuthenticationService(this.state.username, this.state.password)
.then((response) => {
AuthenticationService.registerSuccessfulLoginForJwt(this.state.username,response.data.token)
this.props.history.push(`/welcome/${this.state.username}`)
}).catch( () =>{
this.setState({showSuccessMessage:false})
this.setState({hasLoginFailed:true})
})
}
response.data.token
을 사용해 사용자 확인import { withRouter } from 'react-router'
const HeaderWithRouter = withRouter(HeaderComponent);
<HeaderWithRouter/>
좋은 정보 깔끔하게 정리해주셔서 감사합니다.