diff --git a/build.gradle b/build.gradle index 6a7f2de..af0950f 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,8 @@ dependencies { compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.0') compile('org.springframework.boot:spring-boot-starter-web') compile('io.jsonwebtoken:jjwt:0.7.0') - compileOnly('org.projectlombok:lombok') + compile('org.springframework.boot:spring-boot-starter-security') + compileOnly('org.projectlombok:lombok') runtime('com.h2database:h2') testCompile 'io.rest-assured:rest-assured:3.0.2' testCompile('org.springframework.boot:spring-boot-starter-test') diff --git a/src/main/java/io/spring/api/CurrentUserApi.java b/src/main/java/io/spring/api/CurrentUserApi.java new file mode 100644 index 0000000..a3867b1 --- /dev/null +++ b/src/main/java/io/spring/api/CurrentUserApi.java @@ -0,0 +1,28 @@ +package io.spring.api; + +import io.spring.application.user.UserQueryService; +import io.spring.core.user.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class CurrentUserApi { + private UserQueryService userQueryService; + + @Autowired + public CurrentUserApi(UserQueryService userQueryService) { + this.userQueryService = userQueryService; + } + + @RequestMapping(path = "/user", method = RequestMethod.GET) + public ResponseEntity currentUser(@AuthenticationPrincipal User currentUser, + @RequestHeader(value = "Authorization") String authorization) { + return ResponseEntity.ok(userQueryService.fetchCurrentUser(currentUser.getUsername(), authorization.split(" ")[1])); + } + +} diff --git a/src/main/java/io/spring/api/UsersApi.java b/src/main/java/io/spring/api/UsersApi.java index 5e2dbae..8126e2b 100644 --- a/src/main/java/io/spring/api/UsersApi.java +++ b/src/main/java/io/spring/api/UsersApi.java @@ -12,8 +12,10 @@ import org.hibernate.validator.constraints.NotBlank; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -21,21 +23,22 @@ import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; @RestController -@RequestMapping("/users") public class UsersApi { private UserRepository userRepository; private UserQueryService userQueryService; private String defaultImage; @Autowired - public UsersApi(UserRepository userRepository, UserQueryService userQueryService, @Value("${image.default}") String defaultImage) { + public UsersApi(UserRepository userRepository, + UserQueryService userQueryService, + @Value("${image.default}") String defaultImage) { this.userRepository = userRepository; this.userQueryService = userQueryService; this.defaultImage = defaultImage; } - @RequestMapping(method = RequestMethod.POST) - public ResponseEntity creeteUser(@Valid @RequestBody RegisterParam registerParam, BindingResult bindingResult) { + @RequestMapping(path = "/users", method = RequestMethod.POST) + public ResponseEntity createUser(@Valid @RequestBody RegisterParam registerParam, BindingResult bindingResult) { if (bindingResult.hasErrors()) { throw new InvalidRequestException(bindingResult); } diff --git a/src/main/java/io/spring/api/security/JwtTokenFilter.java b/src/main/java/io/spring/api/security/JwtTokenFilter.java new file mode 100644 index 0000000..335df3d --- /dev/null +++ b/src/main/java/io/spring/api/security/JwtTokenFilter.java @@ -0,0 +1,60 @@ +package io.spring.api.security; + +import io.spring.application.JwtService; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +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; +import java.util.Optional; + +@SuppressWarnings("SpringJavaAutowiringInspection") +public class JwtTokenFilter extends OncePerRequestFilter { + @Autowired + private UserRepository userRepository; + + @Autowired + private JwtService jwtService; + + private String header = "Authorization"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + getTokenString(request.getHeader(header)).ifPresent(token -> { + jwtService.getSubFromToken(token).ifPresent(username -> { + if (SecurityContextHolder.getContext().getAuthentication() == null) { + User user = userRepository.findByUsername(username).get(); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + user, + null, + Collections.emptyList() + ); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + }); + }); + + filterChain.doFilter(request, response); + } + + private Optional getTokenString(String header) { + if (header == null || header.split("").length < 2) { + return Optional.empty(); + } else { + return Optional.ofNullable(header.split(" ")[1]); + } + } +} + diff --git a/src/main/java/io/spring/api/security/WebSecurityConfig.java b/src/main/java/io/spring/api/security/WebSecurityConfig.java new file mode 100644 index 0000000..7b1f7bb --- /dev/null +++ b/src/main/java/io/spring/api/security/WebSecurityConfig.java @@ -0,0 +1,33 @@ +package io.spring.api.security; + +import org.springframework.boot.autoconfigure.security.Http401AuthenticationEntryPoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +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.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + @Bean + public JwtTokenFilter jwtTokenFilter() { + return new JwtTokenFilter(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable() + .exceptionHandling().authenticationEntryPoint(new Http401AuthenticationEntryPoint("Unauthenticated")) + .and() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + .authorizeRequests() + .antMatchers(HttpMethod.POST, "/users", "/users/login").permitAll() + .anyRequest().authenticated(); + + http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/src/main/java/io/spring/application/JwtService.java b/src/main/java/io/spring/application/JwtService.java index 795c2fd..f4261be 100644 --- a/src/main/java/io/spring/application/JwtService.java +++ b/src/main/java/io/spring/application/JwtService.java @@ -1,9 +1,11 @@ package io.spring.application; import io.spring.application.user.UserData; +import org.springframework.stereotype.Service; import java.util.Optional; +@Service public interface JwtService { String toToken(UserData userData); diff --git a/src/main/java/io/spring/application/user/UserQueryService.java b/src/main/java/io/spring/application/user/UserQueryService.java index 94aa18f..1c0cec0 100644 --- a/src/main/java/io/spring/application/user/UserQueryService.java +++ b/src/main/java/io/spring/application/user/UserQueryService.java @@ -20,6 +20,9 @@ public class UserQueryService { return new UserWithToken(userData, jwtService.toToken(userData)); } + public UserWithToken fetchCurrentUser(String username, String token) { + return new UserWithToken(userReadService.findOne(username), token); + } } @JsonRootName("user") diff --git a/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java b/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java index c6e73b9..1e6112b 100644 --- a/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java +++ b/src/main/java/io/spring/infrastructure/service/DefaultJwtService.java @@ -7,17 +7,20 @@ import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; import io.spring.application.JwtService; import io.spring.application.user.UserData; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import java.util.Date; import java.util.Optional; -@Service +@Component public class DefaultJwtService implements JwtService { private String secret; private int sessionTime; + @Autowired public DefaultJwtService(@Value("${jwt.secret}") String secret, @Value("${jwt.sessionTime}") int sessionTime) { this.secret = secret; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e10b0be..d6c5a86 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,7 @@ spring.jackson.deserialization.UNWRAP_ROOT_VALUE=true spring.jackson.serialization.WRAP_ROOT_VALUE=true image.default=https://static.productionready.io/images/smiley-cyrus.jpg - -mybatis.config-location=mybatis-config.xml +jwt.secret=nRvyYC4soFxBdZ-F-5Nnzz5USXstR1YylsTd-mA0aKtI9HUlriGrtkf-TiuDapkLiUCogO3JOK7kwZisrHp6wA +jwt.sessionTime=86400 +mybatis.config-location=classpath:mybatis-config.xml mybatis.mapper-locations=mapper/*Mapper.xml \ No newline at end of file diff --git a/src/test/java/io/spring/api/CurrentUserApiTest.java b/src/test/java/io/spring/api/CurrentUserApiTest.java new file mode 100644 index 0000000..2cb1588 --- /dev/null +++ b/src/test/java/io/spring/api/CurrentUserApiTest.java @@ -0,0 +1,73 @@ +package io.spring.api; + +import io.restassured.RestAssured; +import io.spring.application.JwtService; +import io.spring.application.user.UserData; +import io.spring.application.user.UserReadService; +import io.spring.core.user.User; +import io.spring.core.user.UserRepository; +import io.spring.infrastructure.service.DefaultJwtService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Optional; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +@RunWith(SpringRunner.class) +public class CurrentUserApiTest { + @MockBean + private UserRepository userRepository; + + @MockBean + private UserReadService userReadService; + + @LocalServerPort + private int port; + + @Autowired + private JwtService jwtService; + + @Before + public void setUp() throws Exception { + RestAssured.port = port; + } + + @Test + public void should_get_current_user_with_token() throws Exception { + String email = "john@jacob.com"; + String username = "johnjacob"; + + User user = new User(email, username, "123", "", "https://static.productionready.io/images/smiley-cyrus.jpg"); + when(userRepository.findByUsername(eq(username))).thenReturn(Optional.of(user)); + + UserData userData = new UserData(email, username, "", "https://static.productionready.io/images/smiley-cyrus.jpg"); + when(userReadService.findOne(eq(username))).thenReturn(userData); + + String token = jwtService.toToken(userData); + + given() + .header("Authorization", "Token " + token) + .contentType("application/json") + .when() + .get("/user") + .then() + .statusCode(200) + .body("user.email", equalTo(email)) + .body("user.username", equalTo(username)) + .body("user.bio", equalTo("")) + .body("user.image", equalTo("https://static.productionready.io/images/smiley-cyrus.jpg")) + .body("user.token", equalTo(token)); + } +} diff --git a/src/test/java/io/spring/api/UsersApiTest.java b/src/test/java/io/spring/api/UsersApiTest.java index 239f98c..a120a4c 100644 --- a/src/test/java/io/spring/api/UsersApiTest.java +++ b/src/test/java/io/spring/api/UsersApiTest.java @@ -6,6 +6,7 @@ import io.spring.application.user.UserData; import io.spring.application.user.UserReadService; import io.spring.core.user.User; import io.spring.core.user.UserRepository; +import io.spring.infrastructure.service.DefaultJwtService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -56,13 +57,10 @@ public class UsersApiTest { UserData userData = new UserData(email, username, "", "https://static.productionready.io/images/smiley-cyrus.jpg"); when(userReadService.findOne(eq(username))).thenReturn(userData); - Map param = new HashMap() {{ - put("user", new HashMap() {{ - put("email", email); - put("password", "johnnyjacob"); - put("username", username); - }}); - }}; + when(userRepository.findByUsername(eq(username))).thenReturn(Optional.empty()); + when(userRepository.findByEmail(eq(email))).thenReturn(Optional.empty()); + + Map param = prepareRegisterParameter(email, username); given() .contentType("application/json") @@ -86,13 +84,7 @@ public class UsersApiTest { String email = "john@jacob.com"; String username = ""; - Map param = new HashMap() {{ - put("user", new HashMap() {{ - put("email", email); - put("password", "johnnyjacob"); - put("username", username); - }}); - }}; + Map param = prepareRegisterParameter(email, username); given() .contentType("application/json") @@ -109,13 +101,7 @@ public class UsersApiTest { String email = "johnxjacob.com"; String username = "johnjacob"; - Map param = new HashMap() {{ - put("user", new HashMap() {{ - put("email", email); - put("password", "johnnyjacob"); - put("username", username); - }}); - }}; + Map param = prepareRegisterParameter(email, username); given() .contentType("application/json") @@ -137,13 +123,7 @@ public class UsersApiTest { email, username, "123", "bio", "" ))); - Map param = new HashMap() {{ - put("user", new HashMap() {{ - put("email", email); - put("password", "johnnyjacob"); - put("username", username); - }}); - }}; + Map param = prepareRegisterParameter(email, username); given() .contentType("application/json") @@ -166,13 +146,7 @@ public class UsersApiTest { when(userRepository.findByUsername(eq(username))).thenReturn(Optional.empty()); - Map param = new HashMap() {{ - put("user", new HashMap() {{ - put("email", email); - put("password", "johnnyjacob"); - put("username", username); - }}); - }}; + Map param = prepareRegisterParameter(email, username); given() .contentType("application/json") @@ -183,4 +157,14 @@ public class UsersApiTest { .statusCode(422) .body("errors.email[0]", equalTo("duplicated email")); } + + private HashMap prepareRegisterParameter(final String email, final String username) { + return new HashMap() {{ + put("user", new HashMap() {{ + put("email", email); + put("password", "johnnyjacob"); + put("username", username); + }}); + }}; + } } \ No newline at end of file