Authorization
보호된 리소스에 액세스하기 위한 모든 요청의 헤더에 이 JWT 토큰을 보냅니다.ADMIN
생성할 수 있습니다.USER
에서 포스트 작성 할 수 있습니다.다음 클래스는 보안 구현의 핵심입니다. 여기에는 프로젝트에 필요한 거의 모든 보안구성이 포함되어 있습니다.
SecurityConfig
먼저 패키지 내부에 config
폴더를 생성하고 코드를 살펴보고 각 구성이 수행하는 작업을 알아보겠습니다.
package com.record.backend.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.record.backend.security.CustomUserDetailsService;
import com.record.backend.security.JwtAuthenticationEntryPoint;
import com.record.backend.security.JwtAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomUserDetailsService customUserDetailsService;
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
//Field authenticationManager in service.SecurityServiceImpl required a bean of type 'org.springframework.security.authentication.AuthenticationManager'
//이 오류나서 추가
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js")
.permitAll()
.antMatchers("/api/auth/**")
.permitAll()
.antMatchers("/api/user/checkUsernameAvailability", "/api/user/checkEmailAvailability")
.permitAll()
.antMatchers(HttpMethod.GET, "/api/polls/**", "/api/users/**")
.permitAll()
.anyRequest()
.authenticated();
// Add our custom JWT security filter
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
위의 SecurityConfig
클래스는 IDE에서 사용되는 많은 클래스를 아직 정의하지 않았기 때문에 IDE 컴파일 오류 표시할텐데 차차 작성할거다.
그전에 작성한 어노테이션과 설정 의미를 알아보자.
1. @EnableWebSecurity
이것은 프로젝트에서 웹 보안을 활성화하는데 사용되는 기본 스프링 보안 주석입니다.
2. @EnableGlobalMethodSecurity
이것은 주석을 기반으로 하는 메서드 수준 보안을 활성화하는데 사용됩니다. 메소드 보안을 위해 다음 세가지 유형의 주석을 사용할 수 있습니다.
@Secured
과 같이 컨트롤러/서비스 메서드를 보호할 수 있는 주석을 활성화합니다.@Secured("ROLE_ADMIN")
public User getAllUsers() {}
@Secured({"ROLE_USER", "ROLE_ADMIN"})
public User getUser(Long id) {}
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public boolean isUsernameAvailable() {}
@RolesAllowed
과 같이 사용할 수 있는 주석을 활성화합니다.@RolesAllowed("ROLE_ADMIN")
public Poll createPoll() {}
@PreAuthorize
및@PostAuthorize
주석을 사용하여 보다 복잡한 표현식 기반 액세스 제어 구문을 활성화합니다.@PreAuthorize("isAnonymous()")
public boolean isUsernameAvailable() {}
@PreAuthorize("hasRole('USER')")
public Poll createPoll() {}
3. WebSecurityConfigurerAdapter
이 클래스는 Spring Security의 WebSecurityConfigurer
인터페이스를 구현합니다. 기본 보안 구성을 제공하고 다른 클래스가 이를 확장하고 해당 메서드를 재정의하여 보안 구성을 사용자 지정할 수 있도록 합니다.
우리 SecurityConfig
클래스 WebSecurityConfigurerAdapter
는 사용자 정의 보안 구성을 제공하기 위해 일부 메서드를 확장하고 재정의합니다.
4. CustomUserDetails 서비스
사용자를 인증하거나 다양한 역할 기반 검사를 수행하려면 Spring 보안에서 사용자 세부 정보를 어떻게든 로드해야합니다.
UserDetailsService
이를 위해 사용자 이름을 기반으로 사용자를 로드하는 단일 메서드가 있는 인터페이스로 구성됩니다.
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
인터페이스를 구현하고 메서드에 대한 구현을 제공하는 CustomUserDetailsService
를 정의합니다. UserDetailsService loadUserByUsername()
이 loadUserByUsername()
메서드는 UserDetails
Spring Security가 다양한 인증 및 역할 기반 유효성 검사를 수행하는 데사용하는객체를 반환합니다.
구현에서 인터페이스를 구현하고 메서드에서 개체를 반환하는 사용자 지정 UserPrincipal
클래스도 정의합니다. UserDetails UserPrincipal loadUserByUsername()
5. JwtAuthenticationEntryPoint
이 클래스는 적절한 인증 없이 보호된 리소스에 액세스하려고 하는 클라이언트에 401 무단 오류를 반환하는데 사용됩니다. Spring Security의 AuthenticationEntryPoint
인터페이스를 구현합니다.
6. Jwt 인증 필터
우리는 JwtAuthenticationFilter
다음과 같은 필터를 구현하는 데 사용할 것입니다.
Authorization
모든 요청의 헤더에서 JWT 인증 토큰을 읽습니다.SecurityContext
Spring Security는 사용자 세부 정보를 사용하여 권한 부여 검사를 수행합니다. 또한 컨트롤러에 저장된 사용자 세부 정보에 액세스하여 SecurityContext
비즈니스 로직을 수행할 수 있습니다. 7. AuthenticationManagerBuilder 및 AuthenticationManager
AuthenticationManagerBuilder AuthenticationManager
사용자 인증을 위한 기본 Spring Security 인터페이스인 인스턴스를 생성하는 데 사용됩니다.
AuthenticationManagerBuilder
메모리 내 인증. LDAP 인증. JDBC 인증을 구축하거나 사용자 지정 인증 공급자를 추가하는 데 사용할 수 있습니다.
이 예제에서 우리는 AuthenticationManager를 빌드하기 위해 customUserDetailsService와 passwordEncoder를 제공했습니다.
AuthenticationManager
로그인 API에서 사용자를 인증하도록 구성된 것을 사용할 것입니다.
8. HttpSecurity 구성
HttpSecurity
csrf 구성은 , 및 같은 보안 기능을 구성 sessionManagement
하고 다양한 조건에 따라 리소스를 보호하는 규칙을 추가 하는 데 사용됩니다.
이 예에서는 모든 사람에게 정적 리소스 및 기타 몇 가지 공개 API에 대한 액세스를 허용하고 인증된 사용자에게만 다른 API에 대한 액세스를 제한합니다.
구성 에 JWTAuthenticationEntryPoint
및 사용자 지정 도 추가했습니다 .JWTAuthenticationFilterHttpSecurity
이전 섹션에서 많은 사용자 정의 클래스와 필터를 사용하여 스프링 보안을 구성했습니다. 이 섹션에서는 이러한 클래스를 하나씩 정의합니다.
다음 모든 사용자 지정 보안 관련 클래스는 security
패키지를 생성하여 이 안에 들어갑니다.
1. 커스텀 스프링 스큐리티 AuthenticationEntryPoint
우리가 정의할 첫 번째 스프링 보안 관련 클래스는 JwtAuthenticationEntryPoint
이다.
AuthenticationEntryPoint
인터페이스를 구현하고 해당 매서드에 대한 구현을 제공합니다.
commence()
이 메서드는 인증되지 않은 사용자가 인증이 필요한 리소스에 액세스하려고 하여 예외가 throw될 때마다 호출됩니다.
이 경우 예외 메세지가 포함된 401 오류로 간단히 응답합니다.
package com.example.polls.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
logger.error("Responding with unauthorized error. Message - {}", e.getMessage());
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
}
}
2. 커스텀 스프링 시큐리티 사용자 정보
UserPrincipal
이라고 불리는 UserDetails
를 상속하는 사용자 정의 클래스를 정의해 보겠습니다. 이것은 UserDetailsService
사용자 정의에서 인스턴스가 반환될 클래스입니다.
Spring Security는 객체에 저장된 정보를 사용하여 UserPrincipal
인증 및 권한 부여를 수행합니다.
전체 UserPrincipal
코드는 아래와 같다.
package com.example.polls.security;
import com.example.polls.model.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class UserPrincipal implements UserDetails {
private Long id;
private String name;
private String username;
@JsonIgnore
private String email;
@JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserPrincipal(Long id, String name, String username, String email, String password, Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.name = name;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserPrincipal create(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream().map(role ->
new SimpleGrantedAuthority(role.getName().name())
).collect(Collectors.toList());
return new UserPrincipal(
user.getId(),
user.getName(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities
);
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserPrincipal that = (UserPrincipal) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
3. 커스텀 스프링 시큐리티 UserDetailsService
UserDetailsService
이제 사용자 이름이 지정된 사용자 데이터를 로드하는 사용자 지정을 정의해 보겠습니다.
package com.example.polls.security;
import com.example.polls.model.User;
import com.example.polls.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String usernameOrEmail)
throws UsernameNotFoundException {
// Let people login with either username or email
User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
.orElseThrow(() ->
new UsernameNotFoundException("User not found with username or email : " + usernameOrEmail)
);
return UserPrincipal.create(user);
}
// This method is used by JWTAuthenticationFilter
@Transactional
public UserDetails loadUserById(Long id) {
User user = userRepository.findById(id).orElseThrow(
() -> new UsernameNotFoundException("User not found with id : " + id)
);
return UserPrincipal.create(user);
}
}
첫 번째 방법 loadUserByUsername()
은 Spring 보안에서 사용됩니다. 메소드 의 사용에 주의하십시오 findByUsernameOrEmail
이를 통해 사용자는 사용자 이름이나 이메일을 사용하여 로그인할 수 있습니다.
두 번째 방법 loadUserById()
은 JWTAuthenticationFilter
곧 정의할 것입니다.
4. JWT 생성 및 검증을 위한 유틸리티 클래스
다음 유틸리티 클래스는 사용자가 성공적으로 로그인한 후 JWT를 생성하고 요청의 Authorization 헤더에 전송된 JWT를 검증하는데 사용됩니다.
package com.record.backend.security;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
@Component
public class JwtTokenProvider {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Value("${app.jwtSecret}")
private String jwtSecret;
@Value("${app.jwtExpirationInMs}")
private int jwtExpirationInMs;
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal)authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(getSignKey())
.compact();
}
public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token)
.getBody();
/*
Claims claims = Jwts.parserBuilder()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();*/
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String authToken) {
try {
Jwts.parserBuilder().setSigningKey(getSignKey()).build().parseClaimsJws(authToken);
return true;
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty.");
}
return false;
}
private Key getSignKey() {
return Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
}
}
여기서 jwtSecret
과 jwtExpirationMs
는 application.yml 파일에 정의해놨는데 그걸 받아오도록 작성했다. jwt이 버전업되면서 안되는게 좀 있어서 수정했다.
application.yml 파일에 추가
app:
jwtSecret: jwtsigntutorialasdfasdfasdfasdfasdf
jwtExpirationInMs: 604800000
5. 커스텀 스프링 시큐리티 인증 필터
마지막으로 JwtAuthenticationFilter
요청에서 JWT 토큰을 가져오고 유효성을 검사하고 토큰과 연결된 사용자를 로드하고 이를 Spring Security에 전달하도록 작성해 보겠습니다.
package com.record.backend.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private CustomUserDetailsService customUserDetailsService;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
위의 경우 먼저 요청 헤더 filter에서 검색된 JWT를 구문 분석하고 사용자 ID를 가져옵니다.
Authorization그런 다음 데이터베이스에서 사용자 세부 정보를 로드하고 스프링 보안 컨텍스트 내에서 인증을 설정합니다.
위의 데이터베이스 적중 filter은 선택 사항입니다. JWT 클레임 내에서 사용자의 사용자 이름과 역할을 인코딩하고 UserDetailsJWT에서 해당 클레임을 구문 분석하여 객체를 생성할 수도 있습니다.
그러면 데이터베이스 히트를 피할 수 있습니다.그러나 데이터베이스에서 사용자의 현재 세부 정보를 로드하는 것은 여전히 도움이 될 수 있습니다.
예를 들어, 사용자의 역할이 변경되었거나 이 JWT를 만든 후 사용자가 자신의 비밀번호를 업데이트한 경우 이 JWT를 사용한 로그인을 허용하지 않을 수 있습니다.
그러나 데이터베이스에서 사용자의 현재 세부 정보를 로드하는 것은 여전히 도움이 될 수 있습니다. 예를 들어, 사용자의 역할이 변경되었거나 이 JWT를 만든 후 사용자가 자신의 비밀번호를 업데이트한 경우 이 JWT를 사용한 로그인을 허용하지 않을 수 있습니다.
6. 현재 로그인한 사용자에 접근하기 위한 사용자 정의 어노테이션 작성
Spring 보안은 @AuthenticationPrincipal
컨트롤러에서 현재 인증된 사용자에 액세스하기 위해 호출되는 주석을 제공합니다.
다음 CurrentUser
주석은 주석을 둘러싼 래퍼 @AuthenticationPrincipal
입니다.
package com.example.polls.security;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import java.lang.annotation.*;
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {
}
우리 프로젝트의 모든 곳에서 Spring Security 관련 주석에 너무 많이 얽매이지 않도록 메타 주석을 만들었습니다. 이것은 Spring Security에 대한 의존성을 감소시킨다. CurrentUser따라서 프로젝트에서 Spring Security를 제거하기로 결정했다면 간단히 주석 을 변경하여 쉽게 제거할 수 있습니다.
다음글에서 로그인 및 가입 api 작성을 해보겠습니다.