java - Spring Boot 不记名 token 身份验证给出 401

标签 java spring-boot authentication http-status-code-401

我是 Spring Boot 新手,所以请帮助我。我已经让它工作到能够使用未经身份验证的请求生成 Bearer Token 的程度。接下来,我想使用此 token 与端点一起使用,以便对我的请求进行身份验证 - 这就是我的麻烦所在。我总是收到 401,看起来我的配置有问题。这是我的代码

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 类:

@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;
    }
}
}

安全凭证配置

@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();
}
}

用户详细信息服务Impl

@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");

}
}

网络安全

@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();
}

应用程序属性

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

v1/cooks 的调用始终返回 401。我缺少配置的哪一部分?我按照 https://medium.com/omarelgabrys-blog/microservices-with-spring-boot-authentication-with-jwt-part-3-fafc9d7187e8 上的文档进行操作

但我现在完全迷失了。请求网址为 GET http://localhost:9100/v1/cooks

响应是

{ "时间戳": "2018-10-13T20:08:13.804+0000", “状态”:401, "error": "未经授权", "message": "没有可用的消息", “路径”:“/v1/厨师” }

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>

编辑:添加了 pom 中的 application.properties 和依赖项

最佳答案

根据您的要求,您实际上不需要多个 http 安全配置,直到您对多个路径使用多个身份验证(例如,对于某些路径,您希望拥有 JWT,对于某些路径,您希望拥有基本身份验证或 auth2) )。

因此,删除 SecurityCredentialsConfig 并将 WebSecurity 更新为下面的内容,就可以了。

@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();
    }
}

关于java - Spring Boot 不记名 token 身份验证给出 401,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52797019/

相关文章:

java - Spring Boot 连接到 mysql docker

android - 当我站在 firebase 身份验证 UI 上按下后退按钮时,mt 应用程序不会退出,而是显示带有 hello world 文本的 Activity

ruby-on-rails - 重定向后登录用户时出现奇怪的错误

java - Android mpandroidchart 带截止点的折线图

java - Command Handler 是否只是从总线接收命令并发布事件?

java - 接受多部分文件的 DELETE 方法的 Mockmvc

c# - 如何将 ServiceStack RegistrationFeature 插件与 Redis 一起使用?

java - 使用 Java 和 Jackson 将 Json 序列化为没有模式的通用结构

java注解反射

java - 字符串计数的优雅解决方案?