feat: jwt token create service, filter implement

This commit is contained in:
minseokkang
2022-09-13 16:16:07 +09:00
parent d4763d0098
commit cbc39fcb8a
16 changed files with 240 additions and 58 deletions

View File

@@ -14,15 +14,15 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.projectlombok:lombok:1.18.24'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
// render
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.1.0'
annotationProcessor 'org.projectlombok:lombok'
// https://mvnrepository.com/artifact/org.postgresql/postgresql

View File

@@ -1,25 +0,0 @@
package com.io.realworld.api;
import com.io.realworld.DTO.UserSignupRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Slf4j
@Controller
public class MainController {
@GetMapping("")
public String mainHome(){
return "index";
}
@GetMapping("/register")
public String signupView(){
return "/users/signup";
}
}

View File

@@ -3,13 +3,10 @@ package com.io.realworld.api.users;
import com.io.realworld.DTO.UserSignupRequest;
import com.io.realworld.DTO.UserResponse;
import com.io.realworld.repository.User;
import com.io.realworld.service.UserService;
import com.io.realworld.service.JwtService;
import com.io.realworld.service.UserServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import javax.validation.Valid;
@@ -19,14 +16,19 @@ import javax.validation.Valid;
public class UserController {
private UserServiceImpl userService;
private final UserServiceImpl userService;
public UserController(UserServiceImpl userService) {
private final JwtService jwtService;
public UserController(UserServiceImpl userService, JwtService jwtService) {
this.userService = userService;
this.jwtService = jwtService;
}
@PostMapping(value = "/users")
public UserResponse signup(@Valid @RequestBody UserSignupRequest userSignupRequest) {
public UserResponse signup(@Valid @RequestBody UserSignupRequest userSignupRequest) {
User user = userService.signup(userSignupRequest);
log.info("register");
@@ -34,6 +36,7 @@ public class UserController {
.email(user.getEmail())
.bio(user.getBio())
.image(user.getImage())
.token(jwtService.createToken(user.getEmail()))
.build();
}
}

View File

@@ -1,22 +1,31 @@
package com.io.realworld.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.authentication.AuthenticationManagerFactoryBean;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class WebConfig {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
@@ -35,6 +44,7 @@ public class WebConfig {
.disable()
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
return http.build();
}

View File

@@ -0,0 +1,66 @@
package com.io.realworld.config.jwt;
import com.io.realworld.repository.User;
import com.io.realworld.service.JwtService;
import com.io.realworld.service.UserServiceImpl;
import lombok.AllArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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.Optional;
@Component
@AllArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String HEADER = "Authorization";
private final JwtService jwtService;
private final UserServiceImpl userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Optional<String> token = getToken(request.getHeader(HEADER));
String email = null;
String jwt = null;
if(token.isPresent()){
jwt = String.valueOf(token);
email = jwtService.getEmail(jwt);
}
if(email != null){
User findUser = userService.findByEmail(email);
if(jwtService.validateToken(jwt,findUser)){
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(findUser,null, AuthorityUtils.NO_AUTHORITIES);
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(request,response);
}
private Optional<String> getToken(String header) {
if(header == null){
return Optional.empty();
}else{
String[] s = header.split(" ");
if(s.length < 2){
return Optional.empty();
}else{
return Optional.ofNullable(s[1]);
}
}
}
}

View File

@@ -0,0 +1,18 @@
package com.io.realworld.config.jwt;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
@Component
@Getter
@PropertySource("classpath:application.properties")
public class JwtConfig {
@Value("${real-world.token.expiry}")
private Long expiry;
@Value("${real-world.token.key}")
private String key;
}

View File

@@ -2,13 +2,16 @@ package com.io.realworld.repository;
import lombok.Builder;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.Collection;
@Table(name = "users")
@Entity
@Getter
public class User {
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@@ -30,11 +33,38 @@ public class User {
this.image = image;
}
protected User(){}
public static User of(String username, String email, String password) {
return new User(username, email, password, "", "");
}
protected User() {
@Override
public String getUsername(){
return this.email;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}

View File

@@ -2,7 +2,9 @@ package com.io.realworld.repository;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Long> {
User save(User user);
User findByEmail(String email);
}

View File

@@ -0,0 +1,64 @@
package com.io.realworld.service;
import com.io.realworld.config.jwt.JwtConfig;
import com.io.realworld.repository.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.Optional;
@Service
@Getter
@AllArgsConstructor
public class JwtService {
private final JwtConfig jwtConfig;
private Key getSignKey(String secretKey){
byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
public Claims extractAllClaims(String token) throws ExpiredJwtException{
return Jwts.parserBuilder()
.setSigningKey(getSignKey(jwtConfig.getKey()))
.build()
.parseClaimsJws(token)
.getBody();
}
public String getEmail(String token){
return extractAllClaims(token).get("email",String.class);
}
public Boolean isTokenExpired(String token){
final Date expiration = extractAllClaims(token).getExpiration();
return expiration.before(new Date());
}
public String createToken(String email){
Claims claims = Jwts.claims();
claims.put("email",email);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getExpiry() * 1000))
.signWith(getSignKey(jwtConfig.getKey()))
.compact();
}
public Boolean validateToken(String token, User user){
final String email = getEmail(token);
return email.equals(user.getEmail()) && !isTokenExpired(token);
}
}

View File

@@ -7,11 +7,12 @@ import com.io.realworld.repository.User;
import com.io.realworld.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@@ -37,4 +38,9 @@ public class UserServiceImpl implements UserService {
return passwordEncoder.encode(password);
}
@Transactional
public User findByEmail(String email){
return userRepository.findByEmail(email);
}
}

View File

@@ -11,4 +11,9 @@ spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.format_sql=true
spring.jpq.show-sql=true
#secret
real-world.token.expiry=3000000
real-world.token.key=realworldPostGreSQL0132474654564564213131d31vfxvjfijkjdks

View File

@@ -10,16 +10,12 @@ function register() {
}
})
$.ajax({
axios({
url: "/api/users",
data: userDTO,
contentType: "application/json",
async: false,
type: "POST",
}).success(function (data) {
// Todo redirect home login status
console.log(JSON.stringify(data));
window.location.href= "/index.html";
console.log(window.location.href)
});
headers:{ "Content-Type": "application/json"},
method: "post",
}).then(function(res){
alert(res);
})
}

View File

@@ -4,6 +4,7 @@
<head>
<meta http-equiv="Content-Type" content="application/json; charset=UTF-8"/>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<title>Conduit</title>
<!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on -->
<link href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css">

View File

@@ -1,6 +1,6 @@
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<script type="text/javascript" th:src="@{/js/userAjax.js}"></script>
<script type="text/javascript" th:src="@{/js/userAxios.js}"></script>
<th:block th:replace="framents/header :: HeaderFragment"></th:block>
<div class="auth-page">
<div class="container page">
@@ -12,19 +12,19 @@
<a href="">Have an account?</a>
</p>
<ul class="error-messages">
<li>That email is already taken</li>
<ul class="error-messages" id="error">
<!--<li>That email is already taken</li>-->
</ul>
<form onsubmit="return register()";>
<form th:action="@{/api/users}" th:object="${user}" method="post">
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Your Name" id="username">
<input class="form-control form-control-lg" type="text" placeholder="Username" th:field="*{username}">
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Email" id="email">
<input class="form-control form-control-lg" type="text" placeholder="Email" th:field="*{email}">
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="password" placeholder="Password" id="password">
<input class="form-control form-control-lg" type="password" placeholder="Password" th:field="*{password}">
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right" >
Sign up

View File

@@ -17,6 +17,8 @@ import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import javax.xml.transform.Result;
import static org.mockito.ArgumentMatchers.any;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
@@ -52,13 +54,12 @@ class UserControllerTest {
//when
ResultActions resultActions = mockMvc.perform(
post("/users")
post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(signupRequestTest))
.with(csrf())
);
// then
resultActions.andExpect(status().isOk())
.andExpect(jsonPath("$.user.email", signupResponseTest.getEmail()).exists())

View File

@@ -1,5 +1,6 @@
package com.io.realworld.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.io.realworld.DTO.UserSignupRequest;
import com.io.realworld.repository.User;
import com.io.realworld.repository.UserRepository;
@@ -9,9 +10,12 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
@@ -21,6 +25,7 @@ import static org.mockito.internal.verification.VerificationModeFactory.times;
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
@InjectMocks
private UserServiceImpl userService;