Jenkins
Spring - Security - JWT
Spring - Security - JWT๋?
์๋ ๋ก๊ทธ์ธ ๋ฐฉ์์ผ๋ก cookie ๋๋ session ๋ฐฉ์์ ๋ง์ด ์ด์ฉํด์๋ค.
ํ์ง๋ง ์ต๊ทผ๋ค์ด MSA๊ฐ ๋์
๋๊ณ ํ์ฅ์ฑ์ ๊ฐ์ง๋ DB ๊ตฌ์กฐ์ SNS ๋ก๊ทธ์ธ(OAuth)๊ฐ ์๊ธฐ๋ฉด์ Token ๋ฐฉ์์ธ JWT๋ฅผ ์ฌ์ฉํ๊ฒ ๋์๋ค.
Dependency ์ถ๊ฐ
gradle ๋น๋ ๊ธฐ์ค
testImplementation 'org.springframework.security:spring-security-test'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'com.auth0:java-jwt:3.10.3'
๊ธฐ์กด์ Security์ Dependency์ JWT / implementation โcom.auth0:java-jwt:3.10.3โ๋ฅผ ์ถ๊ฐํด์ค๋ค.
auth0๋ง๊ณ ๋ jjwt๋ผ๋ ๋ํ๋์๊ฐ ์๋๋ฐ ์ถํ์ ํ๋ฒ ์์๋ณด์
SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // session ๋ฐฉ์ ์ฌ์ฉ์ํจ
.and()
.formLogin().disable() // form login ์ฌ์ฉ์ํจ
.httpBasic().disable() //
.authorizeRequests()
.antMatchers("URL")
.access("hasRole('๊ถํ') or hasRole('๊ถํ') or hasRole('๊ถํ')")
.anyRequest().permitAll();
}
@Bean // PasswordEncoder Bean ๋ฑ๋ก
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
JWT์ํ๋ฆฌํฐ๋ฅผ ์ ์ฉํ๊ธฐ ์ํด์ SecurityConfig ํด๋์ค๋ฅผ ์์ฑํ๋ค.
Config๋ WebSecurityConfigurerAdapter์ ์์๋ฐ์ ์ฌ์ฉํ๋ฉฐ @Configuration ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํ๋ค.
์์ ๊ฐ์ด ์ค์ ํ ํ๋ก์ ํธ์ ์ ์ํ๊ฒ ๋๋ฉด ๊ธฐ๋ณธ์ ์ผ๋ก ์ํ๋ฆฌํฐ์์ ์ ๊ณตํ๋ ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก ์ด๋ํ๊ฒ๋๋ค.
ํ์ง๋ง ํ ํฐ๋ฐฉ์์ ์ด์ฉํ๊ธฐ ๋๋ฌธ์ @EnableWebSecurity ์ด๋
ธํ
์ด์
์ ์ถ๊ฐํ๋ค.
SecurityFilter
Session๋ฐฉ์์ Security๋ Login์ โ/loginโ Url์ ํตํด UsernamePasswordAuthenticationFilter๊ฐ ๋์ํ์ฌ ๋ก๊ทธ์ธ์ ์งํํ๊ฒ๋๋ค.
ํ์ง๋ง Token ๋ฐฉ์์ ๋ฐ๋ก form์ ์ฌ์ฉํ์ง ์๊ธฐ์ UsernamePasswordAuthenticationFilter๋ฅผ ์ง์ ๊ตฌํํ์ฌ ๋ก๊ทธ์ธ์ ์งํํ๋ค.

JwtAuthenticationFilter
// ์คํ๋ง ์ํ๋ฆฌ์์ UsernamePasswordAuthenticationFilter ๊ฐ ์๋ค.
// /login ์์ฒญํด์ username, password ์ ์กํ๋ฉด (post)
// UsernamePasswordAuthenticationFilter ๋์์ ํ๋ค.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
// /login ์์ฒญ์ ํ๋ฉด ๋ก๊ทธ์ธ ์๋๋ฅผ ์ํด์ ์คํ๋๋ ํจ์
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("JwtAuthenticationFilter : ๋ก๊ทธ์ธ ์๋์ค");
try {
// ๋์ด์ค๋ username๊ณผ password๋ JSON์ผ๋ก ๋์ด์ ๋ฐ๋ก parse์ ํตํด User Object๋ก ๋ง๋ค์ด์ค๋ค.
ObjectMapper om = new ObjectMapper();
User user = om.readValue(request.getInputStream(), User.class);
// 1. username, password ๋ฐ๋๋ค.
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
// 2. ์ ์์ธ์ง ๋ก๊ทธ์ธ ์๋๋ฅผ ํด๋ณธ๋ค. authenticationManager๋ก ๋ก๊ทธ์ธ ์๋๋ฅผ ํ๋ฉด
// PrincopalDetailService์ loadUserByUsername() ํจ์๊ฐ ์คํ๋ ํ ์ ์์ด๋ฉด authentication์ด ๋ฆฌํด๋๋ค.
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 3. PrincipalDetail์ ์ธ์
์ ๋ด๋๋ค. (๊ถํ ๊ด๋ฆฌ๋ฅผ ์ํด์ Session์ด ํ์)
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
System.out.println("๋ก๊ทธ์ธ ์๋ฃ๋จ : " + principalDetails.getUser().getUsername());// ๋ก๊ทธ์ธ ์ ์์ ์ผ๋ก ๋์๋ค๋ ๋ป.
// authentication ๊ฐ์ฒด๊ฐ session์์ญ์ ์ ์ฅ์ ํด์ผํ๊ณ ๊ทธ ๋ฐฉ๋ฒ์ด return ํด์ฃผ๋ฉด ๋๋ค.
// ๋ฆฌํด์ ์ด์ ๋ ๊ถํ ๊ด๋ฆฌ๋ฅผ security๊ฐ ๋์ ํด์ฃผ๊ธฐ ๋๋ฌธ์ ํธ๋ฆฌํ๊ฒ ๊ตฌํ์ด ๊ฐ๋ฅํ๋ค.
// ๊ตณ์ด JWT ํ ํฐ์ ์ฌ์ฉํ๋ฉด์ ์ธ์
์ ๋ง๋ค ์ด์ ๊ฐ ์๋ค.
// 4. JWTํ ํฐ์ ๋ง๋ค์ด์ ์๋ตํด์ค๋ค.
return authentication;
}catch (IOException e){
e.printStackTrace();
}
return null;
}
// attempAuthentication์คํ ํ ์ธ์ฆ์ด ์ ์์ ์ผ๋ก ๋์์ผ๋ฉด successfulAuthentication ํจ์๊ฐ ์คํ๋๋ค.
// JWT ํ ํฐ์ ๋ง๋ค์ด์ request์์ฒญํ ์ฌ์ฉ์์๊ฒ JWTํ ํฐ์ responeํด์ฃผ๋ฉด ๋๋ค.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
System.out.println("successfulAuthentication ์คํ๋จ : ์ธ์ฆ์ด ์๋ฃ๋์๋ค๋ ๋ป");
PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
// RSA๋ฐฉ์์ด ์๋ Hash์ํธ๋ฐฉ์
String jwtToken = JWT.create()
.withSubject("Subject") // Subject
.withExpiresAt(new Date(System.currentTimeMillis()+(60000*10))) // ์ ํจ๊ธฐ๊ฐ
.withClaim("id", principalDetails.getUser().getId()) // Cliam
.withClaim("username", principalDetails.getUser().getUsername()) // Cliam
.sign(Algorithm.HMAC512("sign")); // ์๋ช
response.addHeader("Authorization", "Bearer "+jwtToken);
}
}
์๋์ ํํฐ๋ฅผ ํตํด ํ ํฐ์ ์๋ช ์ธ์ฆ๊ณผ ์ ํจ์ฑ ์ฒดํฌ๋ฅผ ํ๊ฒ๋๋ค.
JwtAuthorizationFilter
// ์ํ๋ฆฌํฐ๊ฐ filter๋ฅผ ๊ฐ์ง๊ณ ์๋๋ฐ ๊ทธ ํํฐ์ค์ BasicAuthenticationFilter๋ผ๋ ๊ฒ์ด ์๋ค.
// ๊ถํ์ด๋ ์ธ์ฆ์ด ํ์ํ ํน์ ์ฃผ์๋ฅผ ์์ฒญํ์ ๋ ์ ํํฐ๋ฅผ ๋ฌด์กฐ๊ฑด ํ๊ฒ ๋์ด์๋ค.
// ๋ง์ฝ์ ๊ถํ์ด ์ธ์ฆ์ด ํ์ํ ์ฃผ์๊ฐ ์๋๋ผ๋ฉด ์ด ํํฐ๋ฅผ ์ํ๋ค.
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private UserRepository userRepository;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserRepository userRepository) {
super(authenticationManager);
this.userRepository = userRepository;
}
// ์ธ์ฆ์ด๋ ๊ถํ์ด ํ์ํ ์ฃผ์์์ฒญ์ด ์์ ๋ ํด๋น ํํฐ๋ฅผ ํ๊ฒ ๋จ.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//super.doFilterInternal(request, response, chain); // ์๋ต ์ค๋ฅ
System.out.println("์ธ์ฆ์ด๋ ๊ถํ์ด ํ์ํ ์ฃผ์ ์์ฒญ์ด ๋จ.");
String jwtHeader = request.getHeader("Authorization");
System.out.println("jwtHeader = " + jwtHeader);
// header๊ฐ ์๋์ง ํ์ธ
if(jwtHeader == null || !jwtHeader.startsWith("Bearer ")){
chain.doFilter(request, response);
return;
}
// JWT ํ ํฐ์ ๊ฒ์ฆ์ ํด์ ์ ์์ ์ธ ์ฌ์ฉ์์ธ์ง ํ์ธ
String token = jwtHeader.replace("Bearer ", "").trim();
String username = JWT.require(Algorithm.HMAC512("cos")).build().verify(token).getClaim("username").asString();
System.out.println("username = " + username);
// ์๋ช
์ด ์ ์์ ์ผ๋ก ๋จ
if(username != null && !username.equals("")){
System.out.println("username ์ ์");
User findUser = userRepository.findByUsername(username);
System.out.println("findUser = " + findUser.getUsername());
PrincipalDetails principalDetails = new PrincipalDetails(findUser);
// JWT ํ ํฐ ์๋ช
์ ํตํด์ ์๋ช
์ด ์ ์์ด๋ฉด Authentication ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ค๋ค.
Authentication authentication = new UsernamePasswordAuthenticationToken(principalDetails, null, principalDetails.getAuthorities());
// ๊ฐ์ ๋ก ์ํ๋ฆฌํฐ์ ์ธ์
์ ์ ๊ทผํ์ฌ Authentication ๊ฐ์ฒด๋ฅผ ์ ์ฅ.
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
์ด๋ ๊ฒ ์์ฑํ ํํฐ๋ฅผ SecurityConfig์ ์ ์ฉํ๋ค.
private final CorsFilter corsFilter;
private final UserRepository userRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
//security์ filter๋ฑ๋ก, SecurityContextPersistenceFilter ์ ์ ์คํ (Spring์ ํํฐ์ข
๋ฅ๋ฅผ ์์์ผํ๋ค)
//security์ ํํฐ๋ CustomFilter๋ณด๋ค ๋ฌด์กฐ๊ฑด์ ์ผ๋ก ์ฐ์ ์คํ๋๋ค.
//http.addFilterBefore(new CustomFilter(), SecurityContextPersistenceFilter.class);
http.csrf().disable()
.addFilter(corsFilter) // ๋ชจ๋ ์์ฒญ์ filter๋ฅผ ๊ฑฐ์น๊ฒ๋๋ค. Controller์ @CrossOrigin(์ธ์ฆX), ์ํ๋ฆฌํฐ ํํฐ์ ๋ฑ๋ก ์ธ์ฆ(O)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // session ๋ฐฉ์ ์ฌ์ฉ์ํจ
.and()
.formLogin().disable() // form login ์ฌ์ฉ์ํจ
.httpBasic().disable()
.addFilter(new JwtAuthenticationFilter(authenticationManager())) // ๋ก๊ทธ์ธ
.addFilter(new JwtAuthorizationFilter(authenticationManager(), userRepository)) // ํ ํฐ ์๋ช
์ธ์ฆ
.authorizeRequests()
.antMatchers("/api/v1/user/**")
.access("hasRole('ROLE_USER') or hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.antMatchers("/api/v1/manager/**")
.access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
.antMatchers("/api/v1/admin/**")
.access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll();
}
CorsConfig
Token ๋ฐฉ์์ ์ฌ์ฉํ๋ฉฐ Controller ํต์ ์ด์๋ RestController ํต์ ์ ์์ฃผ ์ด์ฉํ๊ฒ ๋๋ค.
JWT๋ฅผ ์ฌ์ฉํ๊ฒ๋๋ฉด React์ ๊ฐ์ ํ๋ก์ ํธ์ ํต์ ํ๋ ํ๋ก์ ํธ๋ฅผ ์งํํ๊ฒ ๋๋๋ฐ ์ด๋ ๋ฐ์ํ๋ ๋ฌธ์ ๊ฐ Cors๋ฌธ์ ์ด๋ค.
Cors๋?
Cross Origin Resource Sharing ์ ์ฝ์๋ก ๋๋ฉ์ธ์ด ๋ค๋ฅธ ์์์ ๋ฆฌ์์ค๋ฅผ ์์ฒญํ ๋ ์ ๊ทผ ๊ถํ์ ๋ถ์ฌํ๋ ๋ฉ์ปค๋์ฆ์ด๋ค.
Spring Project๋ 8080 port๋ฅผ ์ฌ์ฉํ๊ณ React๋ 3000 port๋ฅผ ์ฌ์ฉํ๊ฒ๋๋ค.
๋ง์ฝ React์์ Spring API๋ฅผ ํธ์ถํ๊ฒ๋๋ค๋ฉด CORS๋ฌธ์ ๋ฅผ ๋ง๋ ์ ์์๊ฒ์ด๋ค.
์ด๊ฒ์ ํด๊ฒฐํ๊ธฐ ์ํด CorsConfig๋ฅผ ์์ฑํด์ค๋ค.
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true); // ๋ด ์๋ฒ๊ฐ ์๋ต์ ํ ๋ json์ ์๋ฐ์คํฌ๋ฆฝํธ์์ ์ฒ๋ฆฌํ ์ ์๊ฒ ํ ์ง๋ฅผ ์ค์ ํ๋ ๊ฒ
config.addAllowedOrigin("*"); // ๋ชจ๋ ip์ ์๋ต์ ํ์ฉํ๊ฒ ๋ค.
config.addAllowedHeader("*"); // ๋ชจ๋ header์ ์๋ต์ ํ์ฉํ๊ฒ ๋ค.
config.addAllowedMethod("*"); // ๋ชจ๋ post, get, put, delete์ ๊ทผ์ ํ์ฉํ๊ฒ ๋ค.
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
}
UserDetail
์ํ๋ฆฌํฐ๋ Authentication ๊ฐ์ฒด๋ฅผ ์ธ์ ์ ๋ฃ๊ฒ๋๋๋ฐ ์ด๊ฒ์ ๊ตฌํํ๊ธฐ ์ํด์ UserDetail ์ ์์๋ฐ์ ์ฌ์ฉํ๊ฒ๋๋ค.
User Entity
@Data
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String roles; // USER,ADMIN
public List<String> getRoleList(){
if(this.roles.length() > 0){
return Arrays.asList(this.roles.split(","));
}
return new ArrayList<>();
}
}
Authentication ๊ฐ์ฒด
// ๋ก๊ทธ์ธ์ ์งํ์ด ์๋ฃ๊ฐ ๋๋ฉด ์ํ๋ฆฌํฐ session์ผ ๋ง๋ค์ด ์ค๋๋ค (Security ContextHolder)
// ์ค๋ธ์ ํธ ํ์
=> Authentication ํ์
๊ฐ์ฒด
// Authentication ์์ User ์ ๋ณด๊ฐ ์์ด์ผ ๋จ
public class PrincipalDetails implements UserDetails{
private User user; // ์ ์ ๊ฐ์ฒด
public PrincipalDetails(User user) {
this.user = user;
};
// ํด๋น User์ ๊ถํ์ ๋ฆฌํดํ๋ ๊ณณ!
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
user.getRoleList().forEach(r->{
authorities.add(() -> r);
});
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
// ๊ณ์ ๋ง๋ฃ
@Override
public boolean isAccountNonExpired() {
return true;
}
// ๊ณ์ ์ ๊น
@Override
public boolean isAccountNonLocked() {
return true;
}
// ๋น๋ฐ๋ฒํธ๊ฐ ์ค๋ ๋์๋
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// ๊ณ์ ํ์ฑํ
@Override
public boolean isEnabled() {
// ์ฌ์ดํธ์์ ์ ์ ๊ฐ 1๋
๋์ ๋ก๊ทธ์ธ์ ์ํ๋ค๋ฉด!! ํด๋จผ ๊ณ์ ์ผ๋ก ํ๊ธฐ๋ก ํจ
// ํ์ฌ์๊ฐ - ๋ก๊ธด์๊ฐ => 1๋
์ ์ด๊ณผํ๋ฉด false
return true;
}
}
ํ์๊ฐ์
ํ์๊ฐ์ ์ ๊ฐ๋จํ๊ฒ ์ํธํ๋ฅผ ์งํํ์ฌ ๋ฑ๋กํ๊ฒ๋๋ค.
@RestController
@RequiredArgsConstructor
public class RestApiController
private final PasswordEncoder passwordEncoder; // ๋น๋ฐ๋ฒํธ ์ํธํ๊ฐ ํ์ฌํ๋ค. ์ํ๋ฆฌํฐ ๋ก๊ทธ์ธ ๋ถ๊ฐ๋ฅ
// ํ์๊ฐ์
@PostMapping("join")
public String join(@RequestBody User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setRoles("ROLE_USER");
userRepository.save(user);
return "ํ์๊ฐ์
์๋ฃ";
}
}
๋ก๊ทธ์ธ
UserDetailsService
// ์ํ๋ฆฌํฐ ์ค์ ์์ loginProcessingUrl("/login")
// /login ์์ฒญ์ด ์ค๋ฉด ์๋์ผ๋ก UserDetailsService ํ์
์ผ๋ก IoC๋์ด ์๋ loadUserByUsername ํจ์ ์คํ
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User findUser = userRepository.findByUsername(username);
return new PrincipalDetails(findUser);
}
}
Leave a comment