问题
I am new to Spring boot so please help me. I have got it working to the point where I am able to generate a Bearer Token
with an unauthenticated request. Next I want to use this token to use with an endpoint so that my request is authenticated - this is where my trouble is coming in. I am always getting a 401, looks like something is wrong with my config. Here is my code
public class ApplicationUser {
private String username;
private String password;
private String role;
public ApplicationUser(String username, String password, String role) {
this.username = username;
this.password = password;
this.role = role;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getRole() {
return role;
}
}
JwtConfig Class:
@Component("jwtConfig")
public class JwtConfig {
@Value("${security.jwt.uri:/auth/**}")
private String Uri;
@Value("${security.jwt.header:Authorization}")
private String header;
@Value("${security.jwt.prefix:Bearer }")
private String prefix;
@Value("${security.jwt.expiration:#{24*60*60}}")
private int expiration;
@Value("${security.jwt.secret:JwtSecretKey}")
private String secret;
public String getUri() {
return Uri;
}
public String getHeader() {
return header;
}
public String getPrefix() {
return prefix;
}
public int getExpiration() {
return expiration;
}
public String getSecret() {
return secret;
}
}
JwtTokenAuthenticationFilter
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
private final JwtConfig jwtConfig;
public JwtTokenAuthenticationFilter(JwtConfig jwtConfig) {
this.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 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 username = claims.getSubject();
if(username != null) {
@SuppressWarnings("unchecked")
List<String> authorities = (List<String>) claims.get("authorities");
// 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
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username, null, authorities.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);
}
}
JwtUsernameAndPasswordAuthenticationFilter
public class JwtUsernameAndPasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
// We use auth manager to validate the user credentials
private AuthenticationManager authManager;
private final JwtConfig jwtConfig;
public JwtUsernameAndPasswordAuthenticationFilter(AuthenticationManager authManager, JwtConfig jwtConfig) {
this.authManager = authManager;
this.jwtConfig = jwtConfig;
// By default, UsernamePasswordAuthenticationFilter listens to "/login" path.
// In our case, we use "/auth". So, we need to override the defaults.
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(jwtConfig.getUri(), "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
// 1. Get credentials from request
UserCredentials creds = new ObjectMapper().readValue(request.getInputStream(), UserCredentials.class);
// 2. Create auth object (contains credentials) which will be used by auth manager
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
creds.getUsername(), creds.getPassword(), Collections.emptyList());
// 3. Authentication manager authenticate the user, and use UserDetialsServiceImpl::loadUserByUsername() method to load the user.
return authManager.authenticate(authToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// Upon successful authentication, generate a token.
// The 'auth' passed to successfulAuthentication() is the current authenticated user.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth) throws IOException, ServletException {
Long now = System.currentTimeMillis();
String token = Jwts.builder()
.setSubject(auth.getName())
// Convert to list of strings.
// This is important because it affects the way we get them back in the Gateway.
.claim("authorities", auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.setIssuedAt(new Date(now))
.setExpiration(new Date(now + jwtConfig.getExpiration() * 1000)) // in milliseconds
.signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret().getBytes())
.compact();
// Add token to header
response.addHeader(jwtConfig.getHeader(), jwtConfig.getPrefix() + token);
}
// A (temporary) class just to represent the user credentials
private static class UserCredentials {
private String username, password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
}
SecurityCredentialsConfig
@Configuration
@EnableWebSecurity(debug=true)
public class SecurityCredentialsConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private JwtConfig jwtConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
// make sure we use stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// handle an authorized attempts
.exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
// Add a filter to validate user credentials and add token in the response header
// What's the authenticationManager()?
// An object provided by WebSecurityConfigurerAdapter, used to authenticate the user passing user's credentials
// The filter needs this auth manager to authenticate the user.
.addFilter(new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtConfig))
.authorizeRequests()
// allow all POST requests
.antMatchers(HttpMethod.POST, jwtConfig.getUri()).permitAll()
// any other requests must be authenticated
// .antMatchers(HttpMethod.GET, "/v1/**").hasRole("USER")
.anyRequest().authenticated();
}
// Spring has UserDetailsService interface, which can be overriden to provide our implementation for fetching user from database (or any other source).
// The UserDetailsService object is used by the auth manager to load the user from database.
// In addition, we need to define the password encoder also. So, auth manager can compare and verify passwords.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder encoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
final List<ApplicationUser> users = Arrays.asList(
new ApplicationUser("omar",encoder.encode("12345"), "USER"),
new ApplicationUser("admin", encoder.encode("12345"), "ADMIN")
);
for(ApplicationUser appUser: users) {
if(appUser.getUsername().equals(username)) {
List<GrantedAuthority> grantedAuthorities = AuthorityUtils
.commaSeparatedStringToAuthorityList( appUser.getRole());
return new User(appUser.getUsername(), appUser.getPassword(), grantedAuthorities);
}
}
// If user not found. Throw this exception.
throw new UsernameNotFoundException("Username: " + username + " not found");
}
}
WebSecurity
@EnableWebSecurity(debug=true)
@Order(1000)
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private JwtConfig jwtConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
// make sure we use stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// handle an authorized attempts
.exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
// Add a filter to validate the tokens with every request
.addFilterAfter(new JwtTokenAuthenticationFilter(jwtConfig), UsernamePasswordAuthenticationFilter.class)
// authorization requests config
.authorizeRequests()
// allow all who are accessing "auth" service
.antMatchers(HttpMethod.POST, jwtConfig.getUri()).permitAll()
// must be an admin if trying to access admin area (authentication is also required here)
.antMatchers("/v1/cooks").access("hasRole('ADMIN')")
//for other uris
// .antMatchers(HttpMethod.GET, "/v1/**").hasRole("USER")
// Any other request must be authenticated
.anyRequest().authenticated();
}
}
Controller
@RestController
public class CookController {
@Autowired
private CookService cookService;
// Get All Cooks
@GetMapping("/v1/cooks")
public List<Cook> getAllCooks(){
return cookService.getAllCooks();
}
application.properties
zuul.routes.auth-service.path=/auth/**
zuul.routes.auth-service.service-id=AUTH-SERVICE
zuul.routes.auth-service.strip-prefix=false
zuul.routes.auth-service.sensitive-headers=Cookie,Set-Cookie
spring.application.name=auth-service
server.port=9100
eureka.client.service-url.default-zone=http://localhost:8761/eureka
the call to v1/cooks
always returns 401. What part of the configuration I am missing? I followed the documentation at https://medium.com/omarelgabrys-blog/microservices-with-spring-boot-authentication-with-jwt-part-3-fafc9d7187e8
but I am completely lost now. Request url is GET http://localhost:9100/v1/cooks
Response is
{
"timestamp": "2018-10-13T20:08:13.804+0000",
"status": 401,
"error": "Unauthorized",
"message": "No message available",
"path": "/v1/cooks"
}
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0-b170201.1204</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
EDIT: Added application.properties and dependencies from pom
回答1:
As looking your requirement, you really don't need multiple http security configurations until you are using multiple authentications for multiple paths (like for some path you would like to have JWT and for some you would like to have basic auth or auth2).
So remove SecurityCredentialsConfig
and update WebSecurity
to below and you will be good.
@Configuration
@EnableWebSecurity(debug = true) // Enable security config. This annotation denotes config for spring security.
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private JwtConfig jwtConfig;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
// make sure we use stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// authorization requests config
.authorizeRequests()
// allow all who are accessing "auth" service
.antMatchers(HttpMethod.POST, jwtConfig.getUri()).permitAll()
// must be an admin if trying to access admin area (authentication is also required here)
.antMatchers("/v1/cooks/**").hasAuthority("ADMIN")
//for other uris
// .antMatchers(HttpMethod.GET, "/v1/**").hasRole("USER")
// Any other request must be authenticated
.anyRequest().authenticated()
.and()
// handle an authorized attempts
.exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
// Add a filter to validate the tokens with every request
.addFilterAfter(new JwtTokenAuthenticationFilter(jwtConfig), UsernamePasswordAuthenticationFilter.class)
.addFilter(new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtConfig));
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
回答2:
Try to add this
/**
on line in WebSecurity class,
.antMatchers("/v1/cooks/**" ).access("hasRole('ADMIN')")
please, if you can provide logs and version on spring boot and security dependencies this could help us much.
回答3:
@KK1957
- Try to remove spring-boot-starter-security dependency from your cookAPI.
- As I have seen there is only auth-service added in the zuul-server application.properties. Give cookAPI one port and application-name in the cookAPI property file. Add it in the zuul property file. zuul property file I have followed the same tutorial you have mentioned.
来源:https://stackoverflow.com/questions/52797019/spring-boot-bearer-token-authentication-giving-401