구현하기 전에…
최신 버전으로 Spring Security + jwt 구현?
스프링 시큐리티와 JWT로 로그인을 구현하기 위해 많은 블로그와 강의를 찾아봤지만, 스프링 시큐리티 버전 업그레이드로 인해 기존 인프라나 블로그 게시물의 강의에서 레거시 방식을 사용하여 구현한 예만 있었습니다.
최신 버전의 Security와 jwt 버전 0.11.2 예제를 사용하여 로그인 예제를 구현하기 위해 다른 블로그 게시물을 참조하여 기사를 작성했습니다.
배포 환경 버전은 무엇입니까?
초기에는 Gradle 기반의 InteliJ 및 Springboot가 사용됩니다.
SpringBoot 버전은 3.0.4이고 JDK 17을 사용하고 있으며 Spring Security는 6.0.0 이상을 사용하고 있습니다.
H2 DB를 사용하고 jwt는 0.11.2를 사용합니다.
별도의 화면을 만드는 것이 아니므로 Postman으로 테스트하겠습니다.
로그인 예제를 입력하기 전에 보안 및 JWT에 대한 아래 기사를 먼저 읽어야 합니다.
(개념) 스프링 시큐리티란?
(개념) 스프링 시큐리티란?
스프링 시큐리티란? Spring 기반 애플리케이션의 보안(인증, 권한, 승인)을 담당하는 Spring 서브프레임워크. Spring Security는 인증 및 권한 부여를 위해 필터의 흐름을 따릅니다.
soohykeee.tistory.com
(개념) JWT란?
(개념) JWT란?
JWT를 배우기 전에 세션 인증 방식과 토큰 인증 방식에 대해 잘 알고 있어야 합니다.
(개념) 세션 기반 인증 vs. 토큰 기반 인증 (개념) 세션 기반 인증 vs. 토큰 기반 인증 인증 유형
soohykeee.tistory.com
화신
태도
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 비밀키 작성이 어렵다면 아래 링크에서 비트를 선택하여 랜덤키 값을 생성합니다.
256비트 이상으로 생성해야 합니다.
암호화 키 생성기
암호화 키 생성기 모든 유형의 키를 생성하는 최고의 온라인 도구 상자!
모든 프로그래머는 즐겨찾기에 All Keys Generator가 필요합니다!
무료로 제공되며 광고와 기부로만 지원됩니다.
www.allkeysgenerator.com
패키지 구조
위와 같이 구조를 생성하여 적용하겠습니다.
사용자(회원) 구현
인증과 보안을 구현하기 전에 먼저 사용자를 구현합시다.
회원사
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));
}
}
권위자
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 주석을 첨부하면 데이터 교환 시 데이터가 무시(ignored)되어 응답 값에 표시되지 않습니다.
회원 저장소
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가 ID로 설계되었기 때문에 findByAccount를 사용했습니다.
하지만 이메일은 이전에 고유하게 만들어졌기 때문에 이메일을 ID로 사용할 수 있습니다.
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가 상속되어 구성원 엔터티 또는 실제로 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를 사용하여 데이터베이스에 액세스하고 사용자 정보를 가져와 CustomUserDetails에 전달합니다.
-> 이때 DB에 접근하여 정보를 조회한다.
계정이 유효하지 않으면 예외가 발생합니다.
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에서 생성한 비밀 키에 대한 액세스를 설정합니다.
이렇게 하면 해당 값이 salt 필드에 저장됩니다.
import org.springframework.beans.factory.annotation.Value;
또한 위에서 작성한 것을 가져오지 않고 다른 값을 사용하는 주석을 가져오면 오류가 발생할 수 있음을 유의하십시오.
우선 이 예제에서는 refreshToken이 구현되지 않았지만 토큰에 만료 시간을 부여해야 하므로 exp 필드가 생성되고 만료 시간 1시간이 적용됩니다.
@PostConstruct는 의존성 주입이 완료된 후 실행되어야 하는 메소드에 사용되는 어노테이션으로, 다른 리소스에서 호출하지 않아도 실행된다.
- 호출 순서: 생성자 호출 -> 종속성 주입 완료 -> @PostConstruct
이 주석을 사용하는 이유는 생성자가 호출될 때 Bean이 초기화되기 전이기 때문입니다.
그러나 이를 사용하면 Bean이 초기화된 후 종속성을 확인할 수 있습니다.
또한 bean 라이프사이클에서 여러 초기화 문제를 피하기 위해 한 번만 실행됩니다.
로그인한 계정과 역할을 createToken 메소드를 사용하여 클레임에 입력한 후 만료 시간, 발급 시간, 해시 알고리즘, 비밀 키 값을 입력하여 토큰을 생성하고 반환합니다.
요청이 오면 토큰에 포함된 사용자 계정을 가져오는 getAccount 메소드와 JpaUserDetailsService 클래스에 정의된 loadUserByUsername 메소드를 사용하여 인증 과정에서 권한을 확인하기 위해 DB에 계정이 존재하는지 확인하고, 인증 정보를 전달합니다.
또한 요청 헤더에서 인증 정보를 가져오고 이를 사용자에게 전달하는 resolveToken 메소드를 생성합니다.
또한 실제 토큰이 유효한 토큰인지 확인하기 위해 validateToken 메서드가 생성됩니다.
토큰이 들어오면 먼저 String.substring 메서드를 사용하여 토큰이 베어러인지 확인합니다.
그런 다음 Bearer 뒤에 있는 토큰 정보를 분할한 후 해당 토큰에 저장된 클레임에서 만료 시간을 꺼낸 후 get을 사용하여 토큰이 만료되었는지 확인합니다.
보안 구성
Jwt인증 필터
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는 유효성을 검사하는 필터입니다.
필터를 적용하면 서블릿에 도달하기 전에 검사를 완료할 수 있습니다.
클래스에서 상속된 OncePerRequestFilter는 요청당 한 번만 실행되도록 보장됩니다.
보안 구성
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(새 JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class)
-> 해당 부분을 작성했는데, 그 전에 JWT를 검증하는 필터인 JwtAuthenticationFilter를 생성했습니다.
그렇다면 필터는 언제 적용해야 합니까?
기본적으로 인증을 처리하는 기본 필터는 UsernamePasswordAuthenticationFilter 입니다.
따라서 별도의 인증 로직이 있는 필터를 추가하려면 필터 앞에 추가해야 합니다.
createDelegatingPasswordEncoder()로 설정된 경우
{noop}abcdef~!
@#$% …와 마찬가지로 비밀번호 앞에 Enocoding 방식을 추가하여 암호화 방식을 지정하여 저장할 수 있습니다.
서비스 구현(회원가입, 로그인)
요청 및 응답을 수신할 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;
}
서명 응답
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();
}
}
요청 시 사용자의 권한이나 토큰을 입력할 필요가 없으므로 요청 페이지에서 dto가 생략됩니다.
반면 응답은 권한이나 토큰을 반환해야 하므로 응답 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);
}
}
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을 사용하여 예제가 설계된 대로 작동하는지 확인합니다.
운영 시나리오는 다음과 같습니다.
- 회원 가입
- 계정, 비밀번호, 사용자 정보를 입력하여 회원가입을 진행합니다.
- 계정, 비밀번호, 사용자 정보를 입력하여 회원가입을 진행합니다.
- 등록
- 계정과 비밀번호를 입력하여 로그인 진행
- 로그인에 성공하면 액세스 토큰이 발급되어 인증 헤더에 삽입됩니다.
- 각 후속 요청은 적절한 인증 헤더의 토큰을 사용하여 인증됩니다.
- 사용자 검색
- 인증, 인가된 사용자 확인
먼저 코드를 실행하면 JPA를 통해 H2 DB에 구성원 및 권한 테이블이 생성된 것을 확인할 수 있습니다.
우편배달원을 이용하다
회원 가입
Postman을 이용하여 회원가입이 잘 되었는지 확인했습니다.
추가 데이터 생성 후 데이터베이스를 확인하면 회원가입이 정상적으로 된 것을 확인할 수 있습니다.
등록
인증 확인
오류 – 토큰 정보가 전달되지 않은 경우
위와 같이 미리 로그인하고 생성된 토큰 값을 Authorization에 입력하지 않으면 인증되지 않은 사용자로 나타나며 account=”user1″에 대한 정보를 조회할 수 없습니다.
성공 – 토큰 정보 전달 시
위의 승인 전표에 로그인 시 생성된 토큰의 값을 입력한 후 요청을 제출하면 성공적으로 계정에 접근할 수 있음을 확인할 수 있습니다.