-
Spring Security + JWT ํ์๊ฐ์ , ๋ก๊ทธ์ธ (2)Spring 2022. 10. 23. 16:13๋ฐ์ํ
๐ ์ง๋ ๊ฒ์๊ธ์์ Spring Security์ ๊ตฌ์กฐ์ ํ์ํ Settings์ ๋ค๋ค์ต๋๋ค.
์ด์ด์ Security์ ํ์ํ Class๋ค์ ๋ค๋ค๋ณด๊ฒ ์ต๋๋ค.์ง๋ ํฌ์คํ ์ ์๋ ๋งํฌ๋ฅผ ์ฐธ์กฐ ํ์๊ธธ ๋ฐ๋๋๋ค.
https://developer-been.tistory.com/2
๐ ์ธ์ฆ ๊ตฌ์กฐ ์์1๏ธโฃ (Login → Jwt Token ๋ฐ๊ธ๋ ์ํ) API ์์ฒญ
2๏ธโฃ WebSecurityConfig์์ configure์ addFilterBefore()๋ก ์ค์ ํ
Authentication Filter ์ ๊ทผ
3๏ธโฃ JwtAuthenticationFilter → Jwt Token ๊ฒ์ฆ
4๏ธโฃ ๊ฒ์ฆ์ด ์๋ฃ ๋ฌ๋ค๋ฉด, getAuthentication()์ ํตํด์ UserDetailService ์คํ
→ DB์์ member ์กฐํ→ Security์ User๊ฐ์ฒด๋ฅผ ์์๋ฐ์ MemberAccount ๊ฐ์ฒด return
5๏ธโฃ ์ ๊ณผ์ ํตํด UsernamePasswordAuthenticationToken ์์ฑ ํ,
SecurityContextHolder์ context์ Authentication์ Set.
6๏ธโฃ DispatcherServlet ์ ๋ฌ
์์ํ๊ธฐ
๐ MemberAccount.java
@Getter @EqualsAndHashCode(callSuper =false) public class MemberAccount extends User { private Member member; public MemberAccount(Member member) { super(member.getPhoneNum(), member.getPassword(), Collections.singletonList (newSimpleGrantedAuthority(member.getRoles().get(0)))); this.member = member; } }
- Member์ Adapter Class
- @AuthenticationPrincipal ๋ฅผ ํตํด(SpEL) Principal ๋ด๋ถ ์ ๋ณด์ ์ ๊ทผ ํ ์ ์์ต๋๋ค.
- @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member")
-> ์ต๋ช ์ฌ์ฉ์์ธ ๊ฒฝ์ฐ์ null ๋ฆฌํด
-> ์ธ์ฆ๋ ์ฃผ์ฒด์ธ ๊ฒฝ์ฐ member ๊ฐ์ฒด ๋ฆฌํด
๐ก ์ฆ, Principal์ ๋ด๋ถ ์ ๋ณด์ ์ ๊ทผํ ์ ์๋ Custom Anotation ์์ฑ ์ํ Adapter Class
๐ CustomUserDetails Class ์์ฑ (CustomUserDetails.java)@RequiredArgsConstructor @Service public class CustomUserDetailService implements UserDetailsService { private final MemberRepository memberRepository; @Override public UserDetails loadUserByUsername(String phoneNum) throws UsernameNotFoundException { Member member = memberRepository.findByPhoneNum(phoneNum) .orElseThrow(() -> new UsernameNotFoundException("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.")); return new MemberAccount(member); } }
- Security์ UserDetailsService๋ฅผ ๊ตฌํํ ํด๋์ค ์ ๋๋ค. (UserService๋ผ๊ณ ์ดํดํ๋ฉด ์ฌ์)
- loadUserByUsername() : DB์์ ์ ์ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค์ํ ์ค๋ฒ๋ผ์ด๋ ๋ฉ์๋์ ๋๋ค.
- ๋ง์ฝ ํด๋น username์ ์ฌ์ฉ์ ์ ๋ณด๊ฐ ์๋ค๋ฉด UsernameNotFoundException ์์ธ๋ฅผ ๋์ ธ์ค๋๋ค.
๐ก MemberAccount(Free Naming)๋ฅผ return ํ๋ ์ด์
→ Spring Security๊ฐ ๋ค๋ฃจ๋ ์ ์ ์ ๋ณด์ ๋๋ฉ์ธ์์ ๋ค๋ฃจ๋ ์ ์ ์ ๋ณด ์ฌ์ด ๊ฐญ์ ๋งค๊ฟ์ฃผ๋ ์ผ์ข ์ ์ด๋ํฐ ์ญํ ์ ํฉ๋๋ค.
๐ JWT ๊ฒ์ฆ ํํฐ ์ ์ (JwtAuthenticationFilter.java)
@RequiredArgsConstructor public class JwtAuthenticationFilter extends GenericFilterBean { private final JwtTokenProvider jwtTokenProvider; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 1๏ธโฃ ํค๋์์ JWT ๋ฐ์์ค๊ธฐ String accessToken = jwtTokenProvider.resolveToken((HttpServletRequest) request); // 2๏ธโฃ ์ ํจ ํ ํฐ ๊ฒ์ฆ if (accessToken !=null && jwtTokenProvider.validateToken(accessToken, (HttpServletRequest) request)) { // 3๏ธโฃ ํ ํฐ ์ ํจ์ ์ ์ ์ ๋ณด ๋ฐ์์ค๊ธฐ Authentication authentication = jwtTokenProvider.getAuthentication(accessToken); // 4๏ธโฃ SecurityContext์ Authentication ๊ฐ์ฒด๋ฅผ ์ ์ฅ SecurityContextHolder.getContext().setAuthentication(authentication); } 5๏ธโฃ chain.doFilter(request, response); } }
- ํด๋น ํด๋์ค๋ Custom AuthenticationFilter ์ ๋๋ค. (Servlet๋จ์์ ์ฒ๋ฆฌ)
- GenericFilterBean์ ์์ ๋ฐ์ Filter Class ์์ฑ
- ์ค๋ฒ๋ผ์ด๋ฉ ํ doFilter ํจ์ ์ ์
- ์ต์ข ์ ์ผ๋ก 5๏ธโฃ chain.doFilter() ๋ฉ์๋๋ฅผ ํตํด DispatcherServlet์ผ๋ก ๋์ด๊ฐ๊ฒ ๋ฉ๋๋ค.
๐ JwtTokenProvider Class ์์ฑ (JwtTokenProvider.java)
- ์ธ์ -์ฟ ํค ๊ธฐ๋ฐ์ ๋ฐฉ์์ด ์๋, JWT๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์ ํ ํฐ ๋ฐํ,๊ฒ์ฆ์ ํ ์ ์๋ ๋ชจ๋์ด ์์ด์ผ ํฉ๋๋ค.
- Jwt ์์ฑ, ๊ฒ์ฆ ๋ฑ ํจ์๋ฅผ ์ ์ํ ์ปดํฌ๋ํธ ์์ฑ
- ์๋ ๋ช ์ํ 1๏ธโฃ~7๏ธโฃ๋ฒ ํจ์๋ค ์ ์
@Slf4j @RequiredArgsConstructor @Component public class JwtTokenProvider { // ๋ณด์์ ์ํด appplication.properties์ ์ ์ฅํ key @Value("${project.jwt.access-token-key}") privateString accessTokenKey; private final CustomUserDetailService userDetailsService; private final MemberRepository memberRepository; private final TokenRepository tokenRepository; // ์ ํจ์๊ฐ : AccessToken: 5๋ถ / RefreshToken: 7์ผ ์ค์ . private longACCESS_TOKEN_EXPIRE_TIME = 5 * 60 * 1000L; private longREFRESH_TOKEN_EXPIRE_TIME = 60 * 60 * 24 * 7 * 1000L; @PostConstruct protected void init() { accessTokenKey = Base64.getEncoder().encodeToString(accessTokenKey.getBy tes(StandardCharsets.UTF_8)); } ... ์๋์ ์์ฐจ์ ์ผ๋ก ๋ช ์ํ ํจ์๋ค ์ ์ }
- AccessToken Expired Time : 5๋ถ / RefreshToken Expired Time : 7์ผ ์ค์
- accessTokenkey๋ฅผ Base64๋ก ์ธ์ฝ๋ฉ
๐ก @PostConstruct
๊ฐ์ฒด์ ์ด๊ธฐํ ๋ถ๋ถ
๊ฐ์ฒด๊ฐ ์์ฑ๋ ํ ๋ณ๋์ ์ด๊ธฐํ ์์ ์ ์ํด ์คํํ๋ ๋ฉ์๋๋ฅผ ์ ์ธํ๋ค.
@PostConstruct ์ด๋ ธํ ์ด์ ์ ์ค์ ํด๋์ init ๋ฉ์๋๋ WAS๊ฐ ๋์์ง ๋ ์คํ๋๋ค.
1๏ธโฃ JWT ๋ฐ๊ธ (JwtTokenProvider.java)
public JwtTokensVO createToken(String userPk, List<String> roles) { // Header ๋ถ๋ถ ์ค์ Map<String, Object> headers = new HashMap<>(); headers.put("typ", "JWT"); headers.put("alg", "HS256"); // ํ ํฐ Builder Claims claims = Jwts.claims().setSubject(userPk); // JWT payLoad์ ์ ์ฅ๋๋ ์ ๋ณด๋จ์ claims.put("roles", roles); // key, value ์์ผ๋ก ์ ๋ณด ์ ์ฅ Date now = new Date(); Date accessDate = new Date(now.getTime() + ACCESS_TOKEN_EXPIRE_TIME); Date refreshDate = new Date(now.getTime() + REFRESH_TOKEN_EXPIRE_TIME); return JwtTokensVO.builder() .accessToken(generateToken(headers, claims, now, accessDate)) .refreshToken(generateToken(headers, claims, now, refreshDate)) .accessTokenExpirationDate(accessDate) .refreshTokenExpirationDate(refreshDate) .build(); }
- Header : HS256 ์ํธํ ๋ฐฉ์์ JWT ๋ช ์
- Claims : subject(ํ ํฐ ์ ๋ชฉ)์ userPk ์ ์ฅ → ์ฌ๊ธฐ์๋ ๋ก๊ทธ์ธ ์์ด๋ (ํด๋ํฐ ๋ฒํธ)
- roles : ์ฌ์ฉ์ ๊ถํ ์ ์ฅ
- AccessToken : 5๋ถ | RefreshToken : 7์ผ
- JwtTokensVO ํด๋์ค build
2๏ธโฃ JWT ์์ฑ (JwtTokenProvider.java)
public String generateToken(Map<String, Object> headers, Claims claims, Date now, Date expirationDt) { return Jwts.builder() .setHeader(headers) .setClaims(claims)// ์ ๋ณด ์ ์ฅ .setIssuedAt(now)// ํ ํฐ ๋ฐํ ์๊ฐ ์ ๋ณด .setExpiration(expirationDt)// set Expire Time .signWith(SignatureAlgorithm.HS256, accessTokenKey)// set algorithm, signature secret ๊ฐ .compact(); }
- accessToken, refreshToken ์์ฑ์ ์ํด ๊ณตํต ์ฒ๋ฆฌํ ํจ์
3๏ธโฃ JWT์์ ํ์ ์ ๋ณด ์ถ์ถ (JwtTokenProvider.java)
public String getUserPk(String token) { return Jwts.parser().setSigningKey(accessTokenKey).parseClaimsJws(token).getBo dy().getSubject(); }
- accessTokenKey๋ก ์๋ช , token value๋ก Claims์ subject ๊ฐ(์ ์ฅํด๋ ๋ก๊ทธ์ธ ์์ด๋) return
4๏ธโฃ JWT ํ ํฐ์์ ์ธ์ฆ ์ ๋ณด ์กฐํ (JwtTokenProvider.java)
public Authentication getAuthentication(String token) { UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUser Pk(token)); return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); }
- UserDetailService์ loadUserByUsername ๋ฉ์๋์ ์ธ์๋ก getUser(String token) ํจ์ ํธ์ถ
- UsernamePasswordAuthenticationToken return
5๏ธโฃ Request Header์์ Token ๊ฐ ๊ฐ์ ธ์ค๊ธฐ (JwtTokenProvider.java)
public String resolveToken(HttpServletRequest request) { return request.getHeader("X-AUTH-TOKEN"); }
- API ์์ฒญ์ Header์ Token์ ๋ด์์ ์์ฒญ์ ํ๊ฒ ๋ฉ๋๋ค.
- “X-AUTH-TOKEN” : “TOKEN VALUE” ํ์์ผ ๋, “X-AUTH-TOKEN” ๊ฐ์ ๊ฐ์ ธ์ต๋๋ค.
6๏ธโฃ ํ ํฐ์ ํจ์ฑ + ๋ง๋ฃ์ผ์ ํ์ธ (JwtTokenProvider.java)
public boolean validateToken(String jwtToken, HttpServletRequest request) { try { Jws<Claims> claims = Jwts.parser().setSigningKey(accessTokenKey).parseClaimsJws(jwtToken); return !claims.getBody().getExpiration().before(new Date()); } catch (SignatureException ex) { log.error("Invalid JWT Signature"); request.setAttribute("exception", "invalidSignature"); return false; } catch (MalformedJwtException ex) { log.error("Invalid JWT token"); request.setAttribute("exception", "invalidJwt"); return false; } catch (ExpiredJwtException ex) { log.error("Expired JWT token"); request.setAttribute("exception", "expiredJwt"); return false; } catch (UnsupportedJwtException ex) { log.error("Unsupported JWT exception"); request.setAttribute("exception", "unsupportedJwt"); return false; } catch (IllegalArgumentException ex) { log.error("Jwt claims string is empty"); request.setAttribute("exception", "claimsEmpty"); return false; } }
- accessTokenKey๋ก ์๋ช , token value๋ก ์กฐํํ Claims์ ๋ง๋ฃ์๊ฐ์ ํ์ฌ๋ ์ง์ ๋น๊ต
- Jwt ๊ด๋ จ Exception ๋ฐ์์ Filter์์ ๋ฐ์ํ Exception์ Spring์ DispatcherServlet๊น์ง ๋ฟ์ ์๊ฐ ์๊ธฐ ๋๋ฌธ์ ์ฌ์ฉ์ ์ ์ Exception์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
๐ก JWT ๊ด๋ จ Exception ๋ชฉ๋ก
SignatureException : Invalid Signature
MalformedJwtException : Invalid JWT
ExpiredJwtException : Expired JWT
UnsupportedJwtException : Unsupported Exception
IllegalArgumentException : Empty JWT Claims stirng
*** ํด๊ฒฐ ๋ฐฉ์ (ํด๋น ๋ถ๋ถ์ ์ฌ๊ธฐ๋ฅผ ์ฐธ์กฐํ์ธ์.) ***
- ์๋ฌ ๋ฐ์์ request์ attribute์ ๊ฐ ์๋ฌ ์ ๋ณด Set
- Security Config ๋ถ๋ถ์ ์ค์ ํ EntryPoint → customAuthenticationEntryPoint์์ ํธ๋ค๋ง
7๏ธโฃ Access Token ๊ฐฑ์ (JwtTokenProvider.java)
public BaseResDto refreshAccessToken(HttpServletRequest request, String refreshToken) throws ServiceException { if (validateToken(refreshToken, request)) { String phoneNum = getUserPk(refreshToken); Member member = memberRepository.findByPhoneNum(phoneNum) .orElseThrow(() -> new ServiceException(ResultCode.MEMBER_NOT_EXIST)); if(0 == tokenRepository.countByTokenValueAndMemberId(refreshToken, member.getMemberId())) { throw new ServiceException(ResultCode.REFRESH_TOKEN_EXPIRED); } TokenResDto tokenResDto = new TokenResDto(); JwtTokensVO jwtTokensVO = createToken(member.getUsername(), member.getRoles()); tokenResDto.setAccessToken(jwtTokensVO.getAccessToken()); tokenResDto.setRefreshToken(jwtTokensVO.getRefreshToken()); tokenRepository.deleteByMemberId(member.getMemberId()); Token token =new Token(); token.setTokenValue(jwtTokensVO.getRefreshToken()); token.setMemberId(member.getMemberId()); token.setExpiredDt(jwtTokensVO.getRefreshTokenExpirationDate()); tokenRepository.save(token); return tokenResDto; } else { throw new ServiceException(ResultCode.REFRESH_TOKEN_EXPIRED); } }
- ServiceException : ๊ณตํต @ExceptionHandler Handler (์ฌ์ฉ์ ์ ์)
- memberRepository : ํ์ Repository
- tokenRepository : ํ ํฐ Repository
- BaseResDto : ์ฌ์ฉ์ ์ ์ ๊ฐ์ฒด (resultCode, resultMessage)
.orElseThrow(() -> new ServiceException(ResultCode.MEMBER_NOT_EXIST));
Jpa Query Method์์ Exception ๋ฐ์์ ์ปค์คํ ํ ์๋ฌ ํธ๋ค๋ฌ๋ฅผ ํตํด Reponse ์ถ๋ ฅ
Spring Boot ์์ ๊ณตํต ์๋ฌ ํธ๋ค๋ง ๋ฐฉ๋ฒ์ด ๊ถ๊ธํ์ ๋ถ์ ์ฌ๊ธฐ๋ฅผ ์ฐธ์กฐํด์ฃผ์ธ์.
๐ก ์งํ ๊ณผ์
1๏ธโฃ validateToken() ํจ์ ํธ์ถ
2๏ธโฃ getUserPK() ํจ์๋ก token claim subject (์ฌ๊ธฐ์๋ ํธ๋ํฐ๋ฒํธ) ๊ฐ ์ป์ ๋ค, ํ์ ์กฐํ
3๏ธโฃ createToken() ํจ์๋ก accessToken, refreshToken ๊ฐฑ์
4๏ธโฃ DB์์ ๊ธฐ์กด refreshToekn ์ญ์ , ์๋ก์ด refreshToken ์ถ๊ฐ
5๏ธโฃ tokenResDto Return
6๏ธโฃ ๋ง์ฝ validateToken() ํจ์๋ก ๊ฒ์ฆ์ ์คํจํ์ ๊ฒฝ์ฐ refreshToken ๋ง๋ฃ Exception์ ๋์ง๋๋ค.๋ค์ ํฌ์คํ ์์ ์ด์ด ๊ฐ๊ฒ ์ต๋๋ค.
https://developer-been.tistory.com/4
Reference:
๋ฐ์ํ'Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Spring] Spring์์ CORS ์ฒ๋ฆฌ(์ค์ )ํ๋ ๋ฐฉ๋ฒ (0) 2022.10.27 [Spring] Spring์์ Scheduler ์ฒ๋ฆฌํ๊ธฐ (0) 2022.10.26 Spring Boot ๊ณตํต Global Exception Handler (0) 2022.10.23 Spring Security + JWT ํ์๊ฐ์ , ๋ก๊ทธ์ธ (3) (0) 2022.10.23 Spring Security + JWT ํ์๊ฐ์ , ๋ก๊ทธ์ธ (1) (0) 2022.10.23