์ค๋ ํ๋ฃจ๋ Spring Security์ ๋ํ ์๋ฌธ์ ์ผ๋ก ์์ํด์ Spring Security์ ๋ํ ์๋ฌธ์ ์ผ๋ก ๋๋ฌ๋ค. ๋ฌผ๋ก ํด๊ฒฐํด์ ๊ธฐ๋ถ์ ์ข๋ค๐ค
- Spring Security์ JWT ํ ํฐ ๋ฐฉ์์ ๋ํ ๊ณ ์ฐฐ
- Spring Security Flow ์ฐ๊ตฌ
์ค๋ Spring Security์ JWTํ ํฐ ํํฐ๋ฅผ ์ค์ ํด์ ๋ก๊ทธ์ธ ๋ฐฉ์์ ๊ตฌํํ๋ ๊ฒ์ ๋ํด ์๋ฌธ์ ์ ๊ฐ๊ณ ์์ด ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ํ์๋ค๊ณผ ์ฌ๋ฌ ํ
์คํธ ๋ฐ ๊ฐ์ ์ ํ ๊ฒฐ๊ณผ์ ๋ํ ๋ด์ฉ์ ์์ฑํ๊ณ ์ ํ๋ค.(ํ๋ฆด ์๋ ์์)
Spring Security์ ์ค์ ์ ์๋ฒ์ ์ ๊ทผ ์ JWTํ ํฐ ์ ๋ฌด๋ฅผ ๊ฒ์ฌํ๋ ํํฐ๋ฅผ ๊ฑฐ์น๋๋ก ์ค์ ํ๋ค.
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
// UsernamePasswordAuthenticationFilter ์์ jwtRequestFilter๋ฅผ ๋จผ์ ๊ฑฐ์น๋๋ก ์ค์ .
ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ ์ ๊ทผํ๋ฉด ํด๋น ๋ก์ง์ ๋ฐ๋ผ JWTํ ํฐ ์ ๋ฌด๋ฅผ ํ๋จํ๋ค.
@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtTokenUtil jwtTokenUtil;
String HEADER_STRING = "Authorization";
String TOKEN_PREFIX = "Bearer ";
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
String username = null;
String authToken = null;
if (header != null && header.startsWith(TOKEN_PREFIX)) {
authToken = header.replace(TOKEN_PREFIX,"");
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
} catch (IllegalArgumentException e) {
logger.error("an error occured during getting username from token", e);
} catch (ExpiredJwtException e) {
logger.warn("the token is expired and not valid anymore", e);
} catch(SignatureException e){
logger.error("Authentication Failed. Username or Password not valid.");
}
} else {
logger.warn("couldn't find bearer string, will ignore the header");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(req, res);
}
}
์์ธ ์ฒ๋ฆฌ์ ๋ํ responseํด์ฃผ๋ status๋ ๋ค์์ ์ฝ๋์์ ์ ์ํด์ค๋ค.
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { // ์คํ๋ง ์ํ๋ฆฌํฐ์ ๋ํ exception์ ์ ์ํ ๋ ์ด๋ฐ์์ผ๋ก ์์ฑํจ
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); //status : 401 ๋๊ฒ ์ธ์ฆ ์คํจ๋ 401 ์๋ฌ๋ฅผ ๋ฐ์์ํด
}
}
๋ก๊ทธ์ธ&๊ฐ์ ํ๊ธฐ์ ๊ฐ์ด ํน์ํ ํ์ด์ง๋ Spring Security์ ์ค์ ์ ํตํด ์ ๊ทผ ๊ฐ๋ฅํ๋๋ก ํ์ฉํด์ค๋ค.
http.authorizeRequests()
// image ํด๋๋ฅผ login ์์ด ํ์ฉ
.antMatchers("/images/**").permitAll()
// css ํด๋๋ฅผ login ์์ด ํ์ฉ
.antMatchers("/css/**").permitAll()
// ํ์ ๊ด๋ฆฌ URL ์ ๋ถ๋ฅผ login ์์ด ํ์ฉ
.antMatchers("/user/**").permitAll()
// h2-console URL ์ login ์์ด ํ์ฉ
.antMatchers("/h2-console/**").permitAll()
.antMatchers("/basic.js").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/login/kakao").permitAll()
.antMatchers("/signup").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/docs/**").permitAll()
// ๊ทธ ์ธ ๋ชจ๋ ์์ฒญ์ ์ธ์ฆ๊ณผ์ ํ์
.anyRequest().authenticated()
๋ก๊ทธ์ธ์ jwtํ ํฐ ์์ฑ์ ๋ค์์ ์ฝ๋์์ ์คํํด์ค๋ค.
@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}") // application properties์์ secretkey๊ฐ์ ๊ฐ์ ธ์จ๋ค. ์? ํ๋ก์ ํธ ์งํ ์ ํ
์คํธ์ฉ, ๋ฐฐํฌ์ฉ ๋๊ฐ์ง ๋ฐฉ์์ผ๋ก application properties๋ฅผ ๋๋ ์ ์ค์ ์ ํ ์์ ์ด๊ธฐ ๋๋ฌธ์
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))
.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));
}
}
๋ค์ํ๋ฒ ์ ๋ฆฌํ์๋ฉด ์๋์ ๊ฐ๋ค.
- ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ API๋ฅผ ํธ์ถํ๋ฉด Spring Security์ JWTํํฐ๊ฐ JWTํ ํฐ์ ์ ๋ฌด๋ฅผ ํ๋ณํ๋ค.(JWTํ ํฐ์ ์ ๋ฌด๋ง ํ๋จํ๊ณ ์ฌ์ฉ์๊ฐ ๋๊ตฌ์ธ์ง๋ ํ๋จํ์ง ์์)
- ์ดํ ํ ํฐ๊ฐ์ ๊ฐ๊ณ ์คํ๋ง ์ํ๋ฆฌํฐ์์ UserDetails์ ์์ฑํ๋ค. ์ด ๊ฐ์ @AuthenticationPrincipal ์ด๋ ธํ ์ด์ ์ ์ด์ฉํด ๊ฐ์ ธ๋ค ์ธ ์ ์๋ค.
- ๋ก๊ทธ์ธ๊ณผ ํ์๊ฐ์ ๊ณผ ๊ฐ์ด ํ ํฐ์ด ์กด์ฌํ์ง ์๋ ํ์ด์ง ๋๋ api๋ securityConfigure์์ ์ ๊ทผํ์ฉ์ ํด์ค๋ค.
์ด๋ฒ์ ์ฃผ๋ง์ ์ด๋ฃจ๊ธฐ๋ก ์ฝ์ํด๋ ผ ๊ฒ์ค ๋๋ถ๋ถ ํด๊ฒฐํ์ง ๋ชปํ๋ค. ๋ด๊ฐ ์๊ฐํ๋๊ฒ ๋ณด๋ค ์ผ์ ์ด ๋ง์ด ํ์ดํธํ๋๋ณด๋ค ์ฌ์ด์ ์์์น ๋ชปํ ๋ฌธ์ ๊ฐ ๋ฐ์ํด์ ์ด๋ฅผ ํด๊ฒฐํ๋ ์๊ฐ์ด ๊ฝค๋ ๊ฑธ๋ฆฐ ๊ฒ๋ ๋ฌธ์ ๋ผ๊ณ ์๊ฐํ๋ค.
โํด๋ณด๊ณ ์ถ์ ํ๋ก์ ํธ ํ๋ฒ ์๊ฐํด๋ณด์
ํด๋ณด๊ณ ์ถ์ ํ๋ก์ ํธ ํ๋ฒ ์๊ฐํด๋ณด๊ณ ๊ฐ๋จํ ๊ฒ ๋ถํฐ ํ๋์ฉ ํด๋ณด์. ๊ทธ๋ฆฌ๊ณ ๋ฐฐ์ด ๊ฑธ ์ด ๋์ํด์ ๋ค์์ ํญ๋ชฉ์ ๊ตฌ์ถํด ๋๊ณ ์ด๊ฒ ์ ๊ฒ ๋ง๋ค์ด ๋ณด์. ์๋๋ฐฐํฌ(GitAction) ์ธํ๋ผ ๊ตฌ์ถํด๋๊ธฐ, ํ๋ก ํธ ์๋ ์ ์ ํธ์คํ
์ผ๋ก ๋๋๊ธฐ, ๋ฐฑ์๋ ์๋ฒ EB์ ํ๋ ๋ง๋ค์ด ๋๊ธฐ
์ง๊ธ ๋ง๋๋ ํ๋ก์ ํธ๋ ์ผ๋จ AWS, GitAction ์ฐ์ตํ๋ ๋๋์ผ๋ก ๊ฐ์.
'ํด๋ณด๊ณ ์ถ์ ํ๋ก์ ํธ ์ฃผ์ ๋ฆฌ์คํธ' = [์์ค ๋ณผ ์ ์๋ ์น์ฌ์ดํธ, ์ถ๊ฐ์์ ...]
(์๊ฐ๋ณด๋ค ํด์ผ๋ ๊ฒ ๋ง๋ค. ์ผ๋จ ์บ ํ ์ผ์ ์ ์ถฉ์คํ๊ฒ ๊ตด์...)
โ์ค์๊ฐ ๊ฐ์๋ก ๋ฐฐ์ด ์คํ๋ง ์ฒ์๋ถํฐ ๋ค์ ์ง๋ณด๊ธฐ!!
์ง๊ธ์ ๋ด๊ฒ๋ ๋ฐ๋ณต๋ง์ด ์ด๊ธธ... ์ด๋ฒ์ ์ฒ์๋ถํฐ ์ง์ ์๊ฐํด๋ณด๋ฉด์ ์คํ๋ง ๊ฐ์ ๋ด์ฉ ์ง์ ์ง๋ณด์, ~~(๊นํ์ ์ ๋ฐ์ข ์ฌ๋ ค...)~~
์ต์ํ ์คํ๋ง 4๊ฐ๊น์ง ์๋ฃํ์.
(JWTํ ํฐ ๊ตฌํ๊น์ง ์๋ฃํ๋ค. ํ์ผ๋์ ๊พธ์คํ ๋ ์์ฑํด์ ๋ง๋ฌด๋ฆฌ ์ง์.)