Skip to content

Commit

Permalink
Merge pull request #26 from real-world-study/hoony-lab-issue23
Browse files Browse the repository at this point in the history
[hoony-lab-issue23] authentication: jwt token 기능
  • Loading branch information
hoony-lab committed Sep 5, 2021
2 parents b5adfca + 97af41c commit 66cb514
Show file tree
Hide file tree
Showing 11 changed files with 434 additions and 110 deletions.
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

implementation 'org.springframework.boot:spring-boot-starter-security'

implementation 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'

implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'

runtimeOnly 'com.h2database:h2'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Expand Down
29 changes: 22 additions & 7 deletions src/main/java/com/study/realworld/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
package com.study.realworld.config;

import com.study.realworld.config.auth.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
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;

@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin().disable()
.csrf().disable()
.headers().frameOptions().disable()

.and()
.authorizeRequests()
.antMatchers("/api/hello", "/h2-console/**").permitAll()
.antMatchers("/api/users/**", "/api/users/login/**").permitAll()
.antMatchers("/api/articles/**").permitAll()
.antMatchers("/api/tags/**").permitAll()
.antMatchers("/error").permitAll()
.anyRequest().authenticated()

.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)

.and()
.authorizeRequests()
.antMatchers("/api/hello", "/h2-console/**").permitAll()
.antMatchers("/api/users/**", "/api/users/login/**").permitAll()
.antMatchers("/api/articles/**").permitAll()
.antMatchers("/api/tags/**").permitAll()
.antMatchers("/error").permitAll()
.anyRequest().authenticated();
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.study.realworld.config.auth;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;

@Slf4j
@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private static final String DOFILTERMESSAGE = "{} {} : {}";

private final JwtProvider jwtProvider;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info(DOFILTERMESSAGE, request.getMethod(), request.getRequestURI(), request.getHeader(HttpHeaders.AUTHORIZATION));
String header = request.getHeader(HttpHeaders.AUTHORIZATION);

if(header != null) {
String tokenPrefix = header.split(" ")[0];
String accessToken = header.split(" ")[1];

if("Token".equals(tokenPrefix)) {
String email = jwtProvider.getSubjectFromToken(accessToken);
final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(email, accessToken, Collections.emptyList());

SecurityContextHolder.getContext().setAuthentication(authentication);
}
}

filterChain.doFilter(request, response);
}

}
69 changes: 69 additions & 0 deletions src/main/java/com/study/realworld/config/auth/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.study.realworld.config.auth;

import com.study.realworld.user.domain.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtProvider {

private String secret;
private int accessTime;

public JwtProvider(@Value("${jwt.secret}") String secret,
@Value("${jwt.access-time}") int accessTime) {
this.secret = secret;
this.accessTime = accessTime;
}

public String generateJwtToken(User user) {
return Jwts.builder()
.setSubject(user.getEmail())
.setHeader(createHeader())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + accessTime))
.signWith(createKey(), SignatureAlgorithm.HS512)
.compact();
}

public Claims getClaimsFromToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(DatatypeConverter.parseBase64Binary(secret))
.build()
.parseClaimsJws(token)
.getBody();
} catch (JwtException | NullPointerException e) {
throw new JwtException("Invalid token");
}
}

public String getSubjectFromToken(String token) {
return this.getClaimsFromToken(token)
.getSubject();
}

private Map<String, Object> createHeader() {
Map<String, Object> header = new HashMap<>();
header.put("alg", "HS512");
header.put("typ", "JWT");
return header;
}

private Key createKey() {
byte[] secretKeyBytes = DatatypeConverter.parseBase64Binary(secret);
return new SecretKeySpec(secretKeyBytes, SignatureAlgorithm.HS512.getJcaName());
}

}
27 changes: 13 additions & 14 deletions src/main/java/com/study/realworld/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,59 +8,58 @@
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.NoSuchElementException;

@RequiredArgsConstructor
@Transactional
@Transactional(readOnly = true)
@Service
public class UserService {

private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;

@Transactional(readOnly = true)
public List<User> findAll() {
return userRepository.findAll();
}

@Transactional(readOnly = true)
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException(id + " not found"));
}

@Transactional(readOnly = true)
public User findByEmail(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new NoSuchElementException(email + " not found"));
}

@Transactional(propagation = Propagation.NEVER)
public User save(UserJoinRequest request) {
User user = UserJoinRequest.toUser(request);
user.encodePassword(passwordEncoder);

return userRepository.save(user);
}

public User deleteById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException(id + " not found"));
validateDuplicateUser(user.getEmail());

userRepository.delete(user);
return user;
return userRepository.save(user);
}

@Transactional(readOnly = true)

public User login(UserLoginRequest request) {
User user = this.findByEmail(request.getEmail());

validateMatchesPassword(user, request.getPassword());

return user;
}

private void validateDuplicateUser(String email) {
if(userRepository.findByEmail(email).isPresent()) {
throw new DuplicateKeyException(email + " duplicated email");
}
}

private void validateMatchesPassword(User user, String rawPassword) {
if(!user.matchesPassword(rawPassword, passwordEncoder)) {
throw new NoSuchElementException(rawPassword + " wrong wrong wrong triple wrong" + user.getPassword());
Expand Down
35 changes: 27 additions & 8 deletions src/main/java/com/study/realworld/user/web/UserController.java
Original file line number Diff line number Diff line change
@@ -1,38 +1,57 @@
package com.study.realworld.user.web;

import com.study.realworld.config.auth.JwtProvider;
import com.study.realworld.user.domain.User;
import com.study.realworld.user.dto.UserJoinRequest;
import com.study.realworld.user.dto.UserLoginRequest;
import com.study.realworld.user.dto.UserResponse;
import com.study.realworld.user.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@RequiredArgsConstructor
@RequestMapping("/api")
@RestController
public class UserController {

private final JwtProvider jwtProvider;
private final UserService userService;

@PostMapping("/users")
public ResponseEntity<UserResponse> joinUser(@RequestBody UserJoinRequest request) {
User user = userService.save(request);
String token = null;
String token = jwtProvider.generateJwtToken(user);

return ResponseEntity.ok().body(UserResponse.of(user, token));
return ResponseEntity.ok()
.body(UserResponse.of(user, token));
}

@PostMapping("/users/login")
public ResponseEntity<UserResponse> loginUser(@RequestBody UserLoginRequest request) {
User user = userService.login(request);
String token = null;
String token = jwtProvider.generateJwtToken(user);

return ResponseEntity.ok()
.header(HttpHeaders.AUTHORIZATION, "Token " + token)
.body(UserResponse.of(user, token));
}

@GetMapping("/user")
public ResponseEntity<UserResponse> getUser(HttpServletRequest servletRequest) {
String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
User user = userService.findByEmail(email);
String token = servletRequest.getHeader(HttpHeaders.AUTHORIZATION).split(" ")[1];

return ResponseEntity.ok().body(UserResponse.of(user, token));
return ResponseEntity.ok()
.body(UserResponse.of(user, token));
}

}
4 changes: 4 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ spring:

server:
port: 8080

jwt:
secret: 29effa2dab94e8f33f431d7694e2d47d4eb3999e653fdb9c8c811c0fbab245d1d5c8fba1d405758836b4193a44ecb9308543238bbadfa575cd5e3ee798d2d42e
access-time: 1800000

0 comments on commit 66cb514

Please sign in to comment.