文档结构  
翻译进度:已翻译     翻译赏金:20 元 (?)    ¥ 我要打赏

使用JWTs对Restful API进行安全保护

JSON Web令牌,俗称JWTs,令牌是用来对用户进行身份验证的应用。该技术在过去的几年里已经得到普及,因为它使后端通过简单验证这些JWTs就可以接受该请求。也就是说使用JWTs应用程序可不再持有用户的cookies或其他会话数据。此特性便于扩展性,同时保持应用程序的安全性。

在身份验证过程中,当用户成功使用其凭据登录时,返回一个JSON Web令牌,并且必须在本地保存(通常在local storage)。当用户要访问受保护的路径或资源(终端),用户代理通常在请求的authorizationheader利用承载模式把JWT发送出去。

第 1 段(可获 1.68 积分)

当后台服务器接收一个包含JWT的请求,要做的第一件事是验证令牌。这包括一系列步骤,如果其中任何一项失败,则必须拒绝该请求。下面的列表显示需要的验证步骤:

  • 检查JWT格式。
  • 检查签名。
  • 验证标准的要求。
  • 检查客户端的权限(范围)。

我们不会在这篇关于JWTs细节,但是如果需要的话,这个资源可以提供更多JWTs信息和JWT验证的资源。

第 2 段(可获 1.21 积分)

RESTful Spring Boot API 概述

我们要进行安全保护的RESTful Spring Boot API 是一个任务列表管理器。任务列表是全局保存的,这意味着所有用户都将看到并与同一个列表交互。要克隆和运行这个应用程序,我们发出以下命令:

# clone the starter project
git clone https://github.com/auth0-blog/spring-boot-auth.git

cd spring-boot-auth

# run the unsecured RESTful API
gradle bootRun

按照正确输出结果,RESTful Spring Boot API将会启动并运行。我们可以使用Postman或者curl等工具来发送请求到服务终端进行测试。

第 3 段(可获 1.1 积分)

所有用于上述指令的服务终端是在taskcontroller类定义,属于com.auth0.samples.authapi.task包。除了这个类外,这个包还包含另外两个类:

  • Task:表示应用程序中任务的实体模型。
  • TaskRepository:类负责处理持久性的任务。

我们的应用程序的持久层是由称为HSQLDB的内存数据库。现实中的应用我们通常会使用PostgreSQL或MySQL数据库作为产品数据库。在但在本教程中,这个内存数据库就够了。

第 4 段(可获 1.03 积分)

为Spring Boot APIs启用用户注册

现在我们来看一下RESTful Spring Boot API发布的服务终端,我们将对它进行安全保护。第一步是允许新用户注册自己。这类特征的类创建于一个新的包称为com.auth0.samples.authapi.user。让我们创建这个包和添加一个新的实体类,它被称为applicationuser:

package com.auth0.samples.authapi.user;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class ApplicationUser {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String username;
    private String password;

    public long getId() {
        return id;
    }

    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;
    }
}
第 5 段(可获 0.89 积分)

这个实体类包含三个属性:

  • 作为应用程序中用户实例的主要标识符工作的id。
  • 用户将用来标识自己的用户名。
  • 以及检查用户标识的密码。

管理本单位的持久层,我们将创建一个接口,称为ApplicationUserRepository。该接口作为JpaRepository的扩展提供一些常用方法如保存-它将和Applicationuser类位于相同的包:

package com.auth0.samples.authapi.user;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ApplicationUserRepository extends JpaRepository<ApplicationUser, Long> {
    ApplicationUser findByUsername(String username);
}
第 6 段(可获 0.98 积分)

我们还增加了一种findByUsername这个接口。这种方法将在实现身份验证功能时使用。

允许新用户注册的服务终端将由一个新的@Controller类处理。我们将调用控制器UserController并且把它加到和类ApplicationUser同一个包中:

package com.auth0.samples.authapi.user;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
public class UserController {

    private ApplicationUserRepository applicationUserRepository;
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public UserController(ApplicationUserRepository applicationUserRepository,
                          BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.applicationUserRepository = applicationUserRepository;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @PostMapping("/sign-up")
    public void signUp(@RequestBody ApplicationUser user) {
        user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
        applicationUserRepository.save(user);
    }
}
第 7 段(可获 0.64 积分)

服务终端的实现非常简单。它所做的只是对新用户的密码进行加密(把它作为纯文本保存不是一个好主意)然后将其保存到数据库中。加密过程是由BCryptPasswordEncoder实例的处理,这是一类属于Spring的安全框架。

现在我们的应用程序有两个漏洞:

  1. 我们没有把Spring安全框架作为对我们项目的依赖。
  2. 没有默认实例BCryptPasswordEncoder可以注射在用户控件类。
第 8 段(可获 1.16 积分)

第一个问题是通过将Spring安全框架依赖项添加到 ./build.gradle文件:

...

dependencies {
    ...
    compile("org.springframework.boot:spring-boot-starter-security")
}

第二个问题,通过实施一个生成实例的方法解决缺失的bcryptpasswordencoder. 这个方法必须用@ bean注释,我们将在应用程序类中添加:

package com.auth0.samples.authapi;

// ... other imports
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@SpringBootApplication
public class Application {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // ... main method definition
}
第 9 段(可获 0.58 积分)

这将结束用户注册功能,但我们仍然缺乏对用户身份验证和授权的支持。让我们来解决这些特点下。

Spring Boot的用户身份验证和授权

为了支持我们应用程序中的身份验证和授权,我们将:

  • 实现用户身份验证过滤器发行JWT到用户发送凭据。
  • 实现一个授权过滤器验证包含JWTs的请求。
  • 创建一个自定义的实现UserDetailsService帮助Spring加载特定于用户的数据安全的框架。
  • 和扩展WebSecurityConfigurerAdapter类定制的安全框架的需求。
第 10 段(可获 1.11 积分)

在进行开发过滤器和类之前,让我们创建一个新的包称为com.auth0.samples.authapi.security。这个包将放置我们的应用程序安全相关的所有代码。

认证过滤器

我们要创建的第一个元素是负责身份验证过程的类。我们要调用这个类JWTAuthenticationFilter,并用以下代码进行实现:

package com.auth0.samples.authapi.security;

import com.auth0.samples.authapi.user.ApplicationUser;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;

import static com.auth0.samples.authapi.security.SecurityConstants.EXPIRATION_TIME;
import static com.auth0.samples.authapi.security.SecurityConstants.HEADER_STRING;
import static com.auth0.samples.authapi.security.SecurityConstants.SECRET;
import static com.auth0.samples.authapi.security.SecurityConstants.TOKEN_PREFIX;

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req,
                                                HttpServletResponse res) throws AuthenticationException {
        try {
            ApplicationUser creds = new ObjectMapper()
                    .readValue(req.getInputStream(), ApplicationUser.class);

            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            creds.getUsername(),
                            creds.getPassword(),
                            new ArrayList<>())
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest req,
                                            HttpServletResponse res,
                                            FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {

        String token = Jwts.builder()
                .setSubject(((User) auth.getPrincipal()).getUsername())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
        res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
    }
}
第 11 段(可获 0.83 积分)

注意,我们的身份验证过滤器扩展了UsernamePasswordAuthenticationFilter类创建的。当我们向Spring安全添加一个新的过滤器时,我们可以显式地定义过滤器链中的哪个位置,或者我们可以让框架自己来指出它。通过扩展安全框架中提供的过滤器,Spring可以自动识别将其放在安全链中的最佳位置。

我们的自定义身份验证过滤器重写基类的方法:

  • attemptAuthentication: 我们解析用户的凭证并且发送到AuthenticationManager。
  • successfulAuthentication: 这是用户成功登录时调用的方法。我们使用这个方法来生成一个JWT用户。
第 12 段(可获 1.4 积分)

我们的IDE可能会抱怨这个类中的代码有两个原因。首先,因为代码从一个我们还没有创建的类中导入了四个常量, SecurityConstants. 其次,因为JWTs的生成要利用类Jwts,但我们并没有把此类库作为项目依赖。

让我们先解决缺失的依赖。在./build.gradle文件中,让我们添加以下代码行:

...

dependencies {
    ...
    compile("io.jsonwebtoken:jjwt:0.7.0")
}

这将添加Java JWT:JSON Web Token for Java和Android库到我们的项目,将解决失踪类的问题。现在我们要创建SecurityConstants类:

第 13 段(可获 1.38 积分)
package com.auth0.samples.authapi.security;

public class SecurityConstants {
    public static final String SECRET = "SecretKeyToGenJWTs";
    public static final long EXPIRATION_TIME = 864_000_000; // 10 days
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String HEADER_STRING = "Authorization";
    public static final String SIGN_UP_URL = "/users/sign-up";
}

这个类包含了所有四个常量引用JWTAuthenticationFilter类,在SIGN_UP_URL常数将在稍后使用。

授权过滤器

第 14 段(可获 0.26 积分)

由于我们已经实现了负责验证用户的过滤器,现在我们需要实现负责用户授权的过滤器。我们创建这个滤器作为一种新的类,称为jwtauthorizationfilter,在com.auth0.samples.authapi.security包:

package com.auth0.samples.authapi.security;

import io.jsonwebtoken.Jwts;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;

import static com.auth0.samples.authapi.security.SecurityConstants.HEADER_STRING;
import static com.auth0.samples.authapi.security.SecurityConstants.SECRET;
import static com.auth0.samples.authapi.security.SecurityConstants.TOKEN_PREFIX;

public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    public JWTAuthorizationFilter(AuthenticationManager authManager) {
        super(authManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws IOException, ServletException {
        String header = req.getHeader(HEADER_STRING);

        if (header == null || !header.startsWith(TOKEN_PREFIX)) {
            chain.doFilter(req, res);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(req);

        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(req, res);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader(HEADER_STRING);
        if (token != null) {
            // parse the token.
            String user = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody()
                    .getSubject();

            if (user != null) {
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
            }
            return null;
        }
        return null;
    }
}
第 15 段(可获 0.41 积分)

我们已经扩展了过滤器,使Spring在我们定制的过滤器链中替换它。该过滤器最重要的部分是我们实现的私有getauthentication方法。这个方法从授权头部读取JWT,然后使用Jwts验证令牌。如果一切都到位,我们的用户在SecurityContext允许请求继续前进。

集成Spring Boot的安全过滤器

现在我们已经正确创建了两个安全过滤器,我们必须配置Spring安全过滤器链。要做到这一点,我们将创建一个新类称为WebSecurity,它在 com.auth0.samples.authapi.security包中:

第 16 段(可获 1.34 积分)

我们已经注释该类@EnableWebSecurity并且扩展WebSecurityConfigurerAdapter,这样就可以利用Spring security提供的默认的web安全配置。这允许我们通过覆盖两种方法来调整框架以满足需要:

  • configure(HttpSecurity http): 一个方法,我们可以定义哪些资源是公开的,哪些是安全的。在我们的例子中,我们设置了SIGN_UP_URL服务终端作为公共服务而其他一切都是受安全保护的。我们还配置了Spring安全过滤器链中的自定义安全过滤器。
  • configure(AuthenticationManagerBuilder auth): 这个方法中,我们定义了一个定制的实现UserDetailsService加载特定于用户的数据安全框架。我们还使用此方法设置了应用程序所使用的加密方法。(BCryptPasswordEncoder).
第 17 段(可获 1.39 积分)

Spring Security没有附带内存数据库UserDetailsService的具体实现。因此,我们创建一个新的类UserDetailsServiceImpl,它建立在com.auth0.samples.authapi.user包中:

package com.auth0.samples.authapi.user;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import static java.util.Collections.emptyList;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    private ApplicationUserRepository applicationUserRepository;

    public UserDetailsServiceImpl(ApplicationUserRepository applicationUserRepository) {
        this.applicationUserRepository = applicationUserRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        ApplicationUser applicationUser = applicationUserRepository.findByUsername(username);
        if (applicationUser == null) {
            throw new UsernameNotFoundException(username);
        }
        return new User(applicationUser.getUsername(), applicationUser.getPassword(), emptyList());
    }
}
第 18 段(可获 0.44 积分)

唯一的方法,我们必须实现是loadUserByUsername。当用户试图进行身份验证时,该方法接收用户名,在数据库中搜索包含它的记录,并且(如果找到)返回用户实例。然后,根据登录请求中用户传递的凭据,检查这个实例的属性(用户名和密码)。最后一个过程在Spring安全框架下该类之外执行。

我们现在可以放心,我们的端点不会公开暴露,我们可以支持Spring Boot的JWTS 认证和授权。检查一切,让我们运行我们的应用程序(通过IDE或通过Gradle bootrun)并发出以下请求:

第 19 段(可获 1.41 积分)

结论

RESTful Spring Boot API的JWTs安全认证并不困难。本文表明,通过创建两个类并扩展Spring安全提供的其他几个类,我们可以保护我们的服务终端不受未知用户的影响,使用户能够注册自己并基于JWTs完成用户认证.

当然,对于一个实践的应用需要更多的功能,如密码检索,但是,希望这篇文章揭秘处理JWTs授权对Spring Boot应用程序的请求最重要的部分。

第 20 段(可获 1.14 积分)

文章评论