들어가기 앞서..
Spring Security + jwt 최신 버전으로 구현하나요?
Spring Security와 JWT를 이용한 로그인 구현을 해보기 위해 많은 블로그와 강의를 찾아보았지만, Spring Security의 version upgrade로 인해, 기존에 인프런에 있는 강의들이나 블로그에 있는 글들이 deprecated 된 메서드를 사용해서 구현한 예제만 있었다.
최신 버전의 Security 적용과 jwt 의 0.11.2 버전을 사용한 예제를 적용시키는 로그인 예제를 구현하기 위해서 다른 블로그글들을 참고해서 해당 글을 작성하게 되었다.
구현 환경 버전은 어떻게 되나요?
우선 InteliJ를 이용할 것이고, gradle 기반의 Springboot 를 사용할 것이다.
SpringBoot 버전은 3.0.4이며, JDK 는 17을 사용할 것이고, Spring Security 는 6.0.0 이상을 사용할 것이다.
H2 DB를 사용할 것이고, jwt는 0.11.2 를 사용할 것이다.
별도의 화면은 만들어주지 않을것이기에, 테스트는 Postman을 사용해서 테스트 해볼것이다.
우선 해당 로그인 예제에 들어가기 앞서, Security와 JWT에 대해서는 하단의 글들을 참고하면 좋다.
[개념] Spring Security 란 무엇인가?
Spring Security란? Spring 기반의 애플리케이션의 보안(인증, 권한, 인가)을 담당하는 스프링 하위 프레임워크이다. Spring Security는 인증(Authentication), 권한(Authorization)에 대한 부분을 Filter의 흐름에 따
soohykeee.tistory.com
[개념] JWT란 무엇인가?
JWT를 공부하기 전, 우선으로 Session 인증 방식과 Token 인증 방식에 대해 알아둘필요가 있다. [개념] Session 기반 인증 vs Token 기반 인증 [개념] Session 기반 인증 vs Token 기반 인증 인증 방식 종류 JWT에
soohykeee.tistory.com
구현
Setting
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.0.4'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
}
tasks.named('test') {
useJUnitPlatform()
}
application.yml
server:
port: 8080
servlet:
context-path: /
encoding:
charset: UTF-8
enabled: true
force: true
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:tcp://localhost/~/jwttest
username: sa
password:
jpa:
properties:
hibernate:
format_sql: true
hibernate:
ddl-auto: create #create update none
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
jwt:
secret:
key: H+MbQeThWmZq3t6w9z$C&F)J@NcRfUjX
jwt secret key에 대해 작성하기 힘들다면, 아래의 링크에서 bit를 선택하면 랜덤하게 key 값을 만들어준다.
256bit 이상으로 생성해줘야 한다.
Encryption Key Generator
Encryption Key Generator The all-in-one ultimate online toolbox that generates all kind of keys ! Every coder needs All Keys Generator in its favorites ! It is provided for free and only supported by ads and donations.
www.allkeysgenerator.com
패키지 구조
위와 같이 구조를 만들어서 적용시켜줄 것이다.
사용자(Member) 구현
인증과 Security 구현 전, 사용자를 먼저 구현해줄 것이다.
Member Entity
package com.example.jwttest.member;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String account; // 아이디
private String password; // 비밀번호
private String name; // 이름
private String nickname; // 닉네임
@Column(unique = true)
private String email; // 이메일
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Builder.Default
private List<Authority> roles = new ArrayList<>(); // 사용자 권한(목록)
public void setRoles(List<Authority> role) {
this.roles = role;
role.forEach(o -> o.setMember(this));
}
}
Authority Entity
package com.example.jwttest.member;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Authority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@JsonIgnore
private Long id;
private String name;
@JoinColumn(name = "member")
@ManyToOne(fetch = FetchType.LAZY)
@JsonIgnore
private Member member;
public void setMember(Member member) {
this.member = member;
}
}
@JsonIgnore 어노테이션을 붙이게 되면, 데이터를 주고 받을 떄 해당 데이터는 Ignore (무시) 되어서 response 값에 보이지 않게 된다.
MemberRepository
package com.example.jwttest.member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Transactional
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByAccount(String account);
// Optional<Member> findByEmail(String email);
}
해당 예제에서는 Account를 아이디로 생각하고 개발했기에 findByAccount를 해주었다. 하지만, 앞서 email 도 unique로 해주었기에 email을 아이디로 사용해도 된다.
CustomUserDetails
package com.example.jwttest.security;
import com.example.jwttest.member.Member;
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.stream.Collectors;
public class CustomUserDetails implements UserDetails {
private final Member member;
public CustomUserDetails(Member member) {
this.member = member;
}
public final Member getMember() {
return member;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return member.getRoles().stream().map(
o -> new SimpleGrantedAuthority(o.getName())
).collect(Collectors.toList());
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getAccount();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
다른곳에서 사용하는 예제를 보게되면, UserDetails를 실제 사용하는 Member 엔티티나, User 엔티티에 상속해서 사용하는 경우도 있다.
하지만, 위의 방법으로 사용하게 된다면 실제 유저를 담는 엔티티가 오염되어, 구분하기 힘들 수 있고, 사용이 어려워질 수 있기에 따로 CustomUserDetails란 클래스를 생성해서 해당 클래스에 UserDetails를 상속받아 사용했다.
우리는 JWT를 이용한 인증을 하기에
isAccountNonExpired(), isAccountNonLocked(), isCredentialsNonExpired(), isEnabled() 를 true로 해주었다.
JpaUserDetailsService
package com.example.jwttest.security;
import com.example.jwttest.member.Member;
import com.example.jwttest.member.MemberRepository;
import lombok.RequiredArgsConstructor;
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;
@Service
@RequiredArgsConstructor
public class JpaUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member member = memberRepository.findByAccount(username).orElseThrow(
// DB에 유효하지 않은 유저로 로그인 시도 했을 경우 Exception
() -> new UsernameNotFoundException("Invalid Authentication !")
);
return new CustomUserDetails(member);
}
}
Spring Security의 UserDetailsService는 UserDetails 정보를 토대로 유저 정보를 불러올 때 사용한다.
JPA를 활용하여 DB에 접근해서 유저 정보를 조회해서 CustomUserDetails에 넘겨준다.
-> 이때, DB에 접근해서 정보를 조회하는데, 유효하지 않은 Account 라면 Exception을 발생시킨다.
JWT 설정
이제 JWT 생성 및 검증하기 위한 구현을 해줄 것이다.
JwtProvider
package com.example.jwttest.security;
import com.example.jwttest.member.Authority;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.List;
@RequiredArgsConstructor
@Component
// JWT를 생성 + 검증 하는 클래스
public class JwtProvider {
@Value("${jwt.secret.key}")
private String salt;
private Key secretKey;
// 만료시간 = 1hour
private final Long exp = 1000L * 60 * 60;
private final JpaUserDetailsService userDetailsService;
@PostConstruct
protected void init() {
secretKey = Keys.hmacShaKeyFor(salt.getBytes(StandardCharsets.UTF_8));
}
// 토큰 생성
public String createToken(String account, List<Authority> roles) {
Claims claims = Jwts.claims().setSubject(account);
claims.put("roles", roles);
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + exp))
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}
// 권한 정보 획득
// Spring Security 인증과정에서 권한확인을 위한 기능
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getAccount(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// Token 에 담겨있는 유저 Account 획득
public String getAccount(String token) {
return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().getSubject();
}
// Authorization Header 를 통해 인증
public String resolveToken(HttpServletRequest request) {
return request.getHeader("Authorization");
}
// Token 검증
public boolean validateToken(String token) {
try {
// Bearer 검증
// + equalsIgnoreCase() -> 대소문자 구분없이 문자열 자체만으로 비교
if (!token.substring(0, "BEARER ".length()).equalsIgnoreCase("BEARER ")) {
return false;
} else {
token = token.split(" ")[1].trim();
}
Jws<Claims> claims = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
// 만료되었다면 false, 만료전이라면 true
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
}
@Value 어노테이션을 통해서, 앞서 application.yml에서 작성해주었던 secret key에 접근하도록 설정해준다. 그렇게하면 salt 필드에 해당 값이 저장이 된다.
import org.springframework.beans.factory.annotation.Value;
또한 위의 작성한 것을 import하지 않고 다른 value를 사용하는 어노테이션을 import하면, 오류가 날 수 있으니 주의하자
우선 해당 예제에서는 refreshToken을 구현하지 않았지만, Token에도 만료시간을 부여해야 하기에, exp 필드를 생성해서 1시간의 만료시간이 적용되도록 설정해놓는다.
@PostConstruct는 의존성 주입이 완료 된 후 실행되어야 하는 메서드에 사용하는 어노테이션으로, 다른 리소스에서 호출되지 않아도 수행된다.
- 호출순서 : 생성자 호출 -> 의존성 주입완료 -> @PostConstruct
해당 어노테이션을 사용하는 이유는, 생성자가 호출되었을 때 bean은 초기화 전이다. 하지만 이것을 사용하면 bean이 초기화 됨과 동시에 의존성을 확인할 수 있다. 추가적으로, bean lifeCycle에서 여러번 초기화되는 문제를 방지하기 위해서 오직 한번만 수행된다.
createToken 메서드를 사용해서 로그인한 account와 roles를 claim에 넣어준 후, 만료시간과 발급시간, 해시알고리즘과 secret key 값을 넣어 Token을 생성해서 return 해준다.
요청이 올때, 인증과정에서 권한을 확인하기 위해서 token에 담겨져 있는 유저 account를 획득하는 getAccount 메서드와 이를 앞서 JpaUserDetailsService 클래스에 정의해준 loadUserByUsername 메서드를 활용하여 DB에 해당 account가 있는지 확인하고 인증정보를 넘겨준다.
request의 header에서 Authorization 정보를 가져와 넘겨주는 resolveToken 메서드도 생성해준다.
또한 실제 Token이 유효한 Token 인지 검증하기 위해서 validateToken 메서드를 생성해준다. token이 들어오게 되면 우선적으로 String.substring 메서드로 해당 token이 Bearer 인지 검증을 먼저한다. 그 다음, Bearer 뒤에 token 정보를 split으로 분리한 후, 해당 token에서 담아두었던 claim 내의 만료시간을 get으로 꺼내온 후, token의 만료 여부를 체크한다.
Security Config
JwtAuthenticationFilter
package com.example.jwttest.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
public JwtAuthenticationFilter(JwtProvider jwtProvider) {
this.jwtProvider = jwtProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = jwtProvider.resolveToken(request);
if (token != null && jwtProvider.validateToken(token)) {
// check access token
token = token.split(" ")[1].trim();
Authentication auth = jwtProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
}
Jwt 가 유효성을 검증하는 Filter로, Filter 를 적용함으로써 servlet 에 도달하기 전, 검증을 완료할 수 있다. 해당 클래스에서 상속받는 OncePerRequestFilter 는 단 한번의 요청에 단 한번만 동작하도록 보장된 필터이다.
SecurityConfig
package com.example.jwttest.security;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import java.io.IOException;
import java.util.List;
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {
private final JwtProvider jwtProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ID, password 문자열을 Base64로 인코딩하여 전달하는 구조
.httpBasic().disable()
// Cookie 기반 인증이 아닌, JWT 기반 인증이기에 csrf 사용 X
.csrf().disable()
// CORS 설정
.cors(c -> {
CorsConfigurationSource source = request -> {
// cors 허용 패턴
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("*"));
config.setAllowedMethods(List.of("*"));
return config;
};
c.configurationSource(source);
})
// Spring Security Session 정책 -> Session 생성 및 사용 X
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 조건 별 요청 허용 or 제한 설정
.authorizeHttpRequests()
// register (회원가입), login (로그인) 에는 모두가 접근이 가능하도록 허용
.requestMatchers("/register", "/login").permitAll()
// admin 관련 페이지는 ADMIN 권한인 유저만 접근 가능
.requestMatchers("/admin/**").hasRole("ADMIN")
// user 관련 페이지는 USER 권한인 유저만 접근 가능
.requestMatchers("/user/**").hasRole("USER")
// 위 외의 요청들은 모두 거부
.anyRequest().denyAll()
.and()
// addFilterBefore(A, B) -> A가 B보다 먼저 실행
// 즉, JWT 인증 필터를 적용
.addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class)
// Exception handling 추가
.exceptionHandling()
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 권한 문제 발생시
response.setStatus(403);
response.setCharacterEncoding("utf-8");
response.setContentType("text/html; charset=UTF-8");
response.getWriter().write("권한이 없는 사용자입니다.");
}
})
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 인증 문제 발생 시
response.setStatus(401);
response.setCharacterEncoding("utf-8");
response.setContentType("text/html; charset=UTF-8");
response.getWriter().write("인증되지 않은 사용자입니다.");
}
});
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
.addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class)
-> 해당 부분을 작성해주었는데, 앞서 우리는 JWT를 검증하기 위한 Filter인 JwtAuthenticationFilter 를 생성해주었다.
그렇다면, 해당 필터 적용은 언제 해주어야 바람직할까??
기본적으로 인증을 처리하는 기본 필터는 UsernamePasswordAuthenticationFilter 이다.
그렇기에, 별도의 인증 로직을 가진 필터를 추가해주고 싶다면, 해당 필터 앞에 추가해주는 설정이 필요하다.
createDelegatingPasswordEncoder() 로 설정 시
{noop}abcdef~!@#$% ... 처럼 password의 앞에 Enocoding 방식이 붙은채로 암호화 방식을 지정하여 저장이 가능하다.
Servie 구현 (회원가입, 로그인)
request와 response를 받을 DTO를 생성
SignRequest
package com.example.jwttest.member.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class SignRequest {
private Long id;
private String account;
private String password;
private String nickname;
private String name;
private String email;
}
SignResponse
package com.example.jwttest.member.dto;
import com.example.jwttest.member.Authority;
import com.example.jwttest.member.Member;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SignResponse {
private Long id;
private String account;
private String nickname;
private String name;
private String email;
private List<Authority> roles = new ArrayList<>();
private String token;
public SignResponse(Member member) {
this.id = member.getId();
this.account = member.getAccount();
this.nickname = member.getNickname();
this.name = member.getName();
this.email = member.getEmail();
this.roles = member.getRoles();
}
}
요청시에는, 사용자의 권한이나 token이 들어갈 이유가 없으니 request 쪽 dto에 생략해주었다.
반면, 응답시에는 권한이나 token이 return 되어야하므로 response dto쪽에 추가해주었다.
SignService
package com.example.jwttest.member;
import com.example.jwttest.member.dto.SignRequest;
import com.example.jwttest.member.dto.SignResponse;
import com.example.jwttest.security.JwtProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
@Service
@Transactional
@RequiredArgsConstructor
public class SignService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final JwtProvider jwtProvider;
public SignResponse login(SignRequest request) throws Exception {
Member member = memberRepository.findByAccount(request.getAccount()).orElseThrow(() -> new BadCredentialsException("계정정보를 확인해주세요."));
if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) {
throw new BadCredentialsException("비밀번호를 확인해주세요.");
}
return SignResponse.builder()
.id(member.getId())
.account(member.getAccount())
.name(member.getName())
.nickname(member.getNickname())
.email(member.getEmail())
.roles(member.getRoles())
.token(jwtProvider.createToken(member.getAccount(), member.getRoles()))
.build();
}
public boolean register(SignRequest request) throws Exception {
try {
Member member = Member.builder()
.account(request.getAccount())
.password(passwordEncoder.encode(request.getPassword()))
.name(request.getName())
.nickname(request.getNickname())
.email(request.getEmail())
.build();
member.setRoles(Collections.singletonList(Authority.builder().name("ROLE_USER").build()));
memberRepository.save(member);
} catch (Exception e) {
System.out.println(e.getMessage());
throw new Exception("잘못된 요청입니다.");
}
return true;
}
public SignResponse getMember(String account) throws Exception {
Member member = memberRepository.findByAccount(account).orElseThrow(() -> new Exception("계정을 찾을 수 없습니다."));
return new SignResponse(member);
}
}
RestController 구현
SignController
package com.example.jwttest.member;
import com.example.jwttest.member.dto.SignRequest;
import com.example.jwttest.member.dto.SignResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor
public class SignController {
private final MemberRepository memberRepository;
private final SignService memberService;
@PostMapping(value = "/login")
public ResponseEntity<SignResponse> signin(@RequestBody SignRequest request) throws Exception {
return new ResponseEntity<>(memberService.login(request), HttpStatus.OK);
}
@PostMapping(value = "/register")
public ResponseEntity<Boolean> signup(@RequestBody SignRequest request) throws Exception {
return new ResponseEntity<>(memberService.register(request), HttpStatus.OK);
}
@GetMapping(value = "/user/get")
public ResponseEntity<SignResponse> getUser(@RequestParam String account) throws Exception {
return new ResponseEntity<>(memberService.getMember(account), HttpStatus.OK);
}
@GetMapping(value = "/admin/get")
public ResponseEntity<SignResponse> getUserForAdmin(@RequestParam String account) throws Exception {
return new ResponseEntity<>(memberService.getMember(account), HttpStatus.OK);
}
}
PostMan 검증
여기까지 작성하면, 기본적인 코드는 다 작성한 것이다. 이제, 해당 예제가 정말 계획한대로 동작이 되는지 Postman 을 사용하여 확인해볼 것이다.
동작 시나리오는 다음과 같다.
- 회원가입
- Account, Password, User Info .. 를 입력하여 회원가입 진행
- 로그인
- Account, Password를 입력하여 로그인 진행
- 로그인이 성공적으로 된다면, Access Token을 발급받아 Authentication Header에 넣어준다.
- 이후 요청 시 마다, 해당 Authentication Header에 있는 token으로 인증을 한다.
- 유저 조회
- 인증, 인가된 사용자인지 검증
우선적으로, 코드를 실행해보면 JPA를 통해, H2 DB에 Member, Authority table이 생성되는 것을 확인할 수 있다.
회원가입
회원가입이 성공적으로 되는지 확인해보기 위해서 Postman을 사용했다.
추가적인 데이터를 생성해준 후, db를 확인해보면 성공적으로 회원가입이 진행되는 것을 확인할 수 있다.
로그인
인증 확인
실패 - Token 정보 넘겨주지 않을 시
앞서 로그인 시, 생성된 token 값을 Authorization에 넣어주지 않으면 위 처럼 인증되지 않은 사용자라고 나오게 되고, account='user1' 에 대한 정보를 가져오지 못한다.
성공 - Token 정보 넘겨줄 시
로그인 시 생성된 토큰 값을 위의 Authorization의 Bearer에 값을 넣어준 후, 요청을 하게 되면 성공적으로 해당 계정에 접근이 가능한것을 확인할 수 있다.