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

Sergio Moretti

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

Expertise

Years of Experience

22

Share

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

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

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

OAuth2定义了以下服务器端角色:

  • Resource Owner: 负责控制资源访问的服务
  • Resource Server: 实际提供资源的服务
  • Authorization Server: 服务处理授权过程,充当客户机和资源所有者之间的中间人

JSON Web Token, or JWT, 索赔的陈述说明书是否要在双方之间转让. 声明被编码为JSON对象,用作加密结构的有效负载, 允许对声明进行数字签名或加密.

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

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

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

  • 为无状态REST协议提供无状态授权系统
  • 非常适合多个资源服务器可以共享单个授权服务器的微服务体系结构
  • 由于JSON格式,令牌内容易于在客户端管理

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

  • 无状态协议不允许在服务器端撤销访问
  • 固定令牌的生存期为管理长时间运行的会话增加了额外的复杂性,而不会损害安全性(例如.g. refresh token)
  • 在客户端为令牌提供安全存储的需求

Expected Protocol Flow

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

这种简化应该有助于集中在文章的目的,即.e. 在Spring Boot环境中设置这样一个系统.

简化流程如下:

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

Authentication flow diagram

Spring Security and Spring Boot

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

项目管理工具的选择是 Maven,但由于项目的简单性,应该不难切换到其他工具,如 Gradle.

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

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

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

Security is mostly about authentication, i.e. the verification of the identity, and authorization,授予对资源的访问权.

Spring安全性支持大量的身份验证模型, 由第三方提供或本地实现. A list can be found here.

关于授权,确定了三个主要领域:

  1. Web requests authorization
  2. Method level authorization
  3. 访问域对象实例授权

Authentication

The basic interface is AuthenticationManager 谁负责提供身份验证方法. The UserDetailsService 该界面是否与用户信息收集相关, 在标准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 公开为表示资源访问规则的声明性方式.

方法的存在首先启用方法安全性 @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版本兼容的最新版本).

下面是maven配置文件的摘录 pom.xml 包含与Spring Boot安全性相关的依赖项:

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

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

受保护的资源(作为资源服务器)在下面发布 /api/ 路径,而身份验证路径(作为资源所有者/授权服务器)映射到 /oauth/token, following proposed default.

App’s structure:

  • security 包含安全配置的包
  • 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

设置资源所有者和授权服务器

的存在启用授权服务器行为 @EnableAuthorizationServer annotation. 它的配置与与资源所有者行为相关的配置合并,并且两者都包含在类中 AuthorizationServerConfigurerAdapter.

这里应用的配置与:

  • Client access (using ClientDetailsServiceConfigurer)
    • 选择使用内存或基于JDBC的存储来存储客户端的详细信息 inMemory or jdbc methods
    • Client’s basic authentication using clientId and clientSecret (encoded with the chosen PasswordEncoder bean) attributes
    • 使用访问和刷新令牌的有效时间 accessTokenValiditySeconds and refreshTokenValiditySeconds attributes
    • Grant types allowed using authorizedGrantTypes attribute
    • Defines access scopes with scopes method
    • 识别客户端可访问的资源
  • Authorization server endpoint (using AuthorizationServerEndpointsConfigurer)
    • Define the use of a JWT token with accessTokenConverter
    • Define the use of an UserDetailsService and AuthenticationManager 执行身份验证的接口(作为资源所有者)
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;

   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.accesstokenvalidityseconds:43200}") // 12小时
   私有int accessTokenValiditySeconds;

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

   @Value("${jwt.refreshTokenValiditySeconds:2592000}") // 30天
   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 () {
       JwtAccessTokenConverter = new JwtAccessTokenConverter();
       return converter;
   }

}

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

Setup for Resource Server

资源服务器行为通过使用 @EnableResourceServer 注释及其配置包含在类中 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安全配置包含在类中 ServerSecurityConfig, enabled by the use of @EnableWebSecurity annotation. The @EnableGlobalMethodSecurity 允许在方法级别上指定安全性. Its attribute proxyTargetClass 是为了让它工作而设置的吗 RestController因为控制器通常是类,不实现任何接口.

It defines the following:

  • 要使用的身份验证提供程序,定义bean authenticationProvider
  • 要使用的密码编码器,定义bean passwordEncoder
  • The authentication manager bean
  • 已发布路径的安全配置 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;

   UserDetailsService;

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

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

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

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

   @Override
   (HttpSecurity)抛出异常{
       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(新CustomAccessDeniedHandler ());
   }

}

下面的代码摘录是关于实现的 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
类UserService实现UserDetailsService {

   私有最终UserRepository存储库;

   公共UserService(UserRepository)
       this.repository = repository;
   }

   @Override
   loadUserByUsername(String username)抛出UsernameNotFoundException {
       User user = repository.findByEmail(username).orElseThrow(() -> new RuntimeException("User not found: " + username));
       GrantedAuthority = 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 {

   私有最终UserRepository存储库;

   private final 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鉴权){
       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') ||(认证.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') ||(认证.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')")
   用户创建(@Valid @RequestBody用户){
       return repository.save(res);
   }

   @DeleteMapping("/{id}")
   @PreAuthorize("!hasAuthority('USER')")
   void delete(@PathVariable Long id) {
       if (repository.existsById(id)) {
           repository.deleteById(id);
       } else {
           抛出新的EntityNotFoundException(用户.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)")
   无效changePassword(@PathVariable)长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.

所有源代码都可以在这个GitHub存储库中获得: spring-glee-o-meter. 在这个GitHub存储库中可以找到一个使用发布资源的Angular客户端: glee-o-meter.

Further Reading on the Toptal Blog:

Understanding the basics

  • What is OAuth2?

    OAuth2是一个授权框架,允许第三方应用程序通过共享访问令牌获得对HTTP服务的有限访问. 它的规范取代并淘汰了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的应用程序的配置,同时保持生产级的质量水平.

就这一主题咨询作者或专家.
Schedule a call
Sergio Moretti's profile image
Sergio Moretti

Located in 马焦雷城堡,意大利博洛尼亚大都会

Member since December 11, 2018

About the author

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

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

Expertise

Years of Experience

22

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.