作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Sergio在使用Java和RDBMS(如Oracle)开发企业级应用程序方面有十几年的经验, PostgreSQL, and MySQL.
22
本文是关于如何设置的服务器端实现的指南 JSON Web Token (JWT) - OAuth2 authorization framework using Spring Boot and Maven.
建议对OAuth2有一个初步的了解,可以通过阅读上面链接的草案或在网上搜索有用的信息 this or this.
OAuth2是一个授权框架,取代了2006年创建的第一个版本OAuth. 它定义了客户机和一个或多个HTTP服务之间的授权流,以便获得对受保护资源的访问.
OAuth2定义了以下服务器端角色:
JSON Web Token, or JWT, 索赔的陈述说明书是否要在双方之间转让. 声明被编码为JSON对象,用作加密结构的有效负载, 允许对声明进行数字签名或加密.
包含结构可以是JSON Web Signature (JWS)或JSON Web Encryption (JWE)。.
可以选择JWT作为OAuth2协议中使用的访问和刷新令牌的格式.
由于以下特性,OAuth2和JWT在过去几年中获得了巨大的普及:
However, 如果以下考虑对项目很重要,OAuth2和JWT并不总是最佳选择:
OAuth2的主要特性之一是引入了一个授权层,以便将授权过程与资源所有者分开, for the sake of simplicity, 本文的结果是构建一个模拟所有应用程序的应用程序 resource owner, authorization server, and resource server roles. 因此,通信将只在两个实体之间流动,即服务器和客户端.
这种简化应该有助于集中在文章的目的,即.e. 在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.
关于授权,确定了三个主要领域:
The basic interface is AuthenticationManager
谁负责提供身份验证方法. The UserDetailsService
该界面是否与用户信息收集相关, 在标准JDBC或LDAP方法的情况下,哪些可以直接实现或在内部使用.
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在此基础上增加了一系列固执己见的应用程序配置和第三方库,以便在保持高质量标准的同时简化开发.
现在让我们继续讨论最初的问题,设置一个使用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 handlingusers
, 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
.
这里应用的配置与:
ClientDetailsServiceConfigurer
)
inMemory
or jdbc
methodsclientId
and clientSecret
(encoded with the chosen PasswordEncoder
bean) attributesaccessTokenValiditySeconds
and refreshTokenValiditySeconds
attributesauthorizedGrantTypes
attributescopes
methodAuthorizationServerEndpointsConfigurer
)
accessTokenConverter
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;
}
}
下一节描述应用于资源服务器的配置.
资源服务器行为通过使用 @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应用程序安全性的定义.
Spring web安全配置包含在类中 ServerSecurityConfig
, enabled by the use of @EnableWebSecurity
annotation. The @EnableGlobalMethodSecurity
允许在方法级别上指定安全性. Its attribute proxyTargetClass
是为了让它工作而设置的吗 RestController
因为控制器通常是类,不实现任何接口.
It defines the following:
authenticationProvider
passwordEncoder
HttpSecurity
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控制器内部,我们可以找到两种方法来为每个资源方法应用访问控制:
OAuth2Authentication
passed in by Spring as a parameter@PreAuthorize
or @PostAuthorize
annotationspackage 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<>());
}
}
}
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.
OAuth2是一个授权框架,允许第三方应用程序通过共享访问令牌获得对HTTP服务的有限访问. 它的规范取代并淘汰了OAuth 1.0 protocol.
JWT stands for JSON Web Token, 在双方当事人之间转让的权利要求的陈述说明. 声明被编码为JSON对象,用作加密结构的有效负载,该结构允许对声明进行数字签名或加密.
Spring Security是一个专注于为基于Spring的应用程序提供身份验证和授权的框架.
Spring Boot是Spring平台和第三方库的一个固执己见的观点,它允许最小化基于Spring的应用程序的配置,同时保持生产级的质量水平.
Located in 马焦雷城堡,意大利博洛尼亚大都会
Member since December 11, 2018
Sergio在使用Java和RDBMS(如Oracle)开发企业级应用程序方面有十几年的经验, PostgreSQL, and MySQL.
22
World-class articles, delivered weekly.
World-class articles, delivered weekly.
Join the Toptal® community.