作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Sergio Moretti
Verified Expert in Engineering
22 Years of Experience

Sergio在使用Java和RDBMS(如Oracle)开发企业级应用程序方面有十几年的经验, PostgreSQL, and MySQL.

Expertise

Share

本文是关于如何设置的服务器端实现的指南 JSON Web Token (JWT) - OAuth2 authorization framework using Spring Boot and Maven.

建议对OAuth2有一个初步的了解,可以通过阅读上面链接的草案或在网上搜索有用的信息 this or this.

OAuth2是一个授权框架,取代了2006年创建的第一个版本OAuth. 它定义了客户机和一个或多个HTTP服务之间的授权流,以便获得对受保护资源的访问.

OAuth2 defines the following server-side roles:

  • Resource Owner: The service responsible for controlling resources’ access
  • Resource Server: The service who actually supplies the resources
  • Authorization Server: 服务处理授权过程,充当客户机和资源所有者之间的中间人

JSON Web Token, or JWT, 索赔的陈述说明书是否要在双方之间转让. 声明被编码为JSON对象,用作加密结构的有效负载, enabling the claims to be digitally signed or encrypted.

包含结构可以是JSON Web Signature (JWS)或JSON Web Encryption (JWE)。.

可以选择JWT作为OAuth2协议中使用的访问和刷新令牌的格式.

由于以下特性,OAuth2和JWT在过去几年中获得了巨大的普及:

  • 为无状态REST协议提供无状态授权系统
  • 非常适合多个资源服务器可以共享单个授权服务器的微服务体系结构
  • Token content easy to manage on client’s side due to JSON format

However, 如果以下考虑对项目很重要,OAuth2和JWT并不总是最佳选择:

  • 无状态协议不允许在服务器端撤销访问
  • 固定令牌的生存期为管理长时间运行的会话增加了额外的复杂性,而不会损害安全性(例如.g. refresh token)
  • A requirement for a secure store for a token on the client side

Expected Protocol Flow

OAuth2的主要特性之一是引入了一个授权层,以便将授权过程与资源所有者分开, for the sake of simplicity, 本文的结果是构建一个模拟所有应用程序的应用程序 resource owner, authorization server, and resource server roles. 因此,通信将只在两个实体之间流动,即服务器和客户端.

这种简化应该有助于集中在文章的目的,即.e. the setup of such a system in a Spring Boot’s environment.

The simplified flow is described below:

  1. 授权请求从客户端发送到服务器(作为资源所有者) password authorization grant
  2. Access token is returned to the client (along with refresh token)
  3. 然后,在每个受保护的资源访问请求时,将访问令牌从客户机发送到服务器(充当资源服务器)
  4. Server responds with required protected resources

Authentication flow diagram

Spring Security and Spring Boot

首先,简要介绍了本项目选用的技术栈.

The project management tool of choice is Maven,但由于项目的简单性,应该不难切换到其他工具,如 Gradle.

In the article’s continuation, we focus on Spring Security aspects only, 但是所有的代码摘录都是从一个完全工作的服务器端应用程序中提取的,该应用程序的源代码可以在公共存储库中与使用其REST资源的客户端一起获得.

Spring Security是一个框架,为基于Spring的应用程序提供几乎是声明式的安全服务. 它的根源是从春天的第一个开始,它被组织为一组模块,因为有很多不同的 security technologies covered.

让我们快速了解一下Spring Security体系结构(可以找到更详细的指南) here).

Security is mostly about authentication, i.e. the verification of the identity, and authorization, the grant of access rights to resources.

Spring security supports a huge range of authentication models, either provided by third parties or implemented natively. A list can be found here.

Regarding authorization, three main areas are identified:

  1. Web requests authorization
  2. Method level authorization
  3. Access to domain object instances authorization

Authentication

The basic interface is AuthenticationManager which is responsible to provide an authentication method. The UserDetailsService is the interface related to user’s information collection, 在标准JDBC或LDAP方法的情况下,哪些可以直接实现或在内部使用.

Authorization

The main interface is AccessDecisionManager; which implementations for all three areas listed above delegate to a chain of AccessDecisionVoter. 后一种接口的每个实例都表示对象之间的关联 Authentication (一个用户标识,命名为principal)、一个资源和一个集合 ConfigAttribute, 描述资源所有者如何允许访问资源本身的一组规则, maybe through the use of user roles.

web应用程序的安全性是使用上面描述的servlet过滤器链中的基本元素实现的, and the class WebSecurityConfigurerAdapter 公开为表示资源访问规则的声明性方式.

Method security is first enabled by the presence of the @EnableGlobalMethodSecurity(securedEnabled = true) annotation, 然后通过使用一组专门的注释来应用于每个要保护的方法,如 @Secured, @PreAuthorize, and @PostAuthorize.

Spring Boot在此基础上增加了一系列固执己见的应用程序配置和第三方库,以便在保持高质量标准的同时简化开发.

JWT OAuth2 with Spring Boot

现在让我们继续讨论最初的问题,设置一个使用Spring Boot实现OAuth2和JWT的应用程序.

虽然Java世界中存在多个服务器端OAuth2库(可以找到一个列表) here), 基于Spring的实现是自然的选择,因为我们希望它能很好地集成到Spring Security体系结构中,从而避免为使用它而处理大量的底层细节.

所有与安全相关的库依赖都由Maven在Spring Boot的帮助下处理, 在maven的配置文件中,哪一个组件是唯一需要显式版本的 pom.xml (i.e. 库版本由Maven自动推断,选择与插入的Spring Boot版本兼容的最新版本).

Find below the excerpt from maven’s configuration file pom.xml containing the dependencies related to Spring Boot security:

    
        org.springframework.boot
        spring-boot-starter-security
    
    
        org.springframework.security.oauth.boot
        spring-security-oauth2-autoconfigure
        2.1.0.RELEASE
    

应用程序既充当OAuth2授权服务器/资源所有者,又充当资源服务器.

The protected resources (as resource server) are published under /api/ 路径,而身份验证路径(作为资源所有者/授权服务器)映射到 /oauth/token, following proposed default.

App’s structure:

  • security package containing security configuration
  • errors package containing error handling
  • users, glee REST资源包,包括模型、存储库和控制器

接下来的段落将介绍上面提到的三个OAuth2角色的配置. The related classes are inside security package:

  • OAuthConfiguration, extending AuthorizationServerConfigurerAdapter
  • ResourceServerConfiguration, extending ResourceServerConfigurerAdapter
  • ServerSecurityConfig, extending WebSecurityConfigurerAdapter
  • UserService, implementing UserDetailsService

Setup for Resource Owner and Authorization Server

Authorization server behavior is enabled by the presence of @EnableAuthorizationServer annotation. 它的配置与与资源所有者行为相关的配置合并,并且两者都包含在类中 AuthorizationServerConfigurerAdapter.

The configurations applied here are related to:

  • Client access (using ClientDetailsServiceConfigurer)
    • 选择使用内存或基于JDBC的存储来存储客户端的详细信息 inMemory or jdbc methods
    • Client’s basic authentication using clientId and clientSecret (encoded with the chosen PasswordEncoder bean) attributes
    • Validity time for access and refresh tokens using accessTokenValiditySeconds and refreshTokenValiditySeconds attributes
    • Grant types allowed using authorizedGrantTypes attribute
    • Defines access scopes with scopes method
    • Identify client’s accessible resources
  • Authorization server endpoint (using AuthorizationServerEndpointsConfigurer)
    • Define the use of a JWT token with accessTokenConverter
    • Define the use of an UserDetailsService and AuthenticationManager interfaces to perform authentication (as resource owner)
package net.reliqs.gleeometer.security;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

@Configuration
@EnableAuthorizationServer
公共类OAuthConfiguration扩展AuthorizationServerConfigurerAdapter

   private final AuthenticationManager authenticationManager;

   private final PasswordEncoder passwordEncoder;

   private final UserDetailsService userService;

   @Value("${jwt.clientId:glee-o-meter}")
   private String clientId;

   @Value("${jwt.client-secret:secret}")
   private String clientSecret;

   @Value("${jwt.signing-key:123}")
   private String jwtSigningKey;

   @Value("${jwt.accessTokenValidititySeconds:43200}") // 12 hours
   private int accessTokenValiditySeconds;

   @Value("${jwt.authorizedGrantTypes:密码,authorization_code refresh_token}”)
   private String[] authorizedGrantTypes;

   @Value("${jwt.refreshTokenValiditySeconds:2592000}") // 30 days
   private int refreshTokenValiditySeconds;

   公共OAuthConfiguration(AuthenticationManager, PasswordEncoder passwordEncoder, UserDetailsService userService) {
       this.authenticationManager = authenticationManager;
       this.passwordEncoder = passwordEncoder;
       this.userService = userService;
   }

   @Override
   公共无效配置(ClientDetailsServiceConfigurer客户端)抛出异常{
       clients.inMemory()
               .withClient(clientId)
               .secret(passwordEncoder.encode(clientSecret))
               .accessTokenValiditySeconds(accessTokenValiditySeconds)
               .refreshTokenValiditySeconds(refreshTokenValiditySeconds)
               .authorizedGrantTypes(authorizedGrantTypes)
               .scopes("read", "write")
               .resourceIds("api");
   }

   @Override
   公共无效配置(最终authorizationserverendpointsconfiguratorendpoints) {
       endpoints
               .accessTokenConverter(accessTokenConverter())
               .userDetailsService(userService)
               .authenticationManager(authenticationManager);
   }

   @Bean
   JwtAccessTokenConverter accessTokenConverter() {
       JwtAccessTokenConverter = new JwtAccessTokenConverter();
       return converter;
   }

}

下一节描述应用于资源服务器的配置.

Setup for Resource Server

The resource server behavior is enabled by the use of @EnableResourceServer annotation and its configuration is contained in the class ResourceServerConfiguration.

这里唯一需要的配置是资源标识的定义,以便匹配上一个类中定义的客户端访问.

package net.reliqs.gleeometer.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

@Configuration
@EnableResourceServer
公共类ResourceServerConfiguration扩展ResourceServerConfigurerAdapter

   @Override
   公共无效配置(ResourceServerSecurityConfigurer资源){
       resources.resourceId("api");
   }

}

最后一个配置元素是关于web应用程序安全性的定义.

Web Security Setup

Spring web security configuration is contained in the class ServerSecurityConfig, enabled by the use of @EnableWebSecurity annotation. The @EnableGlobalMethodSecurity permits to specify security on the method level. Its attribute proxyTargetClass is set in order to have this working for RestController因为控制器通常是类,不实现任何接口.

It defines the following:

  • The authentication provider to use, defining the bean authenticationProvider
  • The password encoder to use, defining the bean passwordEncoder
  • The authentication manager bean
  • The security configuration for the published paths using HttpSecurity
  • Use of a custom AuthenticationEntryPoint 以便在标准Spring REST错误处理程序之外处理错误消息 ResponseEntityExceptionHandler
package net.reliqs.gleeometer.security;

import net.reliqs.gleeometer.errors.CustomAccessDeniedHandler;
import net.reliqs.gleeometer.errors.CustomAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
公共类ServerSecurityConfig扩展WebSecurityConfigurerAdapter {

   CustomAuthenticationEntryPoint;

   private final UserDetailsService userDetailsService;

   公共ServerSecurityConfig(CustomAuthenticationEntryPoint, @Qualifier("userService")
           UserDetailsService userDetailsService) {
       this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
       this.userDetailsService = userDetailsService;
   }

   @Bean
   public DaoAuthenticationProvider authenticationProvider() {
       DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
       provider.setPasswordEncoder(passwordEncoder());
       provider.setUserDetailsService(userDetailsService);
       return provider;
   }

   @Bean
   public PasswordEncoder passwordEncoder() {
       return new BCryptPasswordEncoder();
   }

   @Bean
   @Override
   公共AuthenticationManager authenticationManagerBean()抛出异常{
       return super.authenticationManagerBean();
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
               .and()
               .authorizeRequests()
               .antMatchers("/api/signin/**").permitAll()
               .antMatchers("/api/glee/**").hasAnyAuthority("ADMIN", "USER")
               .antMatchers("/api/users/**").hasAuthority("ADMIN")
               .antMatchers("/api/**").authenticated()
               .anyRequest().authenticated()
               .and().exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint).accessDeniedHandler(new CustomAccessDeniedHandler());
   }

}

The code extract below is about the implementation of UserDetailsService 接口,以便提供资源所有者的身份验证.

package net.reliqs.gleeometer.security;

import net.reliqs.gleeometer.users.User;
import net.reliqs.gleeometer.users.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;

@Service
public class UserService implements UserDetailsService {

   private final UserRepository repository;

   public UserService(UserRepository repository) {
       this.repository = repository;
   }

   @Override
   loadUserByUsername(String username)抛出UsernameNotFoundException {
       User user = repository.findByEmail(username).orElseThrow(() -> new RuntimeException("User not found: " + username));
       GrantedAuthority authority = new SimpleGrantedAuthority(user.getRole().name());
       return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), Arrays.asList(authority));
   }
}

下一节是关于REST控制器实现的描述,以便了解如何映射安全约束.

REST Controller

在REST控制器内部,我们可以找到两种方法来为每个资源方法应用访问控制:

  • Using an instance of OAuth2Authentication passed in by Spring as a parameter
  • Using @PreAuthorize or @PostAuthorize annotations
package net.reliqs.gleeometer.users;

import lombok.extern.slf4j.Slf4j;
import net.reliqs.gleeometer.errors.EntityNotFoundException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.ConstraintViolationException;
import javax.validation.Valid;
import javax.validation.constraints.Size;
import java.util.HashSet;

@RestController
@RequestMapping("/api/users")
@Slf4j
@Validated
class UserController {

   private final UserRepository repository;

   private final PasswordEncoder passwordEncoder;

   UserController(UserRepository repository, PasswordEncoder PasswordEncoder) {
       this.repository = repository;
       this.passwordEncoder = passwordEncoder;
   }

   @GetMapping
   Page all(@PageableDefault(size = Integer.MAX_VALUE) Pageable Pageable, OAuth2Authentication鉴权){
       String auth = (String) authentication.getUserAuthentication().getPrincipal();
       String role = authentication.getAuthorities().iterator().next().getAuthority();
       if (role.equals(User.Role.USER.name())) {
           return repository.findAllByEmail(auth, pageable);
       }
       return repository.findAll(pageable);
   }

   @GetMapping("/search")
   Page search(@RequestParam String email, Pageable pageable, OAuth2Authentication authentication) {
       String auth = (String) authentication.getUserAuthentication().getPrincipal();
       String role = authentication.getAuthorities().iterator().next().getAuthority();
       if (role.equals(User.Role.USER.name())) {
           return repository.findAllByEmailContainsAndEmail(email, auth, pageable);
       }
       return repository.findByEmailContains(email, pageable);
   }

   @GetMapping("/findByEmail")
   @PreAuthorize("!hasAuthority('USER') || (authentication.principal == #email)")
   用户findByEmail(@RequestParam String email, OAuth2Authentication鉴权){
       return repository.findByEmail(email).orElseThrow(() -> new EntityNotFoundException(User.class, "email", email));
   }

   @GetMapping("/{id}")
   @PostAuthorize("!hasAuthority('USER') || (returnObject != null && returnObject.email == authentication.principal)")
   User one(@PathVariable Long id) {
       return repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, "id", id.toString()));
   }

   @PutMapping("/{id}")
   @PreAuthorize("!hasAuthority('USER') || (authentication.principal == @userRepository.findById(#id).orElse(new net.reliqs.gleeometer.users.User()).email)")
   无效更新(@PathVariable长id, @有效@RequestBody用户res) {
       User u = repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, "id", id.toString()));
       res.setPassword(u.getPassword());
       res.setGlee(u.getGlee());
       repository.save(res);
   }

   @PostMapping
   @PreAuthorize("!hasAuthority('USER')")
   User create(@Valid @RequestBody User res) {
       return repository.save(res);
   }

   @DeleteMapping("/{id}")
   @PreAuthorize("!hasAuthority('USER')")
   void delete(@PathVariable Long id) {
       if (repository.existsById(id)) {
           repository.deleteById(id);
       } else {
           throw new EntityNotFoundException(User.class, "id", id.toString());
       }
   }

   @PutMapping("/{id}/changePassword")
   @PreAuthorize("!hasAuthority('USER') || (#oldPassword != null && !#oldPassword.isEmpty() && authentication.principal == @userRepository.findById(#id).orElse(new net.reliqs.gleeometer.users.User()).email)")
   void changePassword(@PathVariable Long id, @RequestParam(required = false) String oldPassword, @Valid @Size(min = 3) @RequestParam String newPassword) {
       User user = repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, "id", id.toString()));
       if (oldPassword == null || oldPassword.isEmpty() || passwordEncoder.matches(oldPassword, user.getPassword())) {
           user.setPassword(passwordEncoder.encode(newPassword));
           repository.save(user);
       } else {
           throw new ConstraintViolationException("old password doesn't match", new HashSet<>());
       }
   }
}

Conclusion

Spring Security和Spring Boot允许以近乎声明的方式快速设置完整的OAuth2授权/身份验证服务器. 通过直接配置OAuth2客户机的属性,可以进一步缩短设置时间 application.properties/yml file, as explained in this tutorial.

All source code is available in this GitHub repository: spring-glee-o-meter. 在这个GitHub存储库中可以找到一个使用发布资源的Angular客户端: glee-o-meter.

Further Reading on the Toptal Blog:

Understanding the basics

  • What is OAuth2?

    OAuth2是一个授权框架,允许第三方应用程序通过共享访问令牌获得对HTTP服务的有限访问. Its specification supersedes and obsoletes OAuth 1.0 protocol.

  • What is JWT?

    JWT stands for JSON Web Token, 在双方当事人之间转让的权利要求的陈述说明. 声明被编码为JSON对象,用作加密结构的有效负载,该结构允许对声明进行数字签名或加密.

  • What is Spring Security?

    Spring Security是一个专注于为基于Spring的应用程序提供身份验证和授权的框架.

  • What is Spring Boot?

    Spring Boot是Spring平台和第三方库的一个固执己见的观点,它允许最小化基于Spring的应用程序的配置,同时保持生产级的质量水平.

Hire a Toptal expert on this topic.
Hire Now
Sergio Moretti

Sergio Moretti

Verified Expert in Engineering
22 Years of Experience

Castel Maggiore, Metropolitan City of Bologna, Italy

Member since December 11, 2018

About the author

Sergio在使用Java和RDBMS(如Oracle)开发企业级应用程序方面有十几年的经验, PostgreSQL, and MySQL.

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

Expertise

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Toptal Developers

Join the Toptal® community.

" class="hidden">SNH48中国官方网站