Java 微服务Spring云网关+;Spring安全LDAP作为SSO+;JWT-请求/响应之间的令牌丢失
我正在使用SpringBoot开发一个微服务生态系统。目前到位的微服务:Java 微服务Spring云网关+;Spring安全LDAP作为SSO+;JWT-请求/响应之间的令牌丢失,java,spring-boot,spring-security,jwt,microservices,Java,Spring Boot,Spring Security,Jwt,Microservices,我正在使用SpringBoot开发一个微服务生态系统。目前到位的微服务: Spring Cloud Gateway-Zuul(还负责微服务下游的授权请求-从请求中提取令牌并验证用户是否具有执行请求的正确角色) SSO使用SpringSecurityLDAP(负责验证用户身份并生成JWT令牌),SSO也只有一个使用thymeleaf的登录页面 使用Thymeleaf而不使用登录页面的Web界面(目前不确定是否应该在此处使用spring security) 另一种基于浏览器请求向web ui提供数
- Spring Cloud Gateway-Zuul(还负责微服务下游的授权请求-从请求中提取令牌并验证用户是否具有执行请求的正确角色)
- SSO使用SpringSecurityLDAP(负责验证用户身份并生成JWT令牌),SSO也只有一个使用thymeleaf的登录页面
- 使用Thymeleaf而不使用登录页面的Web界面(目前不确定是否应该在此处使用spring security)
- 另一种基于浏览器请求向web ui提供数据的微服务
- 使用Eureka的发现服务
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtConfig jwtConfig;
@Value("${accessDeniedPage.url}")
private String accessDeniedUrl;
@Override
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf().disable() // Disable CSRF (cross site request forgery)
// we use stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin()
.loginPage("/sso/login")
.permitAll()
.and()
// handle an authorized attempts
// If a user try to access a resource without having enough permissions
.exceptionHandling()
.accessDeniedPage(accessDeniedUrl)
//.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
// Add a filter to validate the tokens with every request
.addFilterBefore(new JwtTokenAuthenticationFilter(jwtConfig), UsernamePasswordAuthenticationFilter.class)
// authorization requests config
.authorizeRequests()
.antMatchers("/web/**").hasAuthority("ADMIN")
// Any other request must be authenticated
.anyRequest().authenticated();
}
}
@RequiredArgsConstructor
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
private final JwtConfig jwtConfig;
@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(jwtConfig.getHeader());
// 2. validate the header and check the prefix
if(header == null || !header.startsWith(jwtConfig.getPrefix())) {
chain.doFilter(request, response); // If not valid, go to the next filter.
return;
}
// If there is no token provided and hence the user won't be authenticated.
// It's Ok. Maybe the user accessing a public path or asking for a token.
// All secured paths that needs a token are already defined and secured in config class.
// And If user tried to access without access token, then he/she won't be authenticated and an exception will be thrown.
// 3. Get the token
String token = header.replace(jwtConfig.getPrefix(), "");
try { // exceptions might be thrown in creating the claims if for example the token is expired
// 4. Validate the token
Claims claims = Jwts.parser()
.setSigningKey(jwtConfig.getSecret().getBytes())
.parseClaimsJws(token)
.getBody();
String email = claims.get("email").toString();
if(email != null) {
String[] authorities = ((String) claims.get("authorities")).split(",");
final List<String> listAuthorities = Arrays.stream(authorities).collect(Collectors.toList());
// 5. Create auth object
// UsernamePasswordAuthenticationToken: A built-in object, used by spring to represent the current authenticated / being authenticated user.
// It needs a list of authorities, which has type of GrantedAuthority interface, where SimpleGrantedAuthority is an implementation of that interface
final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
email, null, listAuthorities
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()));
// 6. Authenticate the user
// Now, user is authenticated
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception e) {
// In case of failure. Make sure it's clear; so guarantee user won't be authenticated
SecurityContextHolder.clearContext();
}
// go to the next filter in the filter chain
chain.doFilter(request, response);
}
}
有一个关于JWT的问题。这被称为“注销问题”。首先你需要了解它是什么 然后,检查负责将授权头传递给下游的令牌中继过滤器(令牌中继网关过滤器工厂) 如果查看该过滤器,您将看到JWT存储在ConcurrentHashMap(InMemoryReactiveAuth2AuthorizedClient服务)中。键是session,值是JWT。因此,将返回会话id,而不是作为响应提供的JWT头 在此之前,一切正常,但当请求到达 网关请求时没有“授权”标头,这意味着没有 JWT令牌 是。当请求到达网关时,TokenRelay筛选器从请求中获取会话id,并从ConcurrentHashMap中找到JWT,然后在下行过程中传递到授权头 这个流可能是由spring安全团队设计的,用于解决JWT注销问题
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtConfig jwtConfig;
@Value("${accessDeniedPage.url}")
private String accessDeniedUrl;
@Override
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf().disable() // Disable CSRF (cross site request forgery)
// we use stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin()
.loginPage("/sso/login")
.permitAll()
.and()
// handle an authorized attempts
// If a user try to access a resource without having enough permissions
.exceptionHandling()
.accessDeniedPage(accessDeniedUrl)
//.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
// Add a filter to validate the tokens with every request
.addFilterBefore(new JwtTokenAuthenticationFilter(jwtConfig), UsernamePasswordAuthenticationFilter.class)
// authorization requests config
.authorizeRequests()
.antMatchers("/web/**").hasAuthority("ADMIN")
// Any other request must be authenticated
.anyRequest().authenticated();
}
}
@RequiredArgsConstructor
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
private final JwtConfig jwtConfig;
@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(jwtConfig.getHeader());
// 2. validate the header and check the prefix
if(header == null || !header.startsWith(jwtConfig.getPrefix())) {
chain.doFilter(request, response); // If not valid, go to the next filter.
return;
}
// If there is no token provided and hence the user won't be authenticated.
// It's Ok. Maybe the user accessing a public path or asking for a token.
// All secured paths that needs a token are already defined and secured in config class.
// And If user tried to access without access token, then he/she won't be authenticated and an exception will be thrown.
// 3. Get the token
String token = header.replace(jwtConfig.getPrefix(), "");
try { // exceptions might be thrown in creating the claims if for example the token is expired
// 4. Validate the token
Claims claims = Jwts.parser()
.setSigningKey(jwtConfig.getSecret().getBytes())
.parseClaimsJws(token)
.getBody();
String email = claims.get("email").toString();
if(email != null) {
String[] authorities = ((String) claims.get("authorities")).split(",");
final List<String> listAuthorities = Arrays.stream(authorities).collect(Collectors.toList());
// 5. Create auth object
// UsernamePasswordAuthenticationToken: A built-in object, used by spring to represent the current authenticated / being authenticated user.
// It needs a list of authorities, which has type of GrantedAuthority interface, where SimpleGrantedAuthority is an implementation of that interface
final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
email, null, listAuthorities
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()));
// 6. Authenticate the user
// Now, user is authenticated
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception e) {
// In case of failure. Make sure it's clear; so guarantee user won't be authenticated
SecurityContextHolder.clearContext();
}
// go to the next filter in the filter chain
chain.doFilter(request, response);
}
}
@Component
public class AuthenticatedFilter extends ZuulFilter {
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
final Object object = SecurityContextHolder.getContext().getAuthentication();
if (object == null || !(object instanceof UsernamePasswordAuthenticationToken)) {
return null;
}
final UsernamePasswordAuthenticationToken user = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
final RequestContext requestContext = RequestContext.getCurrentContext();
/*
final AuthenticationDto authenticationDto = new AuthenticationDto();
authenticationDto.setEmail(user.getPrincipal().toString());
authenticationDto.setAuthenticated(true);
authenticationDto.setRoles(user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList())); */
try {
//requestContext.addZuulRequestHeader(HttpHeaders.AUTHORIZATION, (new ObjectMapper()).writeValueAsString(authenticationDto));
requestContext.addZuulRequestHeader(HttpHeaders.AUTHORIZATION, (new ObjectMapper()).writeValueAsString("authenticationDto"));
} catch (JsonProcessingException e) {
throw new ZuulException("Error on JSON processing", 500, "Parsing JSON");
}
return null;
}
}