스프링 부트 핵심 가이드
[스프링 부트 핵심 가이드] Chapter 13장. 서비스의 인증과 권한 부여
어플리케이션을 개발하다 보면, 유저들이 인증과 인가를 해야 사용할 수 있는 기능들을 개발하게 된다
즉 보안 기능을 추가 해야 한다는 것이고, 보안을 위해서는 스프링에서 Spring Security가 있다
- 어플리케이션의 인증, 인가 등의 보안 기능을 제공하는 스프링 하위 프로젝트 중 하나
- 보안 관련된 많은 기능을 제공한다
보안 용어 이해
인증 (Authentication)
- 사용자가 누구인지 확인하는 단계다 (로그인)
- 확인이 되면, 서버는 유저에게 토큰을 전달한다
- 이 토큰을 이용하여, 유저는 원하는 리소스에 접근할 수 있게 된다
인가 (Authorization)
- 인증된 토큰, 즉 검증된 유저가 어플리케이션 내부의 리소스에 접근할 때 유저가 해당 리소스에 접근할 권리가 있는지 확인하는 과정이다
- 예) 일반 유저는, 관리 권한을 갖지 못 하여, 관리 리소스에 접근할 수 없다
- 인증 단계에서 발급 받은 토큰은 인가 내용을 포함하고 있다
접근 주체 (principal)
- 어플리케이션을 사용하는 주체다
- 접근 주체는 사용자, 디바이스, 시스템이 될 수 있다
- 어플리케이션은 인증 과정을 통해 접근 주체가 신뢰할 수 있는지 확인 후, 인가 과정을 통해 접근 주체에게 부여된 권한을 확인하는 과정을 거친다
스프링 시큐리티의 동작 구조
스프링 시큐리티는 서블릿 필터 (Servlet Filter)를 기반으로 동작을 한다
- DispatcherServlet 앞에 필터가 배치되어 있다
- 필터 체인 (FilterChain)은 서블릿 컨테이너에서 관리하는 ApplicationsFilterChain을 의미한다
- 클라이언트에서 요청을 보내면 서블릿 컨테이너 (ApplicationsFilterChain)은 URI를 확인해서 필터와 서블릿을 매핑한다
- 서블릿 컨테이너의 필터 사이에서 동작시키기 위해 DelegatingFilterProxy를 사용한다
- 이는 서블릿 컨테이너의 생명주기와 스프링 어플리케이션 컨텍스트 (Application Context) 사이에서 다리 역할을 수행하는 필터 구현체다
- 표준 서블릿 필터를 구현하고 있고, 역할을 위임할 필터체인 프록시 (FilterChainProxy, 위에서는 Bean Filter)를 내부에 가지고 있다
- 필터체인 프록시는 스프링 부트의 자동 설정에 의해 자동 생성된다
- 스프링 시큐리티에서 제공하는 필터로서 보안 필터체인 (SecurityFilterChain)을 통해 많은 보안 필터를 사용할 수 있다
- 사용할 수 있는 보안 필터 체인은 List 형식으로 담을 수 있다
- 보안 필터체인은 WebSecurityConfigurerAdapter 클래스를 상속받아 설정할 수 있다
- 여러 보안 필터체인을 만들기 위해서는 위 클래스를 상속 받아야 한다
- WebSecurityConfiguererAdapter 클래스에는 @Order 어노테이션을 통해 우선순위를 지정 되어 있고 2개 이상 클래스를 생성했을 때에는 @Order 어노테이션을 사용하여 우선순위를 정하는 것이 중요하다
- 클라이언트로부터 Http Request, 요청을 받으면 서블릿 필터에서 SecurityFilterChain으로 작업이 맡겨지고, 위에 그림 같은 경우 UsernamePasswordAuthenticationToken에서 인증처리를 한다
- AuthenticationFilter는 요청 객체 (HttpServletRequest)에서 username과 password를 추출해서 토큰을 생성한다
- 생성한 토큰을 AuthenticationManager에 전달을 한다. AuthenticationManager는 인터페이스고, 일반적으로 사용되는 구현체는 ProviderManager이다
- ProviderManager는 인증을 위해 AuthenticationProvider(s)로 토큰을 전달한다
- AuthenticationProvider(s)는 토큰의 정보를 UserDetailsService에 전달한다
- UserDetailsService는 전달받은 정보를 통해 DB에서 일치하는 사용자를 찾아 UserDetails 객체를 생성한다
- 생성된 UserDetails 객체는 AuthenticationProvider로 다시 전달된다
- 해당 AuthenticationProvider에서 인증을 수행하고 성공하게 되면 ProviderManager로 권한을 담은 토큰을 전달한다
- ProviderManager는 검증된 토큰을 AuthenticationFilter로 전달한다
- AuthenticationFilter는 검증된 토큰을 SecurityContextHolder에 있는 SecurityContext에 전달한다
JWT (JSON Web Token)
정보를 JSON 형태로 안전하게 전송하기 위한 토큰이다
- URL로 이용할 수 있는 문자열로만 구성돼 있다
- 디지털 서명이 적용되어 있어 신뢰할 수 있다
- JWT는 주로 서버와의 통신에서 권한 인가를 위해 사용되고, 문자열로만 구성되어 있다
아래는 JWT의 구조다
- Header (헤더)
- 검증과 관련된 내용을 담고 있다
- 해싱 알고리즘과, 토큰의 타입을 지정한다
{
"alg" : "HS256",
"typ" : "JWT"
}
- Payload (내용)
- 토큰에 담긴 정보를 포함한다
- 이곳에 포함된 속성들은 클레임 (Claim)이라고 하며, 세가지로 분류된다
- 등록된 클레임 (Registered Claims)
- 공개 클레임 (Public Claims) : 키 값을 마음대로 정의할 수 있다
- 비공개 클레임 (Private Claims) : 통신 간에 상호 합의되고 등록된 클레임과 공개된 클레임이 아닌 클레임을 의미한다
- 등록된 클레임
- iss : JWT의 발급자 (Issuer) 주체를 나타낸다. iss의 값은 문자열이나 URI를 포함하는 대소문자를 구분하는 문자열이다
- sub : JWT의 제목 (Subject)이다
- aud : JWT의 수신인 (Audience)이다. JWT를 처리하려는 각 주체는 해당 값으로 자신을 식별해야 한다. 요청을 처리하는 주체가 'aud' 값으로 자신을 식별하지 않으면 JWT는 거부된다
- exp : JWT의 만료시간 (Expiration)이다. 시간은 NumericDate 형식으로 지정해야 한다
- nbf : 'Not Before'를 의미한다
- iat : JWT가 발급된 시간 (Issued at) 이다
- jti : JWT의 식별자 (JWT ID) 이다. 주로 중복 처리를 방지하기 위해 사용된다
{
"sub" : "example",
"exp" : "231321321321",
"userId" : "hello",
"username" : "hi"
}
- Signature (서명)
- JWT의 서명 부분은 인코딩된 헤더, 인코딩된 내용, 비밀키, 헤더의 알고리즘 속성값을 가져와 생선된다
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
스프링 시큐리티와 JWT 적용
의존성 추가
# pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
UserDetails와 UserDetailsService 구현
- User 엔티티를 만든다
// entity/User.java
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
@Column(nullable=false, unique=true)
private String uid;
@JsonProperty(access = Access.WRITE_ONLY)
@Column(nullable = false, length = 100)
private String password;
@Column(nullable = false)
private String name;
@ElementCollection(fetch = FetchType.EAGER)
@Builder.Default
private List<String> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return role.stream()
.map(SimpleGrantedAuthority::new) // 스프링 security에서 제공하는 role 관련된 authority를 사용
.collect(Collectors.toList());
}
@JsonProperty(access = Access.WRITE_ONLY)
@Override
public String getUsername() {
return this.uid;
}
@JsonProperty(access = Access.WRITE_ONLY)
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonProperty(access = Access.WRITE_ONLY)
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonProperty(access = Access.WRITE_ONLY)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonProperty(access = Access.WRITE_ONLY)
@Override
public boolean isEnabled() {
return true;
}
}
UserDetails 인터페이스의 메서드
- getAuthorities() : 계정이 가지고 있는 권한 목록을 리턴한다
- getPassword() : 계정의 비밀번호를 리턴한다
- getUsername() : 계정의 이름을 리턴한다. 일반적으로는 아이디를 리턴
- isAccountNonExpired() : 계정이 만료됐는지 리턴한다. true는 만료되지 않았다는 의미다
- isAccountNonLocked() : 계정이 잠겨있는지 리턴한다. true는 잠기지 않았다는 의미다
- isCredentialsNonExpired() : 비밀번호가 만료됐는지 리턴한다. true는 만료되지 않았다는 의미다
- isEnabled() : 계정이 활성화되어 있는지 리턴한다. true는 활성화 상태를 의미한다
UserRepository 구현
- 기존에 레포지토리를 작성하던 방법과 동일하다
public interface UserRepository extends JpaRepository<User, Long> {
User getByUid(String uid);
}
UserDetailsServiceImpl 구현
@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final Logger LOGGER = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LOGGER.info("[loadUserByUsername] loadUserByUsername 수행. username : {}", username);
return userRepository.getByUid(username).
orElseThrow(() -> new RuntimeException("유저를 찾을 수 없음"));
}
}
JwtTokenProvider 구현
- JWT 토큰을 생성하는 데 필요한 정보를 UserDetails에서 가져올 수 있다
- JWT 토큰을 생성하는 JwtTokenProvider를 생성해야 한다
@Component
@RequiredArgsConstructor
public class TokenProvider {
private final Logger LOGGER = LoggerFactory.getLogger(JwtTokenProvider.class);
private final UserDetailsServiceImpl userDetailsServiceImpl;
private static final String KEY_ROLES = "roles";
private static final long VALIDATE_TIME = 1 * 60 * 60 * 1000L; // 1시간
@Value("${spring.jwt.secret}")
private String secretKey;
@PostConstruct
protected void init() {
LOGGER.info("[init] JwtTokenProvider 내 secretKey 초기화 시작");
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));
LOGGER.info("[init] JwtTokenProvider 내 secretKey 초기화 완료");
}
// 토큰 생성 매서드
public String generateToken(String username, List<String> roles) {
LOGGER.info("[generateToken] 토큰 생성 시작");
Claims claims = Jwts.claims().setSubject(username);
claims.put(KEY_ROLES, roles); // key value로 저장
var now = new Date();
var expiredDate = new Date(now.getTime() + VALIDATE_TIME);
String token = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now) // 토큰 생성시간
.setExpiration(expiredDate) // 토큰 만료 시간
.signWith(SignatureAlgorithm.HS512, this.secretKey) // 사용할 암호화 알고리즘, 비밀키
.compact();
LOGGER.info("[generateToken] 토큰 생성 완료");
return token;
}
public Authentication getAuthentication(String jwt) {
LOGGER.info("[getAuthentication] 토큰 인증 정보 조회 시작");
UserDetails userDetails = memberServiceImpl.loadUserByUsername(getUsername(jwt));
LOGGER.info("[getAuthentication] 토큰 인증 정보 조회 완료, UserDetails UserName : {}"
, userDetails.getUsername());
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
public String getUsername(String token) {
LOGGER.info("[getUsername] 토큰 기반 회원 구별 정보 추출");
// 토큰이 유효한지 확인
String info = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
LOGGER.info("[getUsername] 토큰 기반 회원 구별 정보 추출 완료, info : {}", info);
return info;
}
public String resolveToken(HttpServletRequest request) {
LOGGER.info("[resolveToken] HTTP 헤더에서 Token 값 추출");
return request.getHeader("X-AUTH-TOKEN");
}
public boolean validateToken(String token) {
LOGGER.info("[validateToken] 토큰 유효 체크 시작");
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
//토큰 만료 시간이 현재보다 이전인지 아닌지 만료 여부 확인
return !claims.getBody().getExpiration().before(new Date());
// 파싱하는 과정에서 토큰 만료 시간이 지날 수 있다, 만료된 토큰을 확인할 때에
} catch (Exception e) {
LOGGER.info("[validateToken] 토큰 유효 체크 예외 발생");
return false; // 토큰이 빈 문자열이면 false
}
}
}
- secretKey가 필요함으로 application.properites에
spring.jwt.secret={secret 키}
를 추가한다- 위에 경우 인코딩을 init() 에서 하지만, 따로 인코딩을 해서 application.properties에 저장해 놔도 된다
- 위에 init() 은 JwtTokenProvider에 @Component 어노테이션이 지정되어 있고, init()에는 @PostConstruct가 지정되어 있어, 어플리케이션이 가동되면 자동으로 init()이 실행된다
- getAuthentication()
- 필터에서 인증이 성공했을 때 SecurityContextHolder에 저장할 Authentication을 생성하는 역할이다
- UsernamePasswordAuthenticationToken을 사용하는 것이 편하다
- UsernamePasswordAuthenticationToken은 AbstractAuthenticationToken을 상속받고 있고, AbstractAuthenticationToken은 Authentication 인터페이스의 구현체다
- 이를 사용하기 위해서는 UserDetails가 필요하고, 이 객체는 UserDetailsService를 통해 가지고 온다
- resolveToken()
- HttpServletRequest를 파라미터로 받아 헤더 값으로 전달된 'X-AUTH-TOKEN' 값을 가져와 리턴한다
JwtAuthenticationFilter 구현
- JWT 토큰으로 인증하고 SecurityContextHolder에 추가하는 필터를 설정하는 클래스다
// OncePerRequestFilter 한 요청 당 한번 필터가 실행이 된다
// 요청이 들어올 때마다 토큰이 있는지 없는지 확인
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final Logger LOGGER = LoggerFactory.getLogger(JwtTokenProvider.class);
private final TokenProvider tokenProvider;
public static final String TOKEN_HEADER = "Authorization";
//인증 타입
public static final String TOKEN_PREFIX = "Bearer ";
public JwtAuthenticationFilter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = getTokenFromRequest(request);
LOGGER.info("[doFilterInternal] token 값 추출 완료. token : {}", token);
LOGGER.info("[doFilterInternal] token 값 유효성 체크 시작");
if (token != null && tokenProvider.validateToken(token)) {
// 토큰 유효성 검증
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
LOGGER.info("[doFilterInternal] token 값 유효성 체크 완료");
}
filterChain. doFilter(request, response);
}
}
- 대표적으로 많이 사용되는 상속 객체는 GenericFilterBean과 OncePerRequestFilter이다
- GenericFilterBean은 기존 필터에서 가져올 수 없는 스프링의 설정 정보를 가져올 수 있게 확장된 추상 클래스다
- 다만 서블릿은 사용자의 요청을 받으면 서블릿을 생성해서 메모리에 저장해두고 동일한 클라이언트의 요청을 받으면 재활요하는 구조여서 GenericFilterBean을 상속 받으면 RequestDispatcher에 의해 다른 서블릿으로 디스패치가 되면서 필터가 두 번 실행되는 현상이 발생할 수 있다
- 이 문제를 해결하기 위해 등장한 것이 OncePerRequestFilter이다
SecurityConfiguration 구현
- 스프링 시큐리티와 관련된 설정이다
- 대표적으로 WebSecurityConfigurerAdapter를 상속받는 Configuration 클래스를 구현한다
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 어노테이션으로 권한 부여
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final TokenProvider tokenProvider;
@Autowired
public SecurityConfiguration(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.httpBasic().disable()
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 토큰 없이 접근 가능
.antMatchers("/sign-api/sign-in", "/sign-api/sign-up", "/sign-api/exception").permitAll()
// /product로 시작하는 경로의 GET 요청은 모두 허용
.antMatchers(HttpMethod.GET, "/product/**").permitAll()
// exception 단어가 들어간 경로는 모두 허용
.antMatchers("**exception**").permitAll()
// 기타 요청은 인증된 권한을 가진 사용자에게 허용
.anyRequest().hasRole("ADMIN")
.and()
.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler())
.and()
.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.and()
.addFilterBefore(new JwtAuthenticationFilter(tokenProvider),
UsernamePasswordAuthenticationFilter.class);
}
@Override
public void configure(final WebSecurity web) throws Exception {
// 주로 개발 관련
webSecurity.ignoring().antMatchers("/v2/api-docs", "/swagger-resources/**",
"/swagger-ui/**", "/webjars/**", "/swagger/**",
"/sign-api/exception");
}
}
- 스프링 시큐리티의 설정은 대부분 HttpSecurity를 통해 진행한다
- 리소스 접근 권한 설정
- 인증 실패 시 발생하는 예외 처리
- 인증 로직 커스터마이징
- csrf, cors 등의 스프링 시큐리티 설정
- httpBasic().disable()
- UI를 사용하는 것을 기본값으로 가진 시큐리티 설정을 비활성화한다
- csrf().disable()
- REST API에서는 CSRF 보안이 필요 없기 때문에 비활성화 하는 로직이다
- CSRF는 Cross-Site Request Forgery의 줄임말로 '사이트 간 요청 위조'를 의미한다
- csrf() 메서드는 기본적으로 CSRF 토큰을 발급해서 클라이언트로부터 요청을 받을 때마다 토큰을 검증하는 방식으로 동작한다
- 브라우저 사용 환경이 아니면 비활성화를 해도 된다
- sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- REST API 기반 어플리케이션의 동작 방식을 설정한다
- 진행 중인 프로젝트에서는 JWT 토큰으로 인증을 처리하며, 세션을 사용하지 않기 때문에 STATELESS로 설정한다
- authorizeRequest()
- 어플리케이션에 들어오는 요청에 대한 사용 권한을 체크한다
- antMatchers() 메서드는 antPatter을 통해 권한을 설정하는 역할이다
- exceptionHandling().accessDeniedHandler()
- 권한을 확인하는 과정에서 통과하지 못하는 예외가 발생할 경우 예외를 전달한다
- exceptionHandling().authenticationEntryPoint()
- 인증 과정에서 예외가 발생할 경우 예외를 전달한다
각 두 exceptionHandling은 CustomAccessDeniedHandler와 CustomAuthenticationEntryPoint로 예외를 전달한다
커스텀 AccessDeniedHandler, AuthenticationEntryPoint 구현
- 인증과 인가 과정의 예외 상황에서 CustomAccessDeniedHandler와 CustomAuthenticationEntryPoint로 예외를 전달한다
- CustomAccessDeniedHandler
- 엑세스 권한이 없는 리소스에 접근할 경우 발생하는 에외
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private final Logger LOGGER = LoggerFactory.getLogger(JwtTokenProvider.class);
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDniedException exception) throws IOException {
LOGGER.info("[handle] 접근이 막혔을 경우 경로 리다이렉트");
response.sendRedirect("/sign-api/exception");
}
}
- CustomAuthenticationEntryPoint
- commence() 메서드는 예외 처리를 위해 리다이렉트가 아니라 직접 Response를 생성해서 클라이언트에게 응답을 한다
- 메세지를 담기 위해 EntryPointErrorResponse 객체를 사용해 메세지를 설정한다
- response에 상태 코드와 콘텐츠 타입 등을 설정한 후 ObjectMapper를 사용해 EntryPointErrorResponse 객체를 바디 값으로 파싱한
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final Logger LOGGER = LoggerFactory.getLogger(JwtTokenProvider.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException ex) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
LOGGER.info("[commence] 인증 실패로 response.sendError 발생");
EntryPointErrorResponse entryPointErrorResponse = new EntryPointErrorResponse();
entryPointErrorResponse.setMsg("인증이 실패하였습니다");
reponse.setStatus(401);
reponse.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(objectMapper.writeValueAsString(entryPointErrorResponse));
}
}
// EntryPointErrorResponse
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class EntryPointErrorResponse {
private String msg;
}
회원가입
// SignService
public interface SignService {
SignUpResultDto signUp(String id, String password, String name, String role);
SignInResultDto signIn(String id, String password) throws RuntimeException;
}
// SignServiceImpl
@Service
public class SignServiceImpl implements SignService {
private final Logger LOGGER = LoggerFactory.getLogger(SignServiceImpl.class);
public UserRepository userRepository;
public TokenProvider tokenProvider;
public PasswordEncoder passwordEncoder;
@Autowired
public SignServiceImpl(UserRepository userRepository, TokenProvider tokenProvider, PasswordEncoder passwordEncoder){
this.userRepository = userRepository;
this.tokenProvider = tokenProvider;
this.passwordEncoder = passwordEncoder;
}
@Override
SignUpResultDto signUp(String id, String password, String name, String role) {
LOGGER.info("[getSignUpResult] 회원 가입 정보 전달");
User user;
if (role.equalsIgnoreCase("admin")) {
user = User.builder()
.uid(id)
.name(name)
.password(passwordEncoder.encode(password))
.roles(Collections.singletonList("ROLE_ADMIN"))
.build();
} else {
user = User.builder()
.uid(id)
.name(name)
.password(passwordEncoder.encode(password))
.roles(Collections.singletonList("ROLE_USER"))
.build();
}
User savedUser = userRepository.save(user);
SignUpResultDto signUpResultDto = new SignInResultDto();
LOGGER.info("[getSignUpResult] userEntity 값이 들어왔는지 확인 후 결과값 주입");
if(!savedUser.getName().isEmpty()) {
LOGGER.info("[getSignUpResult] 정상 처리 완료");
setSuccessResult(signUpResultDto);
} else {
LOGGER.info("[getSignUpResult] 실패 처리 완료");
setFailResult(SignUpResultDto);
}
return signUpResultDto;
}
@Override
public SignInResultDto signIn(String id, String password) throws RuntimeException {
LOGGER.info("[getSignInResult] signDataHandler로 회원 정보 요청");
User user = userRepository.getByUid(id);
LOGGER.info("[getSignInResult] Id : {}", id);
LOGGER.info("[getSignInResult] 패스워드 비교 수행");
if(!passwordEncoder.matches(password, user.getPassword())) {
throw new RuntimeException("비밀번호가 일치하지 않습니다");
}
LOGGER.info("[getSignInResult] 패스워드 일치");
LOGGER.info("[getSignInResult] SignInResultDto 객체 생성");
SignInResultDto signInResultDto = SignInResultDto.builder()
.token(tokenProvider.createToken(String.valueOf(user.getUid()),
user.getRoles()))
.build();
LOGGER.info("[getSignInResult] SignInResultDto 객체에 값 주입");
setSuccessResult(signInResultDto);
return signInResultDto;
}
private void setSuccessResult(SignUpResultDto result) {
result.setSuccess(true);
result.setCode(CommonResponse.SUCCESS.getCode());
result.setMsg(CommonResponse.SUCCESS.getMsg());
}
private void setFailResult(SignUpResultDto result) {
result.setSuccess(false);
result.setCode(CommonResponse.FAIL.getCode());
result.setMsg(CommomResponse.FAIL.getMsg());
}
}
// PasswordEncoderConfiguration
@Configuration
public class PasswordEncoderConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
로그인
- id를 기반으로 UserRepository에서 User 엔티티를 가져온다
- PasswordEncoder를 사용해 데이터 베이스에 저장되어 있던 비밀번호와 입력받은 비밀번호가 일치하는지 확인
- 비밀번호가 일치해서 인증을 통과하면 TokenProvider를 통해 id와 role 값을 전달해서 토큰을 생성한 후 Response에 담아 전다
// CommonResponse
package com.springboot.security.common;
@Getter
@Setter
public enum CommonResponse {
SUCCESS(0, "Success"), FAIL(-1, "FAIL");
int code;
String msg;
}
// SignController
@RestController
@RequestMapping("/sign-api")
public class SignController {
private final Logger LOGGER = LoggerFactory.getLogger(SignController.class);
private final SignService signService;
@Autowired
public SignController(SignService signService) {
this.signService = signService;
}
@PostMapping(value = "/sign-in")
public SignInResultDto signIn(
@ApiParam(value = "ID", required=true) @RequestParam String id,
@ApiParam(value = "Password", required=true) @RequestParam String password)
throws RuntimeException {
LOGGER.info("[signIn] 로그인을 시도하고 있습니다. id : {}, pw : ****", id);
SignInResultDto signInResultDto = signService.signIn(id, password);
if (signInResultDto.getCode() == 0) {
LOGGER.info("[signIn] 정상적으로 로그인되었습니다. id : {}, token : {}",
id, signInResultDto.getToken());
}
return signInResultDto;
}
@PostMapping(value = "/sign-up")
public SignUpResultDto signUp(
@ApiParam(value = "ID", required=true) @RequestParam String id,
@ApiParam(value = "비밀번호", required=true) @RequestParam String password,
@ApiParam(value = "이름", required=true) @RequestParam String name,
@ApiParam(value = "권한", required=true) @RequestParam String role) {
LOGGER.info("[signUp] 회원가입을 합니다. id : {}, password : ****, name : {}, role : {}",
id, name, role);
SignUpResultDto signUpResultDto = signService.signUp(id, password, name, role);
LOGGER.info("[signUp] 회원가입을 완료했습니다. id : {}" , id);
return signUpResultDto;
}
@GetMapping(value="/exception")
public void exceptionTest() throws RuntimeException {
throw new RuntimeException("접근이 금지되었습니다");
}
@ExceptionHandler(value = RuntimeException.class)
public ResponseEntity<Map<String, String>> ExceptionHandler(RuntimeException e) {
HttpHeaders responseHeaders = new HttpHeaders();
//responseHeaders.add(HttpHeaders.CONTENT_TYPE, "application/json");
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
LOGGER.error("ExceptionHandler 호출, {}, {}", e.getCause(), e.getMessage());
Map<String, String> map = new HashMap<>();
map.put("error type", httpStatus.getReasonPhrase());
map.put("code", "400");
map.put("message", "에러 발생");
return new ResponseEntity<>(map, responseHeaders, httpStatus);
}
}
// SignUpResultDto
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SignUpResultDto {
private boolean success;
private int code;
private String msg;
}
// SignInResultDto
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SignInResultDto {
private String token;
@Builder
public SignInResultDto(boolean success, int code, String msg, String token) {
super(success, code, msg);
this.token = token;
}
}
'독서 > 스프링 부트 핵심 가이드' 카테고리의 다른 글
[스프링 부트 핵심 가이드] Chapter 12장. 서버 간 통신 (0) | 2023.10.08 |
---|---|
[스프링 부트 핵심 가이드] Chapter 11장. 액추에이터 활용하기 (0) | 2023.10.08 |
[스프링 부트 핵심 가이드] Chapter 10장. 유효성 검사와 예외 처리 (0) | 2023.10.01 |
[스프링 부트 핵심 가이드] Chapter 9장. 연관 관계 매핑 (0) | 2023.09.24 |
[스프링 부트 핵심 가이드] Chapter 8장. Spring Data JPA 활용 (1) | 2023.09.16 |