├── .gitignore ├── README.md ├── api-gateway ├── .gitignore ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── github │ │ └── akhuntsaria │ │ └── apigateway │ │ ├── ApiGatewayApplication.java │ │ ├── config │ │ ├── AuthenticationFilter.java │ │ ├── RestTemplateConfig.java │ │ └── WebSecurityConfiguration.java │ │ ├── dto │ │ ├── JwtParseRequestDto.java │ │ └── JwtParseResponseDto.java │ │ └── model │ │ └── UserRole.java │ └── resources │ └── application.properties ├── auth-service ├── .gitignore ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── github │ │ └── akhuntsaria │ │ └── authservice │ │ ├── AuthServiceApplication.java │ │ ├── config │ │ ├── JwtUsernamePasswordAuthenticationFilter.java │ │ └── WebSecurityConfiguration.java │ │ ├── controller │ │ └── JwtController.java │ │ ├── dto │ │ ├── ErrorDto.java │ │ ├── JwtParseRequestDto.java │ │ ├── JwtParseResponseDto.java │ │ └── LoginDto.java │ │ ├── model │ │ └── UserRole.java │ │ └── service │ │ └── JwtService.java │ └── resources │ └── application.properties ├── discovery-server ├── .gitignore ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── github │ │ └── akhuntsaria │ │ └── discoveryserver │ │ └── DiscoveryServerApplication.java │ └── resources │ └── application.properties ├── protected-service ├── .gitignore ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── github │ │ └── akhuntsaria │ │ └── protectedservice │ │ ├── ProtectedServiceApplication.java │ │ └── controller │ │ └── ProtectedController.java │ └── resources │ └── application.properties └── request-examples.http /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Spring Boot API Gateway Demo 3 | This project demonstrates API gateway using microservices architecture, separate authentication service and service discovery. 4 | 5 | # Getting Started 6 | * Run discovery-server and other services 7 | * Run requests in request-examples.http 8 | 9 | # Architecture 10 | ![image info](https://i.imgur.com/YW4DRSd.png) 11 | 12 | # Services 13 | * **api-gateway**: Zuul edge service for routing 14 | * **discover-server**: Eureka server for service discovery 15 | * **auth-service**: JWT authentication service 16 | * **protected-service**: service with sensitive data 17 | -------------------------------------------------------------------------------- /api-gateway/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /api-gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.2.3.RELEASE 9 | 10 | 11 | com.github.akhuntsaria 12 | api-gateway 13 | 0.0.1-SNAPSHOT 14 | API Gateway 15 | API gateway for protected service 16 | 17 | 18 | 14 19 | 2.3.1 20 | 0.7.0 21 | 1.1.1.RELEASE 22 | 2.2.3.RELEASE 23 | 24 | 25 | 26 | 27 | io.jsonwebtoken 28 | jjwt 29 | ${jjwt.version} 30 | 31 | 32 | javax.xml.bind 33 | jaxb-api 34 | ${jaxb.version} 35 | 36 | 37 | org.glassfish.jaxb 38 | jaxb-runtime 39 | ${jaxb.version} 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-security 44 | ${spring-boot.version} 45 | 46 | 47 | org.springframework.cloud 48 | spring-cloud-starter-netflix-eureka-client 49 | ${spring-boot.version} 50 | 51 | 52 | org.springframework.cloud 53 | spring-cloud-starter-netflix-zuul 54 | ${spring-boot.version} 55 | 56 | 57 | org.springframework.security 58 | spring-security-jwt 59 | ${jwt.version} 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-maven-plugin 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/akhuntsaria/apigateway/ApiGatewayApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.apigateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.eureka.EnableEurekaClient; 6 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 7 | 8 | @EnableEurekaClient 9 | @EnableZuulProxy 10 | @SpringBootApplication 11 | public class ApiGatewayApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(ApiGatewayApplication.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/akhuntsaria/apigateway/config/AuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.apigateway.config; 2 | 3 | import com.github.akhuntsaria.apigateway.dto.JwtParseRequestDto; 4 | import com.github.akhuntsaria.apigateway.dto.JwtParseResponseDto; 5 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 6 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 7 | import org.springframework.security.core.context.SecurityContextHolder; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.client.RestTemplate; 10 | import org.springframework.web.filter.OncePerRequestFilter; 11 | 12 | import javax.servlet.FilterChain; 13 | import javax.servlet.ServletException; 14 | import javax.servlet.http.HttpServletRequest; 15 | import javax.servlet.http.HttpServletResponse; 16 | import java.io.IOException; 17 | import java.util.Objects; 18 | import java.util.stream.Collectors; 19 | 20 | @Component 21 | public class AuthenticationFilter extends OncePerRequestFilter { 22 | 23 | public static final String HEADER = "Authorization"; 24 | 25 | public static final String HEADER_VALUE_PREFIX = "Bearer"; 26 | 27 | private static final String JWT_PARSE_URL = "http://auth-service/v1/jwt/parse"; 28 | 29 | private final RestTemplate restTemplate; 30 | 31 | public AuthenticationFilter(RestTemplate restTemplate) { 32 | this.restTemplate = restTemplate; 33 | } 34 | 35 | @Override 36 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 37 | throws ServletException, IOException { 38 | String token = request.getHeader(HEADER); 39 | 40 | if (token != null) { 41 | token = token.replace(HEADER_VALUE_PREFIX + " ", ""); 42 | 43 | try { 44 | JwtParseResponseDto responseDto = parseJwt(token); 45 | 46 | UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( 47 | responseDto.getUsername(), 48 | null, 49 | responseDto.getAuthorities().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()) 50 | ); 51 | SecurityContextHolder.getContext().setAuthentication(auth); 52 | } catch (Exception ignore) { 53 | SecurityContextHolder.clearContext(); 54 | } 55 | } 56 | 57 | filterChain.doFilter(request, response); 58 | } 59 | 60 | private JwtParseResponseDto parseJwt(String token) { 61 | JwtParseResponseDto responseDto = restTemplate.postForObject(JWT_PARSE_URL, new JwtParseRequestDto(token), 62 | JwtParseResponseDto.class); 63 | 64 | Objects.requireNonNull(responseDto); 65 | return responseDto; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/akhuntsaria/apigateway/config/RestTemplateConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.apigateway.config; 2 | 3 | import org.springframework.cloud.client.loadbalancer.LoadBalanced; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.client.RestTemplate; 7 | 8 | @Configuration 9 | public class RestTemplateConfig { 10 | 11 | @Bean 12 | @LoadBalanced 13 | public RestTemplate restTemplate() { 14 | return new RestTemplate(); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/akhuntsaria/apigateway/config/WebSecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.apigateway.config; 2 | 3 | import com.github.akhuntsaria.apigateway.model.UserRole; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 8 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 9 | import org.springframework.security.config.http.SessionCreationPolicy; 10 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 11 | 12 | import javax.servlet.http.HttpServletResponse; 13 | 14 | @EnableWebSecurity 15 | public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { 16 | 17 | private final AuthenticationFilter authenticationFilter; 18 | 19 | public WebSecurityConfiguration(AuthenticationFilter authenticationFilter) { 20 | this.authenticationFilter = authenticationFilter; 21 | } 22 | 23 | @Override 24 | public void configure(final HttpSecurity http) throws Exception { 25 | http 26 | .csrf().disable() 27 | .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 28 | .and() 29 | .anonymous() 30 | .and() 31 | .exceptionHandling().authenticationEntryPoint((request, response, ex) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) 32 | .and() 33 | .addFilterAfter(authenticationFilter, UsernamePasswordAuthenticationFilter.class) 34 | .authorizeRequests() 35 | .antMatchers("/auth/**").permitAll() 36 | .anyRequest().hasRole(UserRole.USER.name()); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/akhuntsaria/apigateway/dto/JwtParseRequestDto.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.apigateway.dto; 2 | 3 | public class JwtParseRequestDto { 4 | 5 | private String token; 6 | 7 | public JwtParseRequestDto() { 8 | } 9 | 10 | public JwtParseRequestDto(String token) { 11 | this.token = token; 12 | } 13 | 14 | public String getToken() { 15 | return token; 16 | } 17 | 18 | public void setToken(String token) { 19 | this.token = token; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/akhuntsaria/apigateway/dto/JwtParseResponseDto.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.apigateway.dto; 2 | 3 | import java.util.List; 4 | 5 | public class JwtParseResponseDto { 6 | 7 | private String username; 8 | 9 | private List authorities; 10 | 11 | public JwtParseResponseDto() { 12 | } 13 | 14 | public JwtParseResponseDto(String username, List authorities) { 15 | this.username = username; 16 | this.authorities = authorities; 17 | } 18 | 19 | public String getUsername() { 20 | return username; 21 | } 22 | 23 | public void setUsername(String username) { 24 | this.username = username; 25 | } 26 | 27 | public List getAuthorities() { 28 | return authorities; 29 | } 30 | 31 | public void setAuthorities(List authorities) { 32 | this.authorities = authorities; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/akhuntsaria/apigateway/model/UserRole.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.apigateway.model; 2 | 3 | public enum UserRole { 4 | USER 5 | } 6 | -------------------------------------------------------------------------------- /api-gateway/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | eureka.client.fetch-registry=true 2 | eureka.client.register-with-eureka=false 3 | server.port=8080 4 | spring.application.name=api-gateway 5 | zuul.routes.auth.path=/auth/** 6 | zuul.routes.auth.service-id=auth-service 7 | zuul.routes.api.path=/api/** 8 | zuul.routes.api.service-id=protected-service 9 | zuul.sensitive-headers=Cookie,Set-Cookie -------------------------------------------------------------------------------- /auth-service/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /auth-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.2.3.RELEASE 9 | 10 | 11 | com.github.akhuntsaria 12 | auth-service 13 | 0.0.1-SNAPSHOT 14 | Auth Service 15 | Auth service for API gateway project 16 | 17 | 18 | 14 19 | 2.3.1 20 | 0.7.0 21 | 2.2.3.RELEASE 22 | 23 | 24 | 25 | 26 | io.jsonwebtoken 27 | jjwt 28 | ${jjwt.version} 29 | 30 | 31 | javax.xml.bind 32 | jaxb-api 33 | ${jaxb.version} 34 | 35 | 36 | org.glassfish.jaxb 37 | jaxb-runtime 38 | ${jaxb.version} 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-security 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-web 47 | 48 | 49 | org.springframework.cloud 50 | spring-cloud-starter-netflix-eureka-client 51 | ${spring-boot.version} 52 | 53 | 54 | 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-maven-plugin 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/github/akhuntsaria/authservice/AuthServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.authservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.eureka.EnableEurekaClient; 6 | 7 | @EnableEurekaClient 8 | @SpringBootApplication 9 | public class AuthServiceApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(AuthServiceApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/github/akhuntsaria/authservice/config/JwtUsernamePasswordAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.authservice.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.github.akhuntsaria.authservice.dto.LoginDto; 5 | import io.jsonwebtoken.Jwts; 6 | import io.jsonwebtoken.SignatureAlgorithm; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.core.AuthenticationException; 11 | import org.springframework.security.core.GrantedAuthority; 12 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 13 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 14 | 15 | import javax.servlet.FilterChain; 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpServletResponse; 18 | import java.io.IOException; 19 | import java.time.Instant; 20 | import java.util.Collections; 21 | import java.util.Date; 22 | import java.util.stream.Collectors; 23 | 24 | public class JwtUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { 25 | 26 | public static final String HEADER = "Authorization"; 27 | 28 | public static final String HEADER_VALUE_PREFIX = "Bearer"; 29 | 30 | private final String signingKey; 31 | 32 | public JwtUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager, String signingKey) { 33 | super(new AntPathRequestMatcher("/v1/login", "POST")); 34 | setAuthenticationManager(authenticationManager); 35 | this.signingKey = signingKey; 36 | } 37 | 38 | @Override 39 | public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 40 | throws AuthenticationException, IOException { 41 | 42 | LoginDto loginDto = new ObjectMapper().readValue(request.getInputStream(), LoginDto.class); 43 | 44 | return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken( 45 | loginDto.getUsername(), 46 | loginDto.getPassword(), 47 | Collections.emptyList() 48 | )); 49 | } 50 | 51 | @Override 52 | protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, 53 | Authentication auth) { 54 | Instant now = Instant.now(); 55 | 56 | String token = Jwts.builder() 57 | .setSubject(auth.getName()) 58 | .claim("authorities", auth.getAuthorities().stream() 59 | .map(GrantedAuthority::getAuthority).collect(Collectors.toList())) 60 | .setIssuedAt(Date.from(now)) 61 | .setExpiration(Date.from(now.plusSeconds(8 * 60 * 60))) // token expires in 8 hours 62 | .signWith(SignatureAlgorithm.HS256, signingKey.getBytes()) 63 | .compact(); 64 | response.addHeader(HEADER, HEADER_VALUE_PREFIX + " " + token); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/github/akhuntsaria/authservice/config/WebSecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.authservice.config; 2 | 3 | import com.github.akhuntsaria.authservice.model.UserRole; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 10 | import org.springframework.security.config.http.SessionCreationPolicy; 11 | import org.springframework.security.crypto.factory.PasswordEncoderFactories; 12 | import org.springframework.security.crypto.password.PasswordEncoder; 13 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 14 | 15 | import javax.servlet.http.HttpServletResponse; 16 | 17 | @EnableWebSecurity 18 | public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { 19 | 20 | private final String signingKey; 21 | 22 | @Autowired 23 | public WebSecurityConfiguration(@Value("${security.jwt.signing-key}") String signingKey) { 24 | this.signingKey = signingKey; 25 | } 26 | 27 | @Autowired 28 | public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { 29 | PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); 30 | auth.inMemoryAuthentication() 31 | .withUser("username").password(encoder.encode("password")).roles(UserRole.USER.name()); 32 | } 33 | 34 | @Override 35 | protected void configure(HttpSecurity httpSecurity) throws Exception { 36 | httpSecurity 37 | .csrf().disable() 38 | .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 39 | .and() 40 | .exceptionHandling() 41 | .authenticationEntryPoint((request, response, ex) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) 42 | .and() 43 | .addFilterAfter(new JwtUsernamePasswordAuthenticationFilter(authenticationManager(), signingKey), UsernamePasswordAuthenticationFilter.class) 44 | .authorizeRequests() 45 | .antMatchers("/v1/login").permitAll() 46 | .antMatchers("/v1/jwt/parse").permitAll() 47 | .anyRequest().authenticated(); 48 | } 49 | } -------------------------------------------------------------------------------- /auth-service/src/main/java/com/github/akhuntsaria/authservice/controller/JwtController.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.authservice.controller; 2 | 3 | import com.github.akhuntsaria.authservice.dto.ErrorDto; 4 | import com.github.akhuntsaria.authservice.dto.JwtParseRequestDto; 5 | import com.github.akhuntsaria.authservice.dto.JwtParseResponseDto; 6 | import com.github.akhuntsaria.authservice.service.JwtService; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | @RestController 14 | @RequestMapping("/v1/jwt") 15 | public class JwtController { 16 | 17 | private final Logger log = LoggerFactory.getLogger(JwtController.class); 18 | 19 | final JwtService jwtService; 20 | 21 | public JwtController(JwtService jwtService) { 22 | this.jwtService = jwtService; 23 | } 24 | 25 | /** 26 | * Validate and parse JWT 27 | */ 28 | @RequestMapping(value = "/parse", method = RequestMethod.POST) 29 | public ResponseEntity getSomeSensitiveData(@RequestBody JwtParseRequestDto requestDto) { 30 | try { 31 | JwtParseResponseDto jwtParseResponseDto = jwtService.parseJwt(requestDto.getToken()); 32 | return new ResponseEntity<>(jwtParseResponseDto, HttpStatus.OK); 33 | 34 | } catch (Exception ex) { 35 | log.error("JWT parsing error: {}, token: {}", ex.getLocalizedMessage(), requestDto); 36 | ex.printStackTrace(); 37 | 38 | return new ResponseEntity<>(new ErrorDto(ex.getLocalizedMessage()), HttpStatus.UNAUTHORIZED); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/github/akhuntsaria/authservice/dto/ErrorDto.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.authservice.dto; 2 | 3 | public class ErrorDto { 4 | 5 | private String message; 6 | 7 | public ErrorDto() { 8 | } 9 | 10 | public ErrorDto(String message) { 11 | this.message = message; 12 | } 13 | 14 | public String getMessage() { 15 | return message; 16 | } 17 | 18 | public void setMessage(String message) { 19 | this.message = message; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/github/akhuntsaria/authservice/dto/JwtParseRequestDto.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.authservice.dto; 2 | 3 | public class JwtParseRequestDto { 4 | 5 | private String token; 6 | 7 | public JwtParseRequestDto() { 8 | } 9 | 10 | public JwtParseRequestDto(String token) { 11 | this.token = token; 12 | } 13 | 14 | public String getToken() { 15 | return token; 16 | } 17 | 18 | public void setToken(String token) { 19 | this.token = token; 20 | } 21 | 22 | @Override 23 | public String toString() { 24 | return "JwtParseRequestDto{" + 25 | "token='" + token + '\'' + 26 | '}'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/github/akhuntsaria/authservice/dto/JwtParseResponseDto.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.authservice.dto; 2 | 3 | import java.util.List; 4 | 5 | public class JwtParseResponseDto { 6 | 7 | private String username; 8 | 9 | private List authorities; 10 | 11 | public JwtParseResponseDto() { 12 | } 13 | 14 | public JwtParseResponseDto(String username, List authorities) { 15 | this.username = username; 16 | this.authorities = authorities; 17 | } 18 | 19 | public String getUsername() { 20 | return username; 21 | } 22 | 23 | public void setUsername(String username) { 24 | this.username = username; 25 | } 26 | 27 | public List getAuthorities() { 28 | return authorities; 29 | } 30 | 31 | public void setAuthorities(List authorities) { 32 | this.authorities = authorities; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/github/akhuntsaria/authservice/dto/LoginDto.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.authservice.dto; 2 | 3 | public class LoginDto { 4 | 5 | private String username; 6 | 7 | private String password; 8 | 9 | public LoginDto() { 10 | } 11 | 12 | public LoginDto(String username, String password) { 13 | this.username = username; 14 | this.password = password; 15 | } 16 | 17 | public String getUsername() { 18 | return username; 19 | } 20 | 21 | public void setUsername(String username) { 22 | this.username = username; 23 | } 24 | 25 | public String getPassword() { 26 | return password; 27 | } 28 | 29 | public void setPassword(String password) { 30 | this.password = password; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/github/akhuntsaria/authservice/model/UserRole.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.authservice.model; 2 | 3 | public enum UserRole { 4 | USER 5 | } 6 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/github/akhuntsaria/authservice/service/JwtService.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.authservice.service; 2 | 3 | import com.github.akhuntsaria.authservice.dto.JwtParseResponseDto; 4 | import io.jsonwebtoken.Claims; 5 | import io.jsonwebtoken.Jwts; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.List; 11 | import java.util.Objects; 12 | import java.util.Set; 13 | 14 | @Service 15 | public class JwtService { 16 | 17 | private final String signingKey; 18 | 19 | @Autowired 20 | public JwtService(@Value("${security.jwt.signing-key}") String signingKey) { 21 | this.signingKey = signingKey; 22 | } 23 | 24 | public JwtParseResponseDto parseJwt(String token) { 25 | Objects.requireNonNull(token); 26 | 27 | Claims claims = Jwts.parser() 28 | .setSigningKey(signingKey.getBytes()) 29 | .parseClaimsJws(token) 30 | .getBody(); 31 | 32 | String username = claims.getSubject(); 33 | //noinspection unchecked 34 | List authorities = claims.get("authorities", List.class); 35 | 36 | return new JwtParseResponseDto(username, authorities); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /auth-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=auth-service 2 | security.jwt.signing-key=should-be-externally-configured 3 | server.port=8081 -------------------------------------------------------------------------------- /discovery-server/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /discovery-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.3.1.RELEASE 9 | 10 | 11 | com.github.akhuntsaria 12 | discovery-server 13 | 0.0.1-SNAPSHOT 14 | Discovery server 15 | Discovery server for Spring Boot microservices demo 16 | 17 | 18 | 14 19 | Hoxton.SR6 20 | 21 | 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-starter-netflix-eureka-server 26 | 27 | 28 | 29 | 30 | 31 | 32 | org.springframework.cloud 33 | spring-cloud-dependencies 34 | ${spring-cloud.version} 35 | pom 36 | import 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-maven-plugin 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /discovery-server/src/main/java/com/github/akhuntsaria/discoveryserver/DiscoveryServerApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.discoveryserver; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; 6 | 7 | @EnableEurekaServer 8 | @SpringBootApplication 9 | public class DiscoveryServerApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(DiscoveryServerApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /discovery-server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | eureka.client.register-with-eureka=false 2 | eureka.client.fetch-registry=false 3 | server.port=8761 4 | spring.application.name=discovery-server -------------------------------------------------------------------------------- /protected-service/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /protected-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.2.3.RELEASE 9 | 10 | 11 | com.github.akhuntsaria 12 | protected-service 13 | 0.0.1-SNAPSHOT 14 | Protected Service 15 | Service which is protected by API gateway 16 | 17 | 18 | 14 19 | 2.2.3.RELEASE 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-web 26 | 27 | 28 | org.springframework.cloud 29 | spring-cloud-starter-netflix-eureka-client 30 | ${spring-boot.version} 31 | 32 | 33 | 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-maven-plugin 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /protected-service/src/main/java/com/github/akhuntsaria/protectedservice/ProtectedServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.protectedservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.eureka.EnableEurekaClient; 6 | 7 | @EnableEurekaClient 8 | @SpringBootApplication 9 | public class ProtectedServiceApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(ProtectedServiceApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /protected-service/src/main/java/com/github/akhuntsaria/protectedservice/controller/ProtectedController.java: -------------------------------------------------------------------------------- 1 | package com.github.akhuntsaria.protectedservice.controller; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.MediaType; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import javax.servlet.http.HttpServletRequest; 11 | 12 | @RestController 13 | @RequestMapping("/v1/protected") 14 | public class ProtectedController { 15 | 16 | @RequestMapping(value = "", method = RequestMethod.GET) 17 | public ResponseEntity getSomeSensitiveData(HttpServletRequest request) { 18 | String linkWithSensitiveData = "https://youtu.be/s35rVw1zskA"; 19 | String responseBody; 20 | 21 | // display link nicely in browser 22 | if (isRequestFromBrowser(request)) { 23 | responseBody = wrapLinkInHtmlTag(linkWithSensitiveData); 24 | } else { 25 | responseBody = linkWithSensitiveData; 26 | } 27 | 28 | return new ResponseEntity<>(responseBody, HttpStatus.OK); 29 | } 30 | 31 | // check if 'Accept' header in request contains passed content type 32 | private boolean acceptHeaderContainsContentType(HttpServletRequest request, String contentType) { 33 | String acceptHeader = request.getHeader("Accept"); 34 | 35 | if (acceptHeader == null) { 36 | return false; 37 | } 38 | 39 | return acceptHeader.contains(contentType); 40 | } 41 | 42 | private boolean isRequestFromBrowser(HttpServletRequest request) { 43 | return acceptHeaderContainsContentType(request, MediaType.TEXT_HTML.toString()); 44 | } 45 | 46 | private String wrapLinkInHtmlTag(String link) { 47 | return String.format("%s", link, link); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /protected-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=protected-service 2 | server.port=8082 -------------------------------------------------------------------------------- /request-examples.http: -------------------------------------------------------------------------------- 1 | ### Login 2 | POST http://localhost:8080/auth/v1/login 3 | Content-Type: application/json 4 | 5 | { 6 | "username": "username", 7 | "password": "password" 8 | } 9 | 10 | > {% 11 | client.global.set("authorizationHeaderValue", response.headers.valueOf("Authorization")); 12 | client.global.set("authorizationToken", response.headers.valueOf("Authorization").replace("Bearer ", "")); 13 | %} 14 | 15 | ### Access protected service 16 | GET http://localhost:8080/api/v1/protected 17 | Authorization: {{authorizationHeaderValue}} 18 | 19 | ### Parse JWT in auth-service 20 | POST http://localhost:8081/v1/jwt/parse 21 | Content-Type: application/json 22 | 23 | { 24 | "token": "{{authorizationToken}}" 25 | } 26 | --------------------------------------------------------------------------------