LostCatBox

SpringProject-Board-CH05

Word count: 3.2kReading time: 20 min
2022/12/24 Share

Spring 게시판 프로젝트 5편 (RestfulApi로의 전환+JWT)

Created Time: July 20, 2022 1:48 PM
Last Edited Time: August 4, 2022 4:32 PM
Tags: Java, Spring, Computer

왜?

  • 프론트 엔드 + 백엔드는 항상 나눠서 개발되는것이 일반적인데, 현재 나는 타임리프를 사용하며 jsp를 이용하여 뷰를 통해 처리하는중이다
  • 명확하게 프론트 엔드와 백엔드를 나눠 json으로 통신하는 구조를 만들고싶었다.
  • 그러기 위해서는 먼저 http method의 구분, 리소스 mapping된 주소 체계를 정형화, 결과 데이터의 구조를 표준화하여 정의하는것이 필요하다

출처

Swagger

Build.gradle

1
2
implementation 'io.springfox:springfox-swagger2:2.6.1'
implementation 'io.springfox:springfox-swagger-ui:2.6.1'

SwaggerConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {

@Bean
public Docket SwaggerApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(swaggerInfo()) // API Docu 및 작성자 정보 매핑
.select()
.apis(RequestHandlerSelectors.basePackage("com.restApi.restApiSpringBootApp.controller"))
.paths(PathSelectors.any()) // controller package 전부
//.paths(PathSelectors.ant("/v1/**")) // controller 패키지 내 v1만 택해서 할수도 있다.
.build()
.useDefaultResponseMessages(false); // 기본 세팅값인 200, 401, 402, 403, 404를 사용하지 않는다.
}

private ApiInfo swaggerInfo() {
return new ApiInfoBuilder().title("Spring API Documentation")
.description("앱 서버 API 설명을 위한 문서입니다.")
.license("woonsik")
.licenseUrl("ws-pace.tistory.com")
.version("1")
.build();
}

적용예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Api(tags = {"1. User"})
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1")
public class UserController {

private final UserJpaRepo userJpaRepo;

@ApiOperation(value = "모든 회원 조회", notes = "모든 회원 목록을 조회합니다.")
@GetMapping("/users")
public List<User> findAllUser() {
return userJpaRepo.findAll();
}

@ApiOperation(value = "회원 등록", notes = "회원을 등록합니다.")
@PostMapping("/user")
public User save(@ApiParam(value = "회원 이메일", required = true) @RequestParam String email,
@ApiParam(value = "회원 이름", required = true) @RequestParam String name) {
User user = User.builder()
.email(email)
.name(name)
.build();

return userJpaRepo.save(user);
}

@ApiOperation(value = "회원 검색 (이름)", notes = "이름으로 회원을 검색합니다.")
@GetMapping("/findUserByName/{name}")
public List<User> findUserByName(@ApiParam(value = "회원 이름", required = true) @PathVariable String name) {
return userJpaRepo.findByName(name);
}

@ApiOperation(value = "회원 검색 (이메일)", notes = "이메일로 회원을 검색합니다.")
@GetMapping("/findUserByEmail/{email}")
public User findUserByEmail(@ApiParam(value = "회원 이메일", required = true) @PathVariable String email) {
return userJpaRepo.findByEmail(email);
}
}
  • @Api(tags = {“1. User”})

    • 제목 역할을 한다
  • @ApiOperation(value = “모든 회원 조회”, notes = “모든 회원 목록을 조회합니다.”)

    • value는 리스트로 봤을 때 간단하게 보이는 용도
    • notes는 펼쳐봤을 때 자세히 알려주는 용도
  • @ApiParam(value = “회원 이메일”, required = true) @RequestParam String email

    • ApiParam은 Swagger에서 이다. value는 설명, required는 필수값 설정 여부설명을 붙이기 위한 용도
  • @RequestParam은 HTTP GET 메소드에서 매칭되는 request parameter값이 자동으로 들어간다.

  • @PathVariable도 HTTP 요청에서 매칭되는 request parameter값이 자동으로 들어간다.

RestfulApi 구분 및 정형화

리소스의 사용목적에 따라 Http method를 구분해서 사용한다.

Http 프로토콜은 여러 가지 사용목적에 따라 HttpMethod를 제공하고 있는데요. 여기서는 그중 아래의 4가지 HttpMethod를 상황에 맞게 api 구현에 사용하도록 하겠습니다.

  • GET – 서버에 주어진 리소스의 정보를 요청한다.(읽기)
  • POST – 서버에 리소스를 제출한다(쓰기)
  • PUT – 서버에 리소스를 제출한다. POST와 달리 리소스 갱신 시 사용한다.(수정)
  • DELETE – 서버에 주어진 리소스를 삭제 요청한다.(삭제 시)

리소스에 Mapping된 주소 체계를 정형화 한다.

주소 체계는 아래처럼 정형화된 구조로 구성하고 HttpMethod를 통해 리소스의 사용목적을 판단하는 것이 핵심입니다.

  • GET /v1/users – 회원 리스트를 조회한다.
  • GET /v1/user/{userId} – 회원 userId에 해당하는 정보를 조회한다.
  • POST /v1/user – 신규 회원정보를 입력한다.
  • PUT /v1/user – 기존 회원의 정보를 수정한다. (id를 다른곳에 실어서보냄)
  • DELETE /v1/user/{userId} – userId로 기존 회원의 정보를 삭제한다.

정형화된 주소체계로 Controller수정

리소스의 사용 목적에 따라 GetMapping, PostMapping, PutMapping, DeleteMapping을 사용하였습니다. 결과 데이터의 형태에 따라 단일건 처리는 getBasicResult()를 다중 건 처리는 getListResult()를, api 처리 성공 결과만 필요한 경우 getSuccessResult()를 사용합니다.

  • 아래 예시는 DTO가 아닌 User Entity를 그대로 사용하므로 바꿔야아함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1")
public class UserController {

private final UserRepository userRepository;
private final ResponseService responseService;

@ApiOperation(value = "회원 단건 검색", notes = "userId로 회원을 조회합니다.")
@GetMapping("/user/{userId}")
public SingleResult<User> findUserByKey(@ApiParam(value = "회원 ID", required = true) @PathVariable Long userId) {
return responseService
.getSingleResult(userRepository.findById(userId).orElse(null));
}

@ApiOperation(value = "회원 목록 조회", notes = "모든 회원을 조회합니다.")
@GetMapping("/users")
public ListResult<User> findAllUser() {
return responseService
.getListResult(userRepository.findAll());
}

@ApiOperation(value = "회원 등록", notes = "회원을 등록합니다.")
@PostMapping("/user")
public SingleResult<User> save(@ApiParam(value = "회원 이메일", required = true) @RequestParam String email,
@ApiParam(value = "회원 이름", required = true) @RequestParam String username) {
User user = User.builder()
.email(email)
.username(username)
.build();
return responseService.getSingleResult(userRepository.save(user));
}

@ApiOperation(value = "회원 수정", notes = "회원 정보를 수정합니다.")
@PutMapping("/user")
public SingleResult<User> modify(@ApiParam(value = "회원 아이디", required = true) @RequestParam Long userId,
@ApiParam(value = "회원 이메일", required = true) @RequestParam String email,
@ApiParam(value = "회원 이름", required = true) @RequestParam String username) {
User user = User.builder()
.id(userId)
.email(email)
.username(username)
.build();
return responseService.getSingleResult(userRepository.save(user));
}

@ApiOperation(value = "회원 삭제", notes = "회원을 삭제합니다.")
@DeleteMapping("/user/{userId}")
public CommonResult delete(@ApiParam(value = "회원 아이디", required = true) @PathVariable Long userId) {
userRepository.deleteById(userId);
return responseService.getSuccessResult();
}
}

JWT 토큰발급!

SpringSecurity

  • spring dispatcherServlet앞단에서 filter를 등록시켜 요청을 가로챈다. 클라이언트에게 리소스 접근 권한이 없을 경우 인증 화면으로 자동으로 리다이렉트한다

SpringSecurity Filter

  • SpringSecurity는 기능별 필터의 집합으로 되어있고 필터의 처리 순서는 아래와 같습니다. 종류가 매우 많지만 여기서 중요한 것은 필터의 처리 순서입니다. 클라이언트가 리소스를 요청할 때 접근 권한이 없는 경우 기본적으로 로그인 폼으로 보내게 되는데 그 역할을 하는 필터는UsernamePasswordAuthenticationFilter입니다. Rest Api에서는 로그인 폼이 따로 없으므로 인증 권한이 없다는 오류 Json을 내려줘야 하므로 UsernamePasswordAuthenticationFilter 전에 관련 처리를 넣어야 함을 알 수 있습니다.

https://daddyprogrammer.org/wp-content/uploads/2019/04/SpringSecurity-security_4-1.jpeg

  1. ChannelProcessingFilter
  2. SecurityContextPersistenceFilter
  3. ConcurrentSessionFilter
  4. HeaderWriterFilter
  5. CsrfFilter
  6. LogoutFilter
  7. X509AuthenticationFilter
  8. AbstractPreAuthenticatedProcessingFilter
  9. CasAuthenticationFilter
  10. UsernamePasswordAuthenticationFilter
  11. BasicAuthenticationFilter
  12. SecurityContextHolderAwareRequestFilter
  13. JaasApiIntegrationFilter
  14. RememberMeAuthenticationFilter
  15. AnonymousAuthenticationFilter
  16. SessionManagementFilter
  17. ExceptionTranslationFilter
  18. FilterSecurityInterceptor
  19. SwitchUserFilter

API 인증(Who?) 및 권한(Can?) 부여, 제한된 리소스의 요청

  • 인증을 위해 가입(Signup)및 로그인(Signin) api를 구현합니다.
  • 가입 시 제한된 리소스에 접근할 수 있는 ROLE_USER 권한을 회원에게 부여합니다.
  • SpringSecurity 설정에는 접근 제한이 필요한 리소스에 대해서 ROLE_USER 권한을 가져야 접근 가능하도록 세팅합니다.
  • 권한을 가진 회원이 로그인 성공 시엔 리소스에 접근할 수 있는 Jwt 보안 토큰을 발급합니다.
  • Jwt 보안 토큰으로 회원은 권한이 필요한 api 리소스를 요청하여 사용합니다.

JWT 란?

JSON Web Token (JWT)은 JSON 객체로서 당사자간에 안전하게 정보를 전송할 수 있는 작고 독립적인 방법을 정의하는 공개 표준 (RFC 7519)입니다. 자세한 내용은 아래 링크에서 확인할 수 있습니다.

https://jwt.io/introduction/

Jwt는 JSON 객체를 암호화하여 만든 String값으로 기본적으로 암호화되어있어 변조하기가 어려운 정보입니다. 또한 다른 토큰과 달리 토큰 자체에 데이터를 가지고 있습니다. api 서버에서는 로그인이 완료된 클라이언트에게 회원을 구분할 수 있는 값을 넣은 Jwt 토큰을 생성하여 발급하고, 클라이언트는 이 Jwt 토큰을 이용하여 권한이 필요한 리소스를 서버에 요청하는 데 사용할 수 있습니다. api서버는 클라이언트에게서 전달받은 Jwt 토큰이 유효한지 확인하고 담겨있는 회원 정보를 확인하여 제한된 리소스를 제공하는데 이용할 수 있습니다.

  • jwt는 헤더(base64인코딩), payload(클래임으로구성됨)(base64인코딩), signiture(서명)(비밀키+헤더+페이로드활용하여 암호화 알고리즘으로 해싱후 base64인코딩)
  • 서버에서 클라이언트로부터 JWT를 받았을때, 비밀키+헤더+페이로드 암호화 알고리즘으로 해싱후 base64인코딩하여생성한 signature가 같다면 검증완료함

구현(맛보기)

build.gradle

1
2
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt:0.9.1'

토큰 다루기 위한 핸들러

  • JwtHandler
  • 지금은 별도의 방식으로 토큰 만들계획없으므로 인터페이스를 정의하지않고 하나의 클래스만 선언
  • JwtHandler는 사용할 때, 기본적으로 Base64로 인코딩된 키를 파라미터로 받게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package hello.postpractice.handler;

import io.jsonwebtoken.*;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtHandler {

private String type = "Bearer ";

public String createToken(String encodedKey, String subject, long maxAgeSeconds) {
Date now = new Date();
return type + Jwts.builder() //1
.setSubject(subject) //2
.setIssuedAt(now) //3
.setExpiration(new Date(now.getTime() + maxAgeSeconds * 1000L))//4
.signWith(SignatureAlgorithm.HS256, encodedKey)//5
.compact();//6
}

public String extractSubject(String encodedKey, String token) {
return parse(encodedKey, token).getBody().getSubject();
}

public boolean validate(String encodedKey, String token) {
try {
parse(encodedKey, token);
return true;
} catch (JwtException e) {
return false;
}
}

private Jws<Claims> parse(String key, String token) {
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(untype(token));
}

private String untype(String token) {
return token.substring(type.length());
}
}

JwtHandler.createToken은 Base64로 인코딩된 key 값을 받고, 토큰에 저장될 데이터 subject, 만료 기간 maxAgeSeconds를 초단위로 받아서 토큰을 만들어주는 작업을 수행합니다.

  1. jwt를 빌드하기 시작합니다.
  2. 토큰에 저장될 데이터를 지정해줍니다. (우리의 서비스에서는 Member의 id 값을 넣어주겠습니다.)
  3. 토큰 발급일을 지정해줍니다. 현재 시간에서 입력된 시간을 더해주었습니다. createToken의 파라미터는 초 단위로 입력받지만, Date는 ms 단위로 입력받기 때문에 1000을 곱해줍니다.
  4. 토큰 만료 일자를 지정해줍니다.
  5. 파라미터로 받은 key로 SHA-256 알고리즘을 사용하여 서명해줍니다.
  6. 주어진 정보로 토큰을 생성해냅니다.

type으로 지정한 “Bearer”는, 생성해낸 토큰이 어떤 타입인지(여기서는 jwt)를 나타냅니다.

JwtHandler.extractSubject

토큰에서 subject를 추출해냅니다. 토큰을 파싱하고, 바디에서 subject를 꺼내올 수 있습니다.

우리의 서비스에서는, 토큰의 subject로 Member의 id가 저장되기 때문에, 이를 이용하여 사용자를 인증할 수 있을 것이라 예상됩니다.

JwtHandler.validate

1
2
3
4
5
6
7
8
public boolean validate(String encodedKey, String token) {
try {
parse(encodedKey, token);
return true;
} catch (JwtException e) {
return false;
}
}

토큰의 유효성을 검증합니다. 토큰을 파싱하면서 jwt 관련 예외가 발생했다면, 유효하지않은 토큰으로 판단합니다.

토큰 파싱 과정

1
2
3
4
5
6
7
8
9
private Jws<Claims> parse(String key, String token) {
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(untype(token));
}

private String untype(String token) {
return token.substring(type.length());
}

parser를 이용하여 사용된 key를 지정해주고, 파싱을 수행해줍니다.

이 때, 토큰 문자열에는 토큰의 타입도 포함되어있으므로, 이를 untype 메소드를 이용하여 제거해줍니다.


jwt 적용 구현

JwtProvider 생성

Json Web Token 생성 및 유효성 검증을 하는 컴포넌트이다. Jwts
는 여러가지 암호화 알고리즘을 제공하고 알고리즘과 비밀키를 가지고 토큰을 생성하게 된다. (SignatureAlgorithm.XXXX + secretKey)

이때 Claim 정보에는 토큰에 부가적으로 실어 보낼 정보를 담을 수 있다. claim 정보에 회원을 구분할 수 있는 값을 세팅하고 토큰이 들어오면 해당 값으로 회원을 구분해서 리소스를 제공하면 된다.

Jwt에는 expire시간을 정해서 해당 토큰의 만료시간을 정해줄 수 있다. 토큰 발급 후 일정한 시간이 지나면 해당 토큰을 만료 시킨다.

resolveToken 메서드는 Http Request header에서 세팅된 토큰값을 가져와서 유효성 검사를 한다. 제한된 리소스에 접근할 때 Http Header에 토큰을 세팅하여 호출하면 유효성 검사를 통해 사용자 인증을 받을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@RequiredArgsConstructor
@Component
public class JwtProvider {

@Value("spring.jwt.secret")
private String secretKey;

private Long tokenValidMillisecond = 60 * 60 * 1000L;

private final CustomUserDetailsService userDetailsService;

@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}

// Jwt 생성
public String createToken(String userPk, List<String> roles) {

// user 구분을 위해 Claims에 User Pk값 넣어줌
Claims claims = Jwts.claims().setSubject(userPk);
claims.put("roles", roles);
// 생성날짜, 만료날짜를 위한 Date
Date now = new Date();

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + tokenValidMillisecond))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}

// Jwt 로 인증정보를 조회
public Authentication getAuthentication (String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}

// jwt 에서 회원 구분 Pk 추출
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}

// HTTP Request 의 Header 에서 Token Parsing -> "X-AUTH-TOKEN: jwt"
public String resolveToken(HttpServletRequest request) {
return request.getHeader("X-AUTH-TOKEN");
}

// jwt 의 유효성 및 만료일자 확인
public boolean validationToken(String token) {
try {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return !claimsJws.getBody().getExpiration().before(new Date()); // 만료날짜가 현재보다 이전이면 false
} catch (Exception e) {
return false;
}
}
}

application-jwt.yml

1
2
3
spring:
jwt:
secret: {AccessToken}

application.yml

1
2
3
spring:
profiles:
include: jwt

JwtAuthenticationFilter 생성

  • Jwt이 유효한 토큰인지 인증하기 위한 Filter
    이다. 이 필터를 Security 설정 시 UsernamePasswordAuthentication 앞에 세팅
    해서 로그인폼으로 반환하기 전에 인증 여부를 Json으로 반환시킨다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

private final JwtProvider jwtProvider;

// request로 들어오는 Jwt의 유효성을 검증 - JwtProvider.validationToken() 을 필터로서 FilterChain에 추가
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
String token = jwtProvider.resolveToken((HttpServletRequest) request);
if (token != null && jwtProvider.validationToken(token)) {
Authentication authentication = jwtProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}

SpringSecurity Configuration 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@RequiredArgsConstructor
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

private final JwtProvider jwtProvider;

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/*/login", "/*/signup").permitAll()
.anyRequest().hasRole("USER")
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);
}

@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v2/api-docs", "/swagger-resources/**",
"/swagger-ui.html", "/webjars/**", "/swagger/**");
}
}
  • httpBasic() : 기본설정은 비 인증시 로그인 폼 화면으로 리다이렉트 되는데 RestApi이므로 disalbe함
  • CSRF() : rest api이므로 상태를 저장하지 않으니 csrf 보안을 설정하지 않아도된다.
  • SessionCreationPolicy(SessionCreationPolicy.STATELESS)
    • Jwt으로 인증하므로 세션이 필요지 않으므로 생성 안한다.
  • antMatchers()
    • 권한 관리 대상을 지정하는 옵션
    • 로그인 및 가입에 대한 접근은 누구나 가능하도록 함 : antMathcers(“//login”, “//signup”).permitAll()
    • 그 외 나머지 요청은 인증된 회원만 가능 : anyRequest().hasRole(“USER”)
  • addFilterBefore()
    • jwt 인증 필터를 UsernamePasswordAuthenticationFilter.class 전에 넣는다.
  • web.ignoring().antMatchers()
    • swagger 관련 url에 대해서는 예외처리

UserDetailsService를 implements받아서 재정의 - 토큰에 포함된 유저 정보로 유저 찾기

1
2
3
4
5
6
7
8
9
10
11
@RequiredArgsConstructor
@Service
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String userPk) throws UsernameNotFoundException {
return userRepository.findById(Long.parseLong(userPk)).orElseThrow(RuntimeException::new);
}
}

User Entity가 UserDetails를 implements하도록 수정

스프링 시큐리티 보안을 User Entity에 적용하기 위해 UserDetails를 상속받아서 메소드를 오버라이드하자.

roles는 회원이 가지고 있는 권한 정보이고 가입 시 기본으로 “ROLE_USER”가 세팅된다. 권한은 회원당 여러개가 정의될 수 있으므로 컬렉션 타입으로 정의한다. (@ElementCollection)

Json 결과로 출력하지 않을 값들은 @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 애노테이션을 선언해준다. read하지 못하게 하는 것이다. (특히 password는 꼭 읽지 못하게 하자)

스프링 시큐리티에서 제공하는 회원 정보 관련 세팅들이 있는데 사용여부에 따라 적절히 구현해준다. 여기서는 사용하지 않으므로 기본 true로 세팅.

SignController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Api(tags = "1. SignUp / LogIn")
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1")
public class SignController {

private final UserService userService;
private final JwtProvider jwtProvider;
private final ResponseService responseService;
private final PasswordEncoder passwordEncoder;

@ApiOperation(value = "로그인", notes = "이메일로 로그인을 합니다.")
@GetMapping("/login")
public SingleResult<String> login(
@ApiParam(value = "로그인 아이디 : 이메일", required = true) @RequestParam String email,
@ApiParam(value = "로그인 비밀번호", required = true) @RequestParam String password) {
UserLoginResponseDto userLoginDto = userService.login(email, password);

String token = jwtProvider.createToken(String.valueOf(userLoginDto.getId()), userLoginDto.getRoles());
return responseService.getSingleResult(token);
}

@ApiOperation(value = "회원가입", notes = "회원가입을 합니다.")
@PostMapping("/signup")
public SingleResult<Long> signup(
@ApiParam(value = "회원 가입 아이디 : 이메일", required = true) @RequestParam String email,
@ApiParam(value = "회원 가입 비밀번호", required = true) @RequestParam String password,
@ApiParam(value = "회원 가입 이름", required = true) @RequestParam String username,
@ApiParam(value = "회원 가입 닉네임", required = true) @RequestParam String nickname) {

UserSignupRequestDto userSignupRequestDto = UserSignupRequestDto.builder()
.email(email)
.password(passwordEncoder.encode(password))
.username(username)
.nickname(nickname)
.build();
Long signupId = userService.signup(userSignupRequestDto);
return responseService.getSingleResult(signupId);
}
}

UserLoginResponseDto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package hello.postpractice.domain;

import lombok.Getter;

import java.time.LocalDateTime;
import java.util.List;

@Getter
public class UserLoginResponseDto {
private Long id;
private List<String> roles;
private LocalDateTime createdDate;

public UserLoginResponseDto(User user) {
this.id = user.getId();
this.roles = user.getRoles();
this.createdDate = user.getCreatedDate();
}
}

UserSignupResponseDto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Getter
public class UserSignupRequestDto {
private String email;
private String password;
private String username;
private String nickname;

@Builder
public UserSignupRequestDto(String email, String password, String username, String nickname) {
this.email = email;
this.password = password;
this.username = username;
this.nickname = nickname;
}

public User toEntity() {
return User.builder()
.email(email)
.password(password)
.nickname(nickname)
.username(username)
.roles(Collections.singletonList("ROLE_USER"))
.build();
}

UserService - 로그인, 회원가입 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@RequiredArgsConstructor
@Service
public class UserService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Transactional
public Long save(UserRequestDto userDto) {
userRepository.save(userDto.toEntity());
return userRepository.findByEmail(userDto.getEmail()).get().getId();
}

@Transactional(readOnly = true)
public UserResponseDto findById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(RuntimeException::new);
return new UserResponseDto(user);
}

@Transactional(readOnly = true)
public UserResponseDto findByEmail(String email) {
User user = userRepository.findByEmail(email).get();
if (user == null) throw new RuntimeException();
else return new UserResponseDto(user);
}

@Transactional(readOnly = true)
public List<UserResponseDto> findAllUser() {
return userRepository.findAll()
.stream()
.map(UserResponseDto::new)
.collect(Collectors.toList());
}

@Transactional
public Long update(Long id, UserRequestDto userRequestDto) {
User modifiedUser = userRepository
.findById(id).orElseThrow(UserNotFoundCException::new);
modifiedUser.setNickname(userRequestDto.getNickname());
return id;
}

@Transactional
public void delete(Long id) {
userRepository.deleteById(id);
}

@Transactional(readOnly = true)
public UserLoginResponseDto login(String email, String password) {
User user = userRepository.findByEmail(email).orElseThrow(RuntimeException::new);
if (!passwordEncoder.matches(password, user.getPassword()))
throw new RuntimeException();
return new UserLoginResponseDto(user);
}

@Transactional
public Long signup(UserSignupRequestDto userSignupDto) {
if (userRepository.findByEmail(userSignupDto.toEntity().getEmail()).orElse(null) == null)
return userRepository.save(userSignupDto.toEntity()).getId();
else throw new RuntimeException();
}
}

UserController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
@Api(tags = {"2. User"})
@RequiredArgsConstructor
@RestController
@RequestMapping("/v1")
public class UserController {

private final UserService userService;
private final ResponseService responseService;

@ApiImplicitParams({
@ApiImplicitParam(
name = "X-AUTH-TOKEN",
value = "로그인 성공 후 AccessToken",
required = true, dataType = "String", paramType = "header")
})
@ApiOperation(value = "회원 단건 검색", notes = "userId로 회원을 조회합니다.")
@GetMapping("/user/id/{userId}")
public SingleResult<UserResponseDto> findUserById
(@ApiParam(value = "회원 ID", required = true) @PathVariable Long userId,
@ApiParam(value = "언어", defaultValue = "ko") @RequestParam String lang) {
return responseService.getSingleResult(userService.findById(userId));
}

@ApiImplicitParams({
@ApiImplicitParam(
name = "X-AUTH-TOKEN",
value = "로그인 성공 후 AccessToken",
required = true, dataType = "String", paramType = "header")
})
@ApiOperation(value = "회원 단건 검색 (이메일)", notes = "이메일로 회원을 조회합니다.")
@GetMapping("/user/email/{email}")
public SingleResult<UserResponseDto> findUserByEmail
(@ApiParam(value = "회원 이메일", required = true) @PathVariable String email,
@ApiParam(value = "언어", defaultValue = "ko") @RequestParam String lang) {
return responseService.getSingleResult(userService.findByEmail(email));
}

@ApiImplicitParams({
@ApiImplicitParam(
name = "X-AUTH-TOKEN",
value = "로그인 성공 후 AccessToken",
required = true, dataType = "String", paramType = "header")
})
@ApiOperation(value = "회원 목록 조회", notes = "모든 회원을 조회합니다.")
@GetMapping("/users")
public ListResult<UserResponseDto> findAllUser() {
return responseService.getListResult(userService.findAllUser());
}

@ApiImplicitParams({
@ApiImplicitParam(
name = "X-AUTH-TOKEN",
value = "로그인 성공 후 AccessToken",
required = true, dataType = "String", paramType = "header")
})
@ApiOperation(value = "회원 수정", notes = "회원 정보를 수정합니다.")
@PutMapping("/user")
public SingleResult<Long> update (
@ApiParam(value = "회원 ID", required = true) @RequestParam Long userId,
@ApiParam(value = "회원 이름", required = true) @RequestParam String nickname) {
UserRequestDto userRequestDto = UserRequestDto.builder()
.nickname(nickname)
.build();
return responseService.getSingleResult(userService.update(userId, userRequestDto));
}

@ApiImplicitParams({
@ApiImplicitParam(
name = "X-AUTH-TOKEN",
value = "로그인 성공 후 AccessToken",
required = true, dataType = "String", paramType = "header")
})
@ApiOperation(value = "회원 삭제", notes = "회원을 삭제합니다.")
@DeleteMapping("/user/{userId}")
public CommonResult delete(
@ApiParam(value = "회원 아이디", required = true) @PathVariable Long userId) {
userService.delete(userId);
return responseService.getSuccessResult();
}

경험

  • api로 구성했으나 뷰네임으로 찾고있는 에러를 발견.
    @responseBody를 사용해야 자바 객체로 json 값으로 변경할수있다..
  • @requestParam 은 form get,post로 url기반 요청 파라미터를 받을수있고, 변수로 받기가능
    @requestBody는 body데이터를 가져올수있음. 따라서 form post와 다른 여러 body요청들을 특정 객체로 받기 가능
CATALOG
  1. 1. Spring 게시판 프로젝트 5편 (RestfulApi로의 전환+JWT)
  2. 2. 왜?
  3. 3. 출처
  4. 4. Swagger
    1. 4.1. Build.gradle
    2. 4.2. SwaggerConfiguration
    3. 4.3. 적용예시
  5. 5. RestfulApi 구분 및 정형화
    1. 5.1. 리소스의 사용목적에 따라 Http method를 구분해서 사용한다.
    2. 5.2. 리소스에 Mapping된 주소 체계를 정형화 한다.
    3. 5.3. 정형화된 주소체계로 Controller수정
    4. 5.4. JWT 토큰발급!
      1. 5.4.1. SpringSecurity
      2. 5.4.2. SpringSecurity Filter
      3. 5.4.3. API 인증(Who?) 및 권한(Can?) 부여, 제한된 리소스의 요청
    5. 5.5. JWT 란?
  6. 6. 구현(맛보기)
    1. 6.1. build.gradle
      1. 6.1.1. 토큰 다루기 위한 핸들러
  7. 7. jwt 적용 구현
    1. 7.1. JwtProvider 생성
      1. 7.1.1. application-jwt.yml
      2. 7.1.2. application.yml
    2. 7.2. JwtAuthenticationFilter 생성
    3. 7.3. SpringSecurity Configuration 작성
    4. 7.4. UserDetailsService를 implements받아서 재정의 - 토큰에 포함된 유저 정보로 유저 찾기
    5. 7.5. User Entity가 UserDetails를 implements하도록 수정
    6. 7.6. SignController
    7. 7.7. UserLoginResponseDto
    8. 7.8. UserSignupResponseDto
    9. 7.9. UserService - 로그인, 회원가입 구현
    10. 7.10. UserController
  8. 8. 경험