I am developing a microservices web application.
I have this microservices:
- frontEnd service (all html files)
- Account service (with Spring Security)
- ZuulGateway service
- Demo service (a simple rest controller that return a string)
- EurekaServer service
My account service works. The login phase works and the JWT creation works. I save it in the header of the response:
response.addHeader("Authorization", "Bearer " + token);
This is the Ajax code in the frontEnd:
/* sign in submit function */
$("#submit").click(function(e) {
e.preventDefault();
$.ajax({ /* Ajax call to AccountMicroservice for login */
url : 'http://localhost:8762/token/generate-token',
type : "POST",
data : {
username : $("#username").val(),
password : $("#password").val()
},
success : function(data) {
console.log(data.token);
window.location.href = 'http://localhost:8762/test';
},
error : function(result) {
alert("Sign in failed!");
console.log(result);
}
});
});
The problems arose with the addition of Spring Cloud Netflix Zuul. The gateway server port is 8762. Each request to 'http://localhost:8762/token/generate-token' return a 401 error.
These are the Zuul gateway service. I omitted the import.
application.properties:
server.port=8762
spring.application.name=gateway-service
eureka.client.serviceUrl.defaultZone=${EUREKA_SERVER_URL:http://localhost:8761/eureka}
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
zuul.routes.test-service.path=/test/**
zuul.routes.test-service.service-id=test-service
zuul.routes.account-service.path=/token/**
zuul.routes.account-service.service-id=account-service
zuul.routes.auth-service.sensitive-headers=Cookie,Set-Cookie
webSecurityConfig:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and()
.addFilterAfter(new JwtTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
// allow all who are accessing "auth" service
.antMatchers(HttpMethod.POST, "/token/**", "/signup").permitAll()
// Any other request must be authenticated
.anyRequest().authenticated();
}
//The CORS filter bean - Configures allowed CORS any (source) to any
//(api route and method) endpoint
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin(CorsConfiguration.ALL);
//config.addAllowedHeaders(Collections.singletonList(CorsConfiguration.ALL));
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
source.registerCorsConfiguration("/**", config);
return source;
}
}
JwtTokenUtil:
@Component
public class JwtTokenUtil implements Serializable {
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(SIGNING_KEY)
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String generateToken(Authentication authentication) {
final String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.signWith(SignatureAlgorithm.HS256, SIGNING_KEY)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS*1000))
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (
username.equals(userDetails.getUsername())
&& !isTokenExpired(token));
}
UsernamePasswordAuthenticationToken getAuthentication(final String token, final Authentication existingAuth, final UserDetails userDetails) {
final JwtParser jwtParser = Jwts.parser().setSigningKey(SIGNING_KEY);
final Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token);
final Claims claims = claimsJws.getBody();
final Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
}
}
JwtTokenAuthenticationFilter:
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 1. get the authentication header. Tokens are supposed to be passed in the authentication header
String header = request.getHeader(HEADER_STRING);
String username = null;
String authToken = null;
// 2. validate the header and check the prefix
if (header != null && header.startsWith(TOKEN_PREFIX)) {
// 3. Get the token
authToken = header.replace(TOKEN_PREFIX,"");
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
System.out.println("\n\n\n\n\n\n\n\n\n\n" + username + "\n\n\n\n\n\n\n\n\n\n");
} catch (IllegalArgumentException e) {
logger.error("c'è stato un errore durante il reperimento dello username dal token", e);
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 4. Validate the token
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
//UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
UsernamePasswordAuthenticationToken authentication = jwtTokenUtil.getAuthentication(authToken, SecurityContextHolder.getContext().getAuthentication(), userDetails);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
// go to the next filter in the filter chain
chain.doFilter(request, response);
}
}
JwtAuthenticationEntryPoint:
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
String json = String.format("{\"message\": \"%s\"}", authException.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(json);
}
}
constants:
public class Constants {
public static final long ACCESS_TOKEN_VALIDITY_SECONDS = 5*60*60;
public static final String SIGNING_KEY = "devglan123r";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String HEADER_STRING = "Authorization";
public static final String AUTHORITIES_KEY = "scopes";
}