Spring Boot and Spring Security with JWT including Access and Refresh Tokens ๐
ํด๋น ๊ธ์ ๋ชฉ์ ์ ์ ๋งํฌ์ ๊ฐ์์์ ์ ๊ฐ ์ค์ํ๋ค๊ณ ์๊ฐํ๊ฑฐ๋ ์ง๊ณ ๋์ด๊ฐ ํ์๊ฐ ์๋ค๊ณ ์๊ฐํ ๋ถ๋ถ์ ์ ๋ฆฌํ ๊ฒ์
๋๋ค. Access Token๊ณผ Refesh Token์ ์ค๋ช
ํ๋ ์ข์ ๊ฐ์์ด๋ JWT๋ก ํ ํฐ์ ๊ตฌํํ์๋ ๋ถ๋ค์ ๊ผญ ์ฐธ๊ณ ํ์๊ธธ ๋ฐ๋๋๋ค.
Spring ์์ฒด์ User
ํด๋์ค๊ฐ ์๊ธฐ์ AppUser
๋ก ํ๋ค๋ฉด import
ํ ๋ ํท๊ฐ๋ฆฌ์ง ์๋๋ค.
[JPA] ์ฆ์ ๋ก๋ฉ, ์ง์ฐ ๋ก๋ฉ | FetchType.EAGER, FetchType.LAZY
public class User {
...
@ManyToMany(fetch = FetchType.EAGER)
private Collection<Role> roles = new ArrayList<>();
}
public interface UserService {
...
List<User> getUsers();
getUsers()
๋ฆฌ์คํธ๋ฅผ ์ ๋ฌํ๋ผ๊ณ ํ์ ๋, ์ ์ ๊ฐ ๋ง์ฝ์ 5์ต๋ช
์ด๋ผ๋ฉด? ๊ทธ๋ฌ๋ฉด ์ ๋ถ ์ ๋ฌํ๊ธฐ์ ๋ฐฑ์๋์ ๋ถ๋ด์ด ๋ ๊ฒ์ด๋ค. ๊ทธ๋ด๊ฒฝ์ฐ ํ์ด์ง ์ฒ๋ฆฌ๋ฅผํด์ 10~100๋ช
๋จ์๋ก ์ ๋ฌํ๋ ๊ฒ์ด ํจ๊ณผ์ ์ผ ๊ฒ์ด๋ค. ๋ง์ฝ์ 2ํ์ด์ง๋ฅผ ๋ณด๋ฉด ์๋ก์ด 10~100๋ช
์ ๋ณด๋ด๋ฉด ๋๋๊น ๋ง์ด๋ค.
์ฌ๊ธฐ์ ์ธํฐํ์ด์ค๋ฅผ ๋ง๋ค ๊ฒ์ด๊ธฐ์ ๊ทธ๋ฌํ ์ฒ๋ฆฌ๋ฅผ ํด์ค ํ์๊ฐ ์๋ค. ํ์ง๋ง ๋ง์ฝ impl
์ ๋ง๋ ๋ค๋ฉด ๊ทธ๋ฌํ ์ฒ๋ฆฌ๋ฅผ ํ๋ ๊ฒ์ด ์ข์ ๊ฒ์ด๋ค.
@Transactional
์ด ์๊ธฐ๋๋ฌธ์ userRepository
๋ฅผ ๋ถ๋ฌ์ ๋ค์ ์ ์ฅํ ํ์๊ฐ ์๋ค๊ณ ๋งํ๋ค.
์คํ๋ง์ด session
์ ์ด์ฉํด ๋ก๊ทธ์ธ์ด ๊ฐ๋ฅํ๋๋ก ์๋ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ๊ณตํ๋ค.
`created(null)`์ ์ฌ์ฉํ๋ ์ด์ .
Spring Boot๋ฅผ ์ด์ฉํ RESTful Web Services ๊ฐ๋ฐ #14 HTTP Status Code ์ ์ด
@PostMapping("/role/addtouser")
public ResponseEntity<?> addRoleToUser(@RequestBody RoleToUserForm form) {
userService.addRoleToUser(form.getUsername(), form.getRoleName());
return ResponseEntity.ok().build();
}
ResponseEntity<?>
๋ฅผ ์ฌ์ฉํ ๋ .build()
๋ก ๋ง๋ฌด๋ฆฌ ํ๋ ์ด์ ๋?
CommandLineRunner
Spring์ด User
๋ฅผ ์ฐพ๋ ์ธ๊ฐ์ง ๋ฐฉ๋ฒ
auth.inMemoryAuthentication()
auth.jdbcAuthentication()
auth.userDetailsService()
auth.inMemoryAuthentication()
spring์ด ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๋ค์ ๊ฑฐ๋ ค์ ์ ์ ๋ฅผ ์ฐพ๋ ๋ฐฉ๋ฒ. ๊ฐ์ฅ ์์ด์ (?)์ธ๋ฏ?auth.jdbcAuthentication()
spring์ด JDBC๋ฅผ ์ฌ์ฉํด ์ ์ ๋ฅผ ์ฐพ๋ ๋ฐฉ๋ฒ. JPA๊ฐ ์๊ธฐ์ JDBC๋ฅผ ์ด์ฉํ ํ์๊ฐ ์๋ค.auth.userDetailsService()
JPA๋ฅผ ์ด์ฉํ๋ ๋ฐฉ๋ฒ โ ์ด๊ฑธ๋ก ์งํ.//๋ช ๋ถ ๋์ ๋ช ๋ฒ ๋ก๊ทธ์ธ ์ ํ๊ณผ ๊ฐ์ ๋ก๊ทธ์ธ ์คํจ ์ ๋์๋ค์ ์๋์ method์์ ์คํ๋๋ค.
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
super.unsuccessfulAuthentication(request, response, failed);
}
unsuccessfulAuthentication()
์ ์ด๋ฒ ๊ฐ์์์ ๋ค๋ฃจ์ง ์๊ณ security ๊ฐ์์์ ๋ค๋ฃฌ๋ค๊ณ ํ๋ค.
์๋ง ์ฌ๊ธฐ์์ ๋ค๋ฃจ๋ ๋ฏ ํ๋ค.
๐ข ๋์ค์ ์์ ๊ฐ์๋ ๋ค์ด๋ณผ ๊ฒ.@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = request.getParameter("username");
String password = request.getParameter("password");
request.getParameter()
์ ์ฌ์ฉํ๊ธฐ์ postman์ ์๋์ ๊ฐ์ด ์ฌ์ฉํด์ผํ๋ค.
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
HashMap<String, String> tokens = new HashMap<>();
tokens.put("accessToken", accessToken);
tokens.put("refreshToken", refreshToken);
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), tokens);
@Override
protected void configure(HttpSecurity http) throws Exception {
...
http.authorizeRequests().anyRequest().permitAll();
anyRequest().permitAll();
์ด ๋์ด์๊ธฐ์ ๋ณด์์ด ๋์ด์๋ค๊ณ ๋ณด๊ธฐ ์ด๋ ต๋ค. ๊ทธ๋ ๊ธฐ์ ํน์ ์ ์ ๋ ์ํฉ์๊ฒ๋ง ๊ถํ์ ์ฃผ๋๋ก ํ์. ์ธ๊ฐ๋ ์ด๋ง ์ ๊ทผ์ ํด์ผํ๊ธฐ์ authenticated()
๋ฅผ ์ฌ์ฉํ๋๋ก ํ์.
์ฌ๊ธฐ์ GET
์ HttpMethod.GET
์ด๋ค.
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
...
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER
= new AntPathRequestMatcher("/login", "POST");
...
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
...
CustomAuthenticationFilter
โUsernamePasswordAuthenticationFilter
์ ๊ฐ๋ณด๋ฉด default๋ก AntPathRequestMatcher("/login", "POST")
์ด ๋์ด์๋ ๊ฒ์ ๋ณผ ์ ์์ผ๋ฏ๋ก ๋ณ๋๋ก ์๋์ ๊ฐ์ด antMatchers("/login")
๋ฅผ ์ค์ ํ ํ์๋ ์๋ค.
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// http.authorizeRequests().antMatchers("/login").permitAll();
http.authorizeRequests().antMatchers("").permitAll();
๋ค๋ง ํต์ผ์ฑ์ ์ํด ์๋์ ๊ฐ์ด overwriteํ๋ค.
@Override
protected void configure(HttpSecurity http) throws Exception {
CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManagerBean());
customAuthenticationFilter.setFilterProcessesUrl("/api/login");
...
http.authorizeRequests().antMatchers("/api/login/**").permitAll();
...
http.addFilter(customAuthenticationFilter);
}
postmanํด๋ณด๋ฉด ์ด์๊ฒ ๋์ค๋ ๊ฒ์ ์ ์ ์๋ค.
ํด๋ผ์ด์ธํธ๋ token์ด๋ ๊ฐ์ด ์์ฒญ์ ๋ณด๋ผ ๋ โbearerโ์ ๊ฐ์ด ๋ถ์ฌ์ ์์ฒญ์ ๋ณด๋ผ ๊ฒ์ด๋ค. ์ ์ ๊ฐ ํ ๋ฒ validated๋๊ณ ํ ํฐ์ ๋ฐ๊ธ ๋ฐ์๊ธฐ์ ๊ทธ ์ดํ์ ์์ฒญ์ ์์ด์ permission์ ๋ฐ์ ํ์๊ฐ ์๋ค. ๊ทธ๋ ๊ธฐ์ ํด๋น ์ ์ ๊ฐ ์์ฑ์, ์ฆ โbearerโ์ด๋ผ๋ ๊ฒ์ ๋ณด์ฌ์ฃผ๊ธฐ์ํด ํด๋น ๋ฌธ๊ตฌ๋ฅผ ์ฝ์ ํ๋ค.
accessToken
์์ด GET์์ฒญ์ ํ๋ฉด 403์๋ฌ๊ฐ ๋ฐ์ํ๋ค.