作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
德扬·米洛舍维奇的头像

Dejan Milosevic

Dejan拥有使用顶级Java和JavaScript框架的丰富经验,并且在DB层非常流利.

Expertise

Years of Experience

18

Share

免责声明:本文正在审查中. 有些内容可能已经过时了.

Security

安全是方便的敌人,反之亦然. 这个说法对任何系统都成立, virtual or real, 从实体房屋入口到网络银行平台. 工程师们不断地尝试为给定的用例找到正确的平衡, 倾斜的向一边或另一边倾斜的. 通常,当新的威胁出现时,我们会转向安全,远离便利. 然后,我们看看是否可以在不降低安全性的情况下恢复一些失去的便利性. 此外,这种恶性循环将永远持续下去.

spring安全性教程:安全性vs. convenience illustration

安全是方便的敌人,反之亦然.

现在让我们来看看REST安全的状态, 使用一个简单的Spring安全性教程来演示它的实际操作.

REST(代表具象状态传输)服务最初是一种极其简化的Web服务方法,它具有大量规范和繁琐的格式, such as WSDL 用于描述服务,或 SOAP 用于指定消息格式. 在REST中,我们没有这些. 我们可以在纯文本文件中描述REST服务,并使用我们想要的任何消息格式, such as JSON, XML甚至是纯文本. The simplified approach was applied to the security of REST services as well; no defined standard imposes a particular way to authenticate users.

尽管REST服务没有太多指定,但重要的一点是缺少状态. 这意味着服务器不保留任何客户机状态,会话就是一个很好的例子. 因此,服务器响应每个请求,就好像它是客户机发出的第一个请求一样. However, even now, 许多实现仍然使用基于cookie的身份验证, 哪些是从标准网站架构设计中继承的. 从安全的角度来看,REST的无状态方法使得会话cookie不合适, but nevertheless, 它们仍然被广泛使用. 除了忽略所需的无状态, 简化的方法是预期的安全权衡. 与用于Web服务的WS-Security标准相比, 创建和使用REST服务要容易得多, 因此,便利性大大提高. The trade-off is pretty slim security; session hijacking and cross-site request forgery (XSRF) are the most common security issues.

试图从服务器中删除客户端会话, 偶尔也使用其他一些方法, 如基本或摘要HTTP身份验证. Both use an Authorization 头发送用户凭据, 添加了一些编码(HTTP Basic)或加密(HTTP Digest). Of course, 它们存在与网站相同的缺陷:HTTP Basic必须在HTTPS上使用,因为用户名和密码是以容易可逆的base64编码发送的, HTTP摘要强制使用过时的MD5哈希,这被证明是不安全的.

最后,一些实现使用任意令牌对客户机进行身份验证. 这似乎是我们目前最好的选择. If implemented properly, 它修复了HTTP Basic的所有安全问题, HTTP摘要或会话cookie, it is simple to use, 它遵循无状态模式.

然而,对于这种任意的标记,几乎没有涉及到标准. 每个服务提供者对于在令牌中放入什么都有自己的想法, 以及如何编码或加密它. 使用来自不同提供者的服务需要额外的设置时间, 只是为了适应所使用的特定令牌格式. The other methods, 另一方面(会话cookie), HTTP Basic和HTTP Digest)是开发人员所熟知的, 几乎所有设备上的所有浏览器都可以开箱即用. 框架和语言已经为这些方法做好了准备, 有内置的功能来无缝地处理每一个.

JWT Authentication

JWT (JSON Web Token的缩写)是一种缺失的标准,用于在Web上使用令牌进行身份验证, not only for REST services. 目前,草案的地位是 RFC 7519. 它是健壮的,可以携带大量的信息, 但即使它的尺寸相对较小,使用起来仍然很简单. Like any other token, JWT可用于在身份提供者和服务提供者(不一定是相同的系统)之间传递经过身份验证的用户的身份。. 它还可以携带所有用户的索赔, 例如授权数据, so the service provider does not need to go into the database or external systems to verify user roles and permissions for each request; that data is extracted from the token.

Here is how JWT security is designed to work:

JWT java flow illustration

  • 客户端通过将其凭据发送到身份提供程序来登录.
  • The identity provider verifies the credentials; if all is OK, 它检索用户数据, 生成包含将用于访问服务的用户详细信息和权限的JWT, 它还设置了JWT的过期时间(可能是无限的).
  • Identity provider signs, and if needed, 加密JWT并将其发送给客户端,作为对初始请求的响应.
  • 客户机将JWT存储有限或无限的时间, 取决于标识提供程序设置的过期时间.
  • 客户端将存储在Authorization头中的JWT发送给服务提供者的每个请求.
  • 对于每个请求,服务提供者都从JWT中获取 Authorization header and decrypts it, if needed, validates the signature, and if everything is OK, 提取用户数据和权限. Based on this data solely, 同样,无需在数据库中查找进一步的详细信息或联系身份提供者, 它可以接受或拒绝客户端的请求. 唯一的要求是身份和服务提供者就加密达成协议,以便服务可以验证签名,甚至解密加密的身份.

此流程允许极大的灵活性,同时仍然保持安全性和易于开发. By using this approach, 很容易向服务提供者集群添加新的服务器节点, 初始化它们时,只允许验证签名,并通过提供共享密钥对令牌进行解密. 不需要会话复制、数据库同步或节点间通信. REST in its full glory.

JWT与其他任意令牌之间的主要区别在于令牌内容的标准化. 控件中发送JWT令牌是另一种推荐的方法 Authorization 头使用承载方案. 头文件的内容应该是这样的:

Authorization: Bearer 

REST安全实现

使REST服务按预期工作, 我们需要一种与传统授权方法略有不同的授权方法, multi-page websites.

而不是在客户端请求安全资源时通过重定向到登录页面来触发身份验证过程, REST服务器使用请求本身中的可用数据对所有请求进行身份验证, 在本例中是JWT令牌. 如果这样的身份验证失败,重定向就没有意义. The REST API simply sends an HTTP code 401 (Unauthorized) response and clients should know what to do; for example, 浏览器将显示一个动态div,允许用户提供用户名和密码.

On the other hand, 在经典验证成功后, multi-page websites, 使用HTTP代码301重定向用户(永久移动), usually to a home page or, even better, 到用户最初请求触发身份验证过程的页面. 对于REST,这也是没有意义的. 相反,我们将简单地继续执行请求,就好像资源根本没有受到保护一样, 返回HTTP代码200 (OK)和预期的响应体.

Spring Security Example

Spring REST安全性与JWT和Java

现在,让我们看看如何使用以下方法实现基于JWT令牌的REST API Java and Spring,同时尽量重用Spring Security的默认行为.

As expected, Spring Security框架附带了许多现成的插件类,它们处理“旧的”授权机制:会话cookie, HTTP Basic, and HTTP Digest. 然而,它缺乏对JWT的本地支持,我们需要亲自动手使它工作. 有关更详细的概述,您应该咨询官方 Spring安全文档.

现在,让我们从平常的开始 Spring安全过滤器定义 in web.xml:


	springSecurityFilterChain
	org.springframework.web.filter.DelegatingFilterProxy


	springSecurityFilterChain
	/*

请注意,Spring Security过滤器的名称必须与 springSecurityFilterChain 以使Spring配置的其余部分开箱即用.

接下来是与安全性相关的Spring bean的XML声明. 为了简化XML,我们将默认名称空间设置为 security by adding xmlns="http://www.springframework.org/schema/security" to the root XML element. XML的其余部分看起来像这样:

      (1)
    
       (2)
    

     (3)
          (4)
          (5)
    
    
      (6)
        
          (7)
    

    
          (8)
    
  • 在这一行,我们激活 @PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize 上下文中任何spring bean上的注释.
  • (2) We define the login and signup endpoints to skip security; even “anonymous” should be able to do these two operations.
  • (3) Next, 我们定义了应用于所有请求的过滤器链,同时添加了两个重要配置:入口点引用和将会话创建设置为 stateless (我们不希望为了安全目的而创建会话,因为我们对每个请求都使用令牌).
  • (4) We do not need csrf 保护,因为我们的令牌是免疫的.
  • (5) Next, 我们将特殊的身份验证过滤器插入到Spring预定义的过滤器链中, 就在表单登录过滤器之前.
  • (6) This bean is the declaration of our authentification filter; since it is extending Spring’s AbstractAuthenticationProcessingFilter,我们需要用XML声明它来连接它的属性(这里不能自动连接)。. 稍后我们将解释过滤器的作用.
  • 的默认成功处理程序 AbstractAuthenticationProcessingFilter is not good enough for REST purposes because it redirects the user to a success page; that is why we set our own here.
  • (8) .创建的提供商声明 authenticationManager 是我们的过滤器用来验证用户身份的.

现在让我们看看如何实现上面XML中声明的特定类. 注意,Spring将为我们连接它们. 我们从最简单的开始.

RestAuthenticationEntryPoint.java

RestAuthenticationEntryPoint实现AuthenticationEntryPoint {

    @Override
    启动HttpServletRequest请求, HttpServletResponse响应, auththeexception)抛出IOException {
        //当用户试图访问一个安全的REST资源而不提供任何凭据时调用
        //我们应该发送401 Unauthorized响应,因为没有“登录页面”可重定向
        response.sendError (HttpServletResponse.SC_UNAUTHORIZED,“未经授权的”);
    }
}

As explained above, 当身份验证失败时,该类只返回HTTP代码401(未授权), 重写Spring的默认重定向.

JwtAuthenticationSuccessHandler.java

JwtAuthenticationSuccessHandler实现AuthenticationSuccessHandler

    @Override
    HttpServletRequest请求, HttpServletResponse响应, 认证认证){
        //我们不需要在REST认证成功时做任何额外的事情, 因为没有可重定向到的页面
    }

}

这个简单的覆盖删除了成功身份验证的默认行为(重定向到用户请求的主页或任何其他页面)。. 如果你想知道为什么我们不需要重写 AuthenticationFailureHandler, 这是因为默认实现不会重定向到任何地方,如果它的重定向URL没有设置, 我们只需要避免设置URL, which is good enough.

JwtAuthenticationFilter.java

JwtAuthenticationFilter扩展AbstractAuthenticationProcessingFilter

    公共JwtAuthenticationFilter() {
        super("/**");
    }

    @Override
    protected boolean requiresAuthentication(HttpServletRequest请求,HttpServletResponse响应){
        return true;
    }

    @Override
    HttpServletRequest请求, HttpServletResponse)抛出AuthenticationException {

        String header = request.getHeader(“授权”);

        if (header == null || !header.startsWith("Bearer ")) {
            抛出新的JwtTokenMissingException("在请求头中没有找到JWT令牌");
        }

        String authToken = header.substring(7);

        JwtAuthenticationToken authRequest = new JwtAuthenticationToken(authToken);

        返回getAuthenticationManager ().验证(authRequest);
    }

    @Override
    HttpServletRequest请求, HttpServletResponse响应, FilterChain chain, 验证authResult)
            抛出IOException, ServletException {
        super.successfulAuthentication(request, response, chain, authResult);

        //由于这个身份验证在HTTP报头中,成功后我们需要正常地继续请求
        //并返回响应,就好像资源没有被保护一样
        chain.doFilter(请求、响应);
    }
}

This class is the entry point of our JWT authentication process; the filter extracts the JWT token from the request headers and delegates authentication to the injected AuthenticationManager. 如果找不到令牌,则抛出异常,停止处理请求. 我们还需要覆盖成功的身份验证,因为默认的Spring流将停止过滤器链并继续进行重定向. 请记住,我们需要链条完全执行, 包括生成响应, as explained above.

JwtAuthenticationProvider.java

JwtAuthenticationProvider扩展AbstractUserDetailsAuthenticationProvider

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public boolean supports(Class authentication) {
        返回(JwtAuthenticationToken.class.isAssignableFrom(身份验证));
    }

    @Override
    addalauthenticationchecks (UserDetails, UsernamePasswordAuthenticationToken验证)抛出AuthenticationException {
    }

    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken验证)抛出AuthenticationException {
        JwtAuthenticationToken JwtAuthenticationToken = (JwtAuthenticationToken)认证;
        String token = jwtAuthenticationToken.getToken();

        User parsedUser = jwtUtil.parseToken(token);

        if (parsedUser == null) {
            抛出new JwtTokenMalformedException("JWT令牌无效");
        }

        List authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList (parsedUser.getRole());

        返回新的AuthenticatedUser(parsedUser).getId(), parsedUser.getUsername(), token, authorityList);
    }

}

在这个类中,我们使用Spring的默认值 AuthenticationManager,但我们注入了我们自己的 AuthenticationProvider 它完成了实际的身份验证过程. 要实现这一点,我们扩展 AbstractUserDetailsAuthenticationProvider,它只需要我们返回 UserDetails 基于身份验证请求,在我们的示例中,JWT令牌封装在 JwtAuthenticationToken class. 如果令牌无效,则抛出异常. 但是,如果它是有效的并且通过 JwtUtil 如果成功,我们提取用户详细信息(我们将在 JwtUtil 类),而根本不访问数据库. 关于用户的所有信息,包括他或她的角色,都包含在令牌本身中.

JwtUtil.java

public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret;

    /**
     *尝试解析指定的字符串作为JWT令牌. 如果成功,则返回预先填充了用户名、id和角色的User对象(从令牌中提取).
     *如果不成功(令牌无效或不包含所有必需的用户属性), simply returns null.
     * 
     @param token要解析的JWT令牌
     @返回从指定令牌提取的用户对象,如果令牌无效则返回null.
     */
    public User parseToken(String token) {
        try {
            Claims body = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();

            User u = new User();
            u.setUsername(body.getSubject());
            u.setId(Long.parseLong((String) body.get("userId")));
            u.setRole((String) body.get("role"));

            return u;

        } catch (JwtException | ClassCastException e) {
            return null;
        }
    }

    /**
     *生成一个JWT令牌,其中包含用户名作为主题,userId和role作为附加声明. 这些属性取自指定的
     * User object. 令牌的有效性是无限的.
     * 
     * @param u为其生成令牌的用户
     * @return the JWT token
     */
    公共字符串generateToken(用户u) {
        Claims claims = Jwts.claims().setSubject(u.getUsername());
        claims.put("userId", u.getId() + "");
        claims.put("role", u.getRole());

        return Jwts.builder()
                .setClaims(claims)
                .signWith (SignatureAlgorithm.HS512, secret)
                .compact();
    }
}

Finally, the JwtUtil 类负责将令牌解析为 User 对象并生成令牌 User object. 它很简单,因为它使用 jjwt library to do all the JWT work. 在我们的示例中,我们只是将用户名、用户ID和用户角色存储在令牌中. 我们还可以存储更多的任意内容,并添加更多的安全功能, 例如令牌的过期. 中使用令牌的解析 AuthenticationProvider as shown above. The generateToken() 方法从登录和注册REST服务调用, 哪些是不安全的,不会触发任何安全检查或要求在请求中提供令牌. 最后,它生成将根据用户返回给客户机的令牌.

Conclusion

Although the old, 标准化的安全方法(会话cookie), HTTP Basic, 和HTTP摘要)也将与REST服务一起工作, 它们都有问题,如果使用更好的标准就可以避免这些问题. 智威汤逊及时赶到,挽救了局面, 最重要的是,它非常接近成为IETF标准.

JWT的主要优势在于以无状态的方式处理用户身份验证, and therefore scalable, way, 同时用最新的加密标准保证一切安全. 在令牌中存储声明(用户角色和权限)在分布式系统体系结构中带来了巨大的好处,在这种体系结构中,发出请求的服务器无法访问身份验证数据源.

Understanding the basics

  • What is REST?

    REST, 代表状态转移的缩写, 在web服务之间公开一致api的架构风格是什么.

  • What is JWT?

    JSON Web令牌(JWT)是一种编码信息的标准,可以作为JSON对象安全地传输.

就这一主题咨询作者或专家.
Schedule a call
德扬·米洛舍维奇的头像
Dejan Milosevic

Located in Lisbon, Portugal

Member since November 29, 2015

About the author

Dejan拥有使用顶级Java和JavaScript框架的丰富经验,并且在DB层非常流利.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Years of Experience

18

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.