在Spring Boot应用程序中使用Spring Security 5实现JWT令牌认证和授权

原始地址:https://dev.to/d_tomov/jwt-bearer-authentication-authorization-with-spring-security-5-in-a-spring-boot-app-2cfe

这几天,我一直在尝试编译一个关于如何在Spring Boot应用程序中实现JWT Bearer安全性的合理简单的示例。这是REST网络服务安全性的标准。我找到了一些不错的例子,但都不能满足我的兴趣。我试图尽可能收集信息,并将其作为示例清晰地呈现出来。

概述

  • 依赖项
  • 令牌服务接口
  • JWTTokenService(实现)-JWT生成和解析
  • 身份验证/授权过滤器
  • 安全配置
  • 端点示例
  • 注意事项

1. 依赖项


    org.springframework.boot
    spring-boot-starter-security


    org.springframework.boot
    spring-boot-starter-web



    io.jsonwebtoken
    jjwt-api
    0.11.1


    io.jsonwebtoken
    jjwt-impl
    0.11.1
    runtime


    io.jsonwebtoken
    jjwt-jackson
    0.11.1
    runtime

我们将应用程序作为普通Spring Boot应用程序启动。添加Spring Web用于标准REST API和Spring Security用于安全部分-下载并解压缩。
我们还需要添加io.jsonwebtoken的JWT依赖项。注意,JWT的两个依赖项是从maven中央复制的,在编译阶段不需要,只需要在应用程序运行时。编译所需的唯一依赖项是jjwt-api

2. 令牌服务接口

对于示例,令牌操作被分离到一个称为TokenService的接口中:

public interface TokenService {
    String generateToken(User user);
    UserPrincipal parseToken(String token);
}

User是应用程序中的实体,如下所示:

public class User {
    private Integer id;
    private String username;
    private String password;
    private boolean isAdmin;
    
}

UserPrincipal是将在Spring的安全上下文中的Principal对象。Principal是当前登录应用程序的用户。稍后我们将看到如何在Spring Security Context中设置它。
UserPrincipal如下所示:

public class UserPrincipal {
    private Integer id;
    private String username;
    private boolean isAdmin;
    
}

3. JWTTokenService(实现)

我们需要实现两个方法,一个用于令牌生成,一个用于令牌解析。(JWT_SECRET是一个String)

令牌生成
@Override
public String generateToken(User user) {
    Instant expirationTime = Instant.now().plus(1, ChronoUnit.HOURS);
    Date expirationDate = Date.from(expirationTime);
    Key key = Keys.hmacShaKeyFor(JWT_SECRET.getBytes());
    String compactTokenString = Jwts.builder()
            .claim("id", user.getId())
            .claim("sub", user.getUsername())
            .claim("admin", user.isAdmin())
            .setExpiration(expirationDate)
            .signWith(key, SignatureAlgorithm.HS256)
            .compact();
    return "Bearer " + compactTokenString;
}

我们使用Jwts.builder()构建我们的令牌,并将其构建为紧凑的令牌。在示例中,我们使用它来设置idsubadmin的声明,但是您可以添加任何声明。声明是将添加到JWT的正文中的信息。有一些标准声明,如sub(主题),iss(发行者),…,您可以在此处查看。我们在前面附加了带有空格的“Bearer”作为前缀,以指定身份验证方案为Bearer类型。名称“Bearer身份验证”可以理解为“给予该令牌持有者访问权限”。在此处您将需要做一些决策:

  • 令牌的到期时间是多少?这在很大程度上取决于您的用例。有非常长的到期时间的用例,如1周、1天、8小时,也有非常短的到期时间,如1小时、30分钟、10分钟。对于该示例,1小时即可。
  • 您的秘密将是什么?- 秘密将用于在签发JWT时进行签名,以便您在解析时能够验证它。秘密的长度取决于您将使用的签名算法,您可以从此网站生成一些随机秘密。- 您将使用什么秘密策略?或者您将如何管理秘密。您可以使用一个密钥并使用其签署所有的JWT。您可以对每个用户使用不同的密钥,并将它们保存在数据库中。您可以对每个令牌使用不同的密钥,并将密钥保存在令牌和数据库中。(注意:JWT的一个理念是不在数据库中查找进行身份验证的用户)可能还有其他策略,但我没有见过,所以我没有在这里列出它们。对于该示例,对所有JWT使用一个密钥进行签名即可。- 您将使用什么签名算法?- JWT使用HMAC SHA算法,这是一种用于数据完整性验证(数据认证)的算法。使用HS256是JWT的标准,但您可以使用更高级的算法如HS512。您的密钥长度取决于算法HS256 -> 256位,等等… 对于该示例,使用HS256即可。
令牌解析
/**
 * @param token - 从“Bearer”前缀中剥离的紧凑令牌
 */
@Override
public UserPrincipal parseToken(String token) {
    byte[] secretBytes = JWT_SECRET.getBytes();
    Jws jwsClaims = Jwts.parserBuilder()
            .setSigningKey(secretBytes)
            .build()
            .parseClaimsJws(token);
    String username = jwsClaims.getBody()
            .getSubject();
    Integer userId = jwsClaims.getBody()
            .get("id", Integer.class);
    boolean isAdmin = jwsClaims.getBody().get("admin", Boolean.class);
    return new UserPrincipal(userId, username, isAdmin);
}

在解析令牌时,您需要与生成JWT时使用的相同的密钥。根据您选择的秘密策略或业务逻辑,您可能需要在此处进行一些验证。使用**Jwts.parserBuilder()**将令牌解析为Jws对象,您可以在其中获取您在令牌中放置的任何声明。您知道它们存在,因为JWT是不可变的,如果有人伪造了一个令牌,解析将失败并引发无效签名异常。

4. 身份验证/授权过滤器

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final TokenService tokenService;
    public JwtAuthenticationFilter(TokenService tokenService) {
        this.tokenService = tokenService;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws IOException, ServletException {
        String authorizationHeader = httpServletRequest.getHeader("Authorization");
        if (authorizationHeaderIsInvalid(authorizationHeader)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }
        UsernamePasswordAuthenticationToken token = createToken(authorizationHeader);
        SecurityContextHolder.getContext().setAuthentication(token);
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
    private boolean authorizationHeaderIsInvalid(String authorizationHeader) {
        return authorizationHeader == null
                || !authorizationHeader.startsWith("Bearer ");
    }
    private UsernamePasswordAuthenticationToken createToken(String authorizationHeader) {
        String token = authorizationHeader.replace("Bearer ", "");
        UserPrincipal userPrincipal = tokenService.parseToken(token);
        List authorities = new ArrayList<>();
        if (userPrincipal.isAdmin()) {
            authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
        }
        return new UsernamePasswordAuthenticationToken(userPrincipal, null, authorities);
    }
}

在该示例中,我们扩展了OncePerRequestFilter,这是为了保证每个请求调度一次执行的。我们首先简单地检查“Authorization”头是否存在(通常用于传递Bearer令牌)。然后将令牌去掉其“Bearer”前缀,然后将令牌解析返回的UserPrincipal传递给UsernamePasswordAuthenticationToken,它将用作Spring Security上下文中的我们的身份验证/授权。应该使用SecurityContextHolder的setAuthentication方法将这个UsernamePasswordAuthenticationToken设置在SecurityContext中,以便以后使用。

5. 安全配置

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private TokenService tokenService;
    @Autowired
    public SecurityConfig(TokenService tokenService) {
        this.tokenService = tokenService;
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .mvcMatchers("/users", "/users/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(tokenService),
                        UsernamePasswordAuthenticationFilter.class);
    }
}

@EnableWebSecurity用于将此类标记为Web安全配置

@EnableGlobalMethodSecurity(prePostEnabled = true)用于启用@PreAuthorize注释,以在控制器方法调用之前检查权限/角色。该类扩展了WebSecurityConfigurerAdapter,这是用于安全配置的基础(适配器)类。这里禁用CSRF-在Web服务中不需要防止跨站请求伪造保护,它通常在浏览器应用程序上下文中使用。进入重点我们配置端点安全性。(顺序很重要,要小心)- 首先,对于由mvcMatchers()匹配的端点允许所有请求- 通常是注册和登录端点。- 然后我们配置除允许的端点之外的每个请求进行身份验证。这意味着Spring将在安全上下文中查找一些形式的身份验证-在我们的例子中是UsernamePasswordAuthenticationToken,如果不存在则返回403 FORBIDDEN。最后,我们添加了过滤器,并将其顺序设置为在UsernamePasswordAuthenticationFilter之前。因为我们没有扩展有序过滤器并且在过滤器类上没有注解@Order,所以需要设置顺序。

6. 端点示例

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
    @GetMapping("/hello/admin")
    @PreAuthorize("hasRole('ADMIN')")
    public String helloAdmin() {
        return "hello admin";
    }
    @GetMapping("/hello/user")
    public String helloUser() {
        UserPrincipal userPrincipal =
                (UserPrincipal) SecurityContextHolder
                        .getContext()
                        .getAuthentication()
                        .getPrincipal();
        return "hello " + userPrincipal.getUsername();
    }
}

我们有三个示例端点:

  • 仅要求身份验证 - /hello
  • 需要身份验证和作为管理员的授权 - /hello/admin
  • 使用SecurityContextHolder获取Security Context中的UserPrincipal(您自己创建的自定义类)- /hello/user

7. 注意事项

  • 这是一个使用Spring Security 5实现的简单示例Bearer JWT身份验证/授权的示例(我不认为有什么特别针对5的,但这是我使用的版本)
  • 此示例可以通过刷新令牌流程进行扩展-我可能会在将来这样做
  • 我使用了端点(/users/login),返回生成的令牌,作为替代方案,您可以使用过滤器。
    如果您对示例中的某些内容不同意,可以随时留下评论,我会考虑到它

你可能感兴趣的:(java,开发语言)