在spring boot api上使用jwt认证

本文翻译自:
https://auth0.com/blog/implementing-jwt-authentication-on-spring-boot/
的前半部分,也就是关于在spring boot中使用jwt的部分

我会在之后把自己的程序上传,以及在写一篇关于这个的解释,但是首先我们要把原文看一遍。

译文:

在本博客文章中, 我们将学习如何处理使用 Spring boot编写的 rest api 的身份验证和授权。我们将从 GitHub 中克隆一个简单的 spring boot应用程序, 它公开了公共端点, 然后我们将使用 spring security和 JWT 来保护这些端点。

使用JWT来保卫RESTful API的安全(译者注:JWTs是JWT的复数)

JSON Web token(通常称为 JWT) 是用于对应用程序上的用户进行身份验证的标记。这一技术在过去几年中获得了普及, 因为它使后台能够通过验证这些 JWTS 的内容来接受请求。也就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他session数据。此特性便于可伸缩性, 同时保证应用程序的安全。

在身份验证过程中, 当用户使用其凭据成功登录时, 将返回 JSON Web token, 并且必须在本地保存 (通常在本地存储中)。每当用户要访问受保护的路由或资源 (端点) 时, 用户代理(user agent)必须连同请求一起发送 JWT, 通常在授权标头中使用Bearer schema

当后端服务器接收到带有 JWT 的请求时, 首先要做的是验证token。这由一系列步骤组成, 如果其中任何一个失败, 则必须拒绝该请求。以下列表显示了所需的验证步骤:

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

在本文中, 我们不会深入讨论 JWTS 的具体细节, 但是, 如果需要, 这里提供了更多关于JWTS的东西 和JWT认证.

RESTful Spring Boot API

我们要保护的restful Spring boot API 是一个任务列表管理器。任务列表是全局保留的, 这意味着所有用户都将看到并与同一列表进行交互。要克隆并运行此应用程序, 让我们使用以下命令(译者注:此处使用到了git):

# 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这样的工具来向可用端点发出请求(译者注:原文作者这里用的是curl):

# issue a GET request to see the (empty) list of tasks
curl http://localhost:8080/tasks

# issue a POST request to create a new task
curl -H "Content-Type: application/json" -X POST -d '{
    "description": "Buy some milk(shake)"
}'  http://localhost:8080/tasks

# issue a PUT request to update the recently created task
curl -H "Content-Type: application/json" -X PUT -d '{
    "description": "Buy some milk"
}'  http://localhost:8080/tasks/1

# issue a DELETE request to remove the existing task
curl -X DELETE http://localhost:8080/tasks/1

上面的命令中使用的所有端点都在 TaskController 类中定义, 属于 com.auth0.samples.authapi.task包。除此类外, 此包还包含另外两个类:

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

我们的应用程序的持久性层由一个名为 HSQLDB 的内存中数据库支持。我们通常会在实际的应用程序中使用像 PostgreSQL 或 MySQL 这样的生产数据库, 但是对于本教程, 这个内存中的数据库将足够使用

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

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

  • id 作为应用程序中用户实例的主要标识符
  • username 用户用来识别自己的用户名
  • password 用于检查用户标识的密码

要管理此实体的持久性层, 我们将创建一个称为 ApplicationUserRepository 的接口。此接口是 JpaRepository 的扩展, 它使我们能够访问一些常用的方法 (如 save), 并将在 ApplicationUser 类的同一包中创建:

package com.auth0.samples.authapi.user;

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

public interface ApplicationUserRepository extends JpaRepository {
    ApplicationUser findByUsername(String username);
}

我们还在这个接口上添加了一个称为 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);
    }
}

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

现在我们的应用程序有两个空白:

  1. 我们没有将 Spring 安全框架添加到项目的依赖。
  2. 没有可在 UserController 类中插入的 BCryptPasswordEncoder 的默认实例。

我们通过将 Spring 安全框架的依赖项添加到. gradle 文件中来解决第一个问题:、

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

第二个问题, 缺少的 BCryptPasswordEncoder 实例, 我们通过实现一个生成 BCryptPasswordEncoder 实例的方法来解决。此方法必须用 @Bean 批注, 我们将在Application类中添加它:

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
}

这就结束了用户注册功能, 但我们仍然缺乏对用户身份验证和授权的支持。接下来我们来处理这些功能。

spring boot的用户身份验证和授权

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

  • 实现身份验证filter, 以便向发送凭据的用户发出 JWTS
  • 实现授权filter以验证包含 JWTS 的请求
  • 创建 UserDetailsService 的自定义实现, 以帮助 Spring security在框架中加载用户特定的数据
  • 扩展 WebSecurityConfigurerAdapter 类, 以根据需要自定义安全框架

在继续开发这些filter和类之前, 让我们创建一个名为 com.auth0.samples.authapi.security 的新包。这个包将保存所有与我们的应用程序的安全性相关的代码。

身份验证filter

我们要创建的第一个元素是负责身份验证过程的类。我们将调用此类 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.getBytes())
                .compact();
        res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
    }
}

请注意, 我们创建的身份验证filter扩展了 UsernamePasswordAuthenticationFilter 类。当我们在 Spring security中添加新的filter时, 我们可以明确地定义它filter链中的位置, 也可以让框架自行计算出来。通过扩展安全框架中提供的筛选器, Spring 可以自动确定将其放在安全链中的最佳位置。

我们的自定义身份验证filter覆盖基类的两种方法:

  • attemptAuthentication: 我们在这里分析用户的凭据并将其发给 AuthenticationManager
  • successfulAuthentication: 这是用户成功登录时调用的方法。我们使用此方法为该用户生成 JWT。

我们的 IDE 可能会抱怨此类中的代码, 原因有两个。首先, 代码从我们尚未创建的类SecurityConstants中导入了四个常量。第二, 因为此类在名为 JWTS 的类的帮助下生成 JWTS, 它属于我们没有添加到项目的依赖项的库。

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

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

这将向我们的项目中添加 java JWT: 用于 java 和 Android 库的 JSON Web token, 并将解决丢失的类的问题。现在, 我们必须创建 SecurityConstants 类:

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 常量。

授权 Filter

由于我们已经实现了负责对用户进行身份验证的filter, 因此我们现在需要实现负责用户授权的filter。我们将此filter作为一个新类 (称为 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.getBytes())
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody()
                    .getSubject();

            if (user != null) {
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
            }
            return null;
        }
        return null;
    }
}

我们已经扩展了 BasicAuthenticationFilter, 使spring在filter链中替换为我们的自定义实现。我们已经实现的filter中最重要的部分是私有 的getAuthentication 方法。此方法从Authorization 标头中读取 JWT, 然后使用 Jwts 验证令牌。如果一切就绪, 我们会在 SecurityContext 中设置用户, 并允许请求继续进行。

在spring boot中集成Security Filters

现在, 我们已经正确创建了两个安全filter, 我们必须在 Spring Security filter 链上配置它们。为此, 我们将在 com.auth0.samples.authapi.security 包中创建一个名为 WebSecurity 的新类:

package com.auth0.samples.authapi.security;

import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.context.annotation.Bean;

import static com.auth0.samples.authapi.security.SecurityConstants.SIGN_UP_URL;

@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
    private UserDetailsService userDetailsService;
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public WebSecurity(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().authorizeRequests()
                .antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                // this disables session creation on Spring Security
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
    }

  @Bean
  CorsConfigurationSource corsConfigurationSource() {
    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
    return source;
  }
}

我们用 @EnableWebSecurity 对此类进行了注释, 并使其扩展 WebSecurityConfigurerAdapter 以利用 Spring security提供的默认 web 安全配置。这使我们可以通过定义三个方法来微调框架以满足我们的需要:

  • configure(HttpSecurity http): 一种我们可以定义哪些资源是公共的, 哪些是受保护的的方法。在我们的例子中, 我们将 SIGN_UP_URL 端点设置为公共的, 其他所有内容都是受保护的。我们还通过 http.cors() 配置 CORS (跨源资源共享Cross-Origin Resource Sharing) 支持, 并在 Spring Security filter 链中添加自定义安全filter。
  • configure(AuthenticationManagerBuilder auth): 一种我们定义了 UserDetailsService 的自定义实现的方法, 以便在安全框架中加载用户特定的数据。我们还使用此方法来设置应用程序使用的加密方法 (BCryptPasswordEncoder)。
  • corsConfigurationSource (): 一种可以允许/限制我们的 CORS 支持的方法。在我们的情况下, 我们把它公开, 允许来自任何来源的请求 (**)。

spring security不附带一个具体的 UserDetailsService实现, 我们可以使用我们的内存中的数据库。因此, 我们在 com.auth0.samples.authapi.user包中创建一个名为 UserDetailsServiceImpl 的新类来提供一个:

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

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

我们现在可以放心了, 我们的端点不会公开暴露, 我们可以支持与 JWTS 在spring boot中正确的认证和授权。要检查所有内容, 请运行我们的应用程序 (通过 IDE 或通过 gradle bootRun) 并发出以下请求:

# issues a GET request to retrieve tasks with no JWT
# HTTP 403 Forbidden status is expected
curl http://localhost:8080/tasks

# registers a new user
curl -H "Content-Type: application/json" -X POST -d '{
    "username": "admin",
    "password": "password"
}' http://localhost:8080/users/sign-up

# logs into the application (JWT is generated)
curl -i -H "Content-Type: application/json" -X POST -d '{
    "username": "admin",
    "password": "password"
}' http://localhost:8080/login

# issue a POST request, passing the JWT, to create a task
# remember to replace xxx.yyy.zzz with the JWT retrieved above
curl -H "Content-Type: application/json" \
-H "Authorization: Bearer xxx.yyy.zzz" \
-X POST -d '{
    "description": "Buy watermelon"
}'  http://localhost:8080/tasks

# issue a new GET request, passing the JWT
# remember to replace xxx.yyy.zzz with the JWT retrieved above
curl -H "Authorization: Bearer xxx.yyy.zzz" http://localhost:8080/tasks

翻译到此结束,原文后面还有使用Auth0的一些内容,与本文无关,故不做翻译。

你可能感兴趣的:(在spring boot api上使用jwt认证)