• 欢迎访问 winrains 的个人网站!
  • 本网站主要从互联网整理和收集了与Java、网络安全、Linux等技术相关的文章,供学习和研究使用。如有侵权,请留言告知,谢谢!

Spring boot使用Spring Security和OAuth2保护REST接口

OAuth2 winrains 来源:架构师公社 1年前 (2019-08-31) 70次浏览

在本文中,我将会演示SpringSecurity+OAuth2如何去保护SpringBoot上的RESTAPI端点,以及客户端和用户凭证如何存储在关系数据库中。因此,我们需要做到以下几点:

配置Spring Security +数据库

创建授权服务器

创建资源服务器

获取访问令牌和刷新令牌

使用访问令牌获取安全资源

一、引言

OAuth2.0定义了一个委托协议,这个协议可用来启用Web的应用程序和API的网络传输。OAuth可以用于各种各样的应用程序,包括为用户身份验证提供机制。

OAuth角色

用户在OAuth中定义了四个重要的角色:

资源所有者(用户):资源所有者,这里可以理解为用户。

资源服务器(API服务器):服务器托管受保护的资源,能够接受和响应使用令牌访问保护资源的请求。

客户端:携带资源所有者的授权,向受保护资源发起请求。

授权服务器 :服务器在成功认证资源所有者和获得授权之后,向客户端发出访问令牌。

授权类型

OAuth 2为不同的用户提供了几种“授权类型”为:

授权码

密码

客户凭证

隐式

下面是密码授予的总体流程:

Spring boot使用Spring Security和OAuth2保护REST接口

二、应用

让我们考虑一下示例应用程序的数据库层和应用程序层

业务数据

我们的主要业务对象就是公司:

Spring boot使用Spring Security和OAuth2保护REST接口

基于公司和部门对象的CRUD操作,我们需要定义以下规则来进行访问:

COMPANY_CREATE

COMPANY_READ

COMPANY_UPDATE

COMPANY_DELETE

DEPARTMENT_CREATE

DEPARTMENT_READ

DEPARTMENT_UPDATE

DEPARTMENT_DELETE

除此之外,我们还需要创建ROLE_COMPANY_READER角色。

OAuth2客户端安装程序

我们需要在数据库中创建以下表(用于OAuth2实现的内部目的):

  • OAUTH_CLIENT_DETAILS
  • OAUTH_CLIENT_TOKEN
  • OAUTH_ACCESS_TOKEN
  • OAUTH_REFRESH_TOKEN
  • OAUTH_CODE
  • OAUTH_APPROVALS

假设我们要调用一个资源服务器,如“resource server rest api”。对于这个服务器,我们定义了两个名字:

spring-security-oauth2-read-client(授权类型:读取)

spring-security-oauth2-read-write-client(授权类型:读,写)

INSERT INTO OAUTH_CLIENT_DETAILS(CLIENT_ID, RESOURCE_IDS, CLIENT_SECRET, SCOPE, AUTHORIZED_GRANT_TYPES, AUTHORITIES, ACCESS_TOKEN_VALIDITY, REFRESH_TOKEN_VALIDITY)
VALUES ('spring-security-oauth2-read-client', 'resource-server-rest-api',
/*spring-security-oauth2-read-client-password1234*/'$2a$04$WGq2P9egiOYoOFemBRfsiO9qTcyJtNRnPKNBl5tokP7IP.eZn93km',
'read', 'password,authorization_code,refresh_token,implicit', 'USER', 10800, 2592000);
INSERT INTO OAUTH_CLIENT_DETAILS(CLIENT_ID, RESOURCE_IDS, CLIENT_SECRET, SCOPE, AUTHORIZED_GRANT_TYPES, AUTHORITIES, ACCESS_TOKEN_VALIDITY, REFRESH_TOKEN_VALIDITY)
VALUES ('spring-security-oauth2-read-write-client', 'resource-server-rest-api',
/*spring-security-oauth2-read-write-client-password1234*/'$2a$04$soeOR.QFmClXeFIrhJVLWOQxfHjsJLSpWrU1iGxcMGdu.a5hvfY4W',
'read,write', 'password,authorization_code,refresh_token,implicit', 'USER', 10800, 2592000);

权限和用户设置

Spring Security提供了两个有用的接口:

UserDetails – 提供核心用户信息。

GrantedAuthority – 表示授予Authentication对象的权限。

为了存储授权数据,我们需要定义以下数据模型:

Spring boot使用Spring Security和OAuth2保护REST接口

如果我们想要一些预先加载的数据,那么我们就要加载所有权限的脚本:

INSERT INTO AUTHORITY(ID, NAME) VALUES (1, 'COMPANY_CREATE');
INSERT INTO AUTHORITY(ID, NAME) VALUES (2, 'COMPANY_READ');
INSERT INTO AUTHORITY(ID, NAME) VALUES (3, 'COMPANY_UPDATE');
INSERT INTO AUTHORITY(ID, NAME) VALUES (4, 'COMPANY_DELETE');
INSERT INTO AUTHORITY(ID, NAME) VALUES (5, 'DEPARTMENT_CREATE');
INSERT INTO AUTHORITY(ID, NAME) VALUES (6, 'DEPARTMENT_READ');
INSERT INTO AUTHORITY(ID, NAME) VALUES (7, 'DEPARTMENT_UPDATE');
INSERT INTO AUTHORITY(ID, NAME) VALUES (8, 'DEPARTMENT_DELETE');

下面是加载所有用户和分配权限的脚本:

INSERT INTO USER_(ID, USER_NAME, PASSWORD, ACCOUNT_EXPIRED, ACCOUNT_LOCKED, CREDENTIALS_EXPIRED, ENABLED)
VALUES (1, 'admin', /*admin1234*/'$2a$08$qvrzQZ7jJ7oy2p/msL4M0.l83Cd0jNsX6AJUitbgRXGzge4j035ha', FALSE, FALSE, FALSE, TRUE);
INSERT INTO USER_(ID, USER_NAME, PASSWORD, ACCOUNT_EXPIRED, ACCOUNT_LOCKED, CREDENTIALS_EXPIRED, ENABLED)
VALUES (2, 'reader', /*reader1234*/'$2a$08$dwYz8O.qtUXboGosJFsS4u19LHKW7aCQ0LXXuNlRfjjGKwj5NfKSe', FALSE, FALSE, FALSE, TRUE);
INSERT INTO USER_(ID, USER_NAME, PASSWORD, ACCOUNT_EXPIRED, ACCOUNT_LOCKED, CREDENTIALS_EXPIRED, ENABLED)
VALUES (3, 'modifier', /*modifier1234*/'$2a$08$kPjzxewXRGNRiIuL4FtQH.mhMn7ZAFBYKB3ROz.J24IX8vDAcThsG', FALSE, FALSE, FALSE, TRUE);
INSERT INTO USER_(ID, USER_NAME, PASSWORD, ACCOUNT_EXPIRED, ACCOUNT_LOCKED, CREDENTIALS_EXPIRED, ENABLED)
VALUES (4, 'reader2', /*reader1234*/'$2a$08$vVXqh6S8TqfHMs1SlNTu/.J25iUCrpGBpyGExA.9yI.IlDRadR6Ea', FALSE, FALSE, FALSE, TRUE);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 1);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 2);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 3);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 4);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 5);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 6);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 7);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 8);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (1, 9);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (2, 2);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (2, 6);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (3, 3);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (3, 7);
INSERT INTO USERS_AUTHORITIES(USER_ID, AUTHORITY_ID) VALUES (4, 9);

要注意了,密码是使用BCrypt(8轮)进行散列的。

应用层

测试应用程序是否在Spring boot +Hibernate + Flyway中开发的,并带有一个公开的RESTAPI。 为了演示数据公司的运营,我们还要创建以下端点:

@RestController
@RequestMapping("/secured/company")
public class CompanyController {
    @Autowired
    private CompanyService companyService;
    @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public @ResponseBody List<Company> getAll() {
        return companyService.getAll();
    }
    @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public @ResponseBody Company get(@PathVariable Long id) {
        return companyService.get(id);
    }
    @RequestMapping(value = "/filter", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public @ResponseBody Company get(@RequestParam String name) {
        return companyService.get(name);
    }
    @RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public ResponseEntity<?> create(@RequestBody Company company) {
        companyService.create(company);
        HttpHeaders headers = new HttpHeaders();
        ControllerLinkBuilder linkBuilder = linkTo(methodOn(CompanyController.class).get(company.getId()));
        headers.setLocation(linkBuilder.toUri());
        return new ResponseEntity<>(headers, HttpStatus.CREATED);
    }
    @RequestMapping(method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public void update(@RequestBody Company company) {
        companyService.update(company);
    }
    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.OK)
    public void delete(@PathVariable Long id) {
        companyService.delete(id);
    }
}

三、PasswordEncoders

由于我们要为OAuth2客户端和用户使用不同的加密,因此我们需要为加密定义单独的密码编码器:

注意:OAuth2客户端密码 -BCrypt(4轮) 用户密码 – BCrypt(8轮)

@Configuration
public class Encoders {
    @Bean
    public PasswordEncoder oauthClientPasswordEncoder() {
        return new BCryptPasswordEncoder(4);
    }
    @Bean
    public PasswordEncoder userPasswordEncoder() {
        return new BCryptPasswordEncoder(8);
    }
}

Spring安全配置

如果我们想要从数据库中获取用户和权限,那么我们就要告诉Spring Security如何获取这些数据。所以,我们必须提供UserDetailsService接口来实现:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;
    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user != null) {
            return user;
        }
        throw new UsernameNotFoundException(username);
    }
}

我们需要使用JPA存储库创建UserRepository来分离服务和存储库层:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT DISTINCT user FROM User user " + "INNER JOIN FETCH user.authorities AS authorities "
            + "WHERE user.username = :username")
    User findByUsername(@Param("username") String username);
}

设置Spring Security

@EnableWebSecurity注释和WebSecurityConfigurerAdapter为应用程序提供安全性。@Order注释用来指定首先要考虑哪个WebSecurityConfigurerAdapter

@Configuration
@EnableWebSecurity
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
@Import(Encoders.class)
public class ServerSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder userPasswordEncoder;
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(userPasswordEncoder);
    }
}

四、OAuth2配置

首先,我们需要实现以下两个组件:

授权服务器和资源服务器

授权服务器

授权服务器:负责验证用户身份并提供令牌。

Spring boot使用Spring Security和OAuth2保护REST接口

SpringSecurity处理身份验证和SpringSecurity OAuth2处理授权。 要配置和启用OAuth 2.0授权服务器,我们必须使用@EnableAuthorizationServer注释。

@Configuration
@EnableAuthorizationServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Import(ServerSecurityConfig.class)
public class AuthServerOAuth2Config extends AuthorizationServerConfigurerAdapter {
    @Autowired
    @Qualifier("dataSource")
    private DataSource dataSource;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder oauthClientPasswordEncoder;
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }
    @Bean
    public OAuth2AccessDeniedHandler oauthAccessDeniedHandler() {
        return new OAuth2AccessDeniedHandler();
    }
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
        oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()")
                .passwordEncoder(oauthClientPasswordEncoder);
    }
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }
}

重点来了:

我们必须定义TokenStore bean,让Spring知道使用数据库要进行token操作。

覆盖configure方法以使用自定义UserDetailsService实现,AuthenticationManager bean和OAuth2客户端的密码编码器。

为了身份验证问题我们定义了处理程序bean。

通过重写configure(AuthorizationServerSecurityConfigureroauthServer)方法,我们启用了两个端点来检查令牌(/ oauth / check_token和/ oauth /token_key)。

资源服务器

Spring boot使用Spring Security和OAuth2保护REST接口

Spring OAuth2提供了一种认证过滤器。 @EnableResourceServer注释启用SpringSecurity过滤器,过滤器通过传入的OAuth2令牌对请求进行身份验证。

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    private static final String RESOURCE_ID = "resource-server-rest-api";
    private static final String SECURED_READ_SCOPE = "#oauth2.hasScope('read')";
    private static final String SECURED_WRITE_SCOPE = "#oauth2.hasScope('write')";
    private static final String SECURED_PATTERN = "/secured/**";
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID);
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.requestMatchers().antMatchers(SECURED_PATTERN).and().authorizeRequests()
                .antMatchers(HttpMethod.POST, SECURED_PATTERN).access(SECURED_WRITE_SCOPE).anyRequest()
                .access(SECURED_READ_SCOPE);
    }
}

configure(HttpSecurity http)方法使用HttpSecurity类配置访问并请求受保护资源的匹配者(路径)。我们需要保护URL路径/ secured /*。 必须注意的是,如果要调用任何POST方法请求,我们都需要“写入”范围。

那么让我们检查一下身份验证端点是否正常工作吧:

curl -X POST
http://localhost:8080/oauth/token
-H 'authorization: Basic c3ByaW5nLXNlY3VyaXR5LW9hdXRoMi1yZWFkLXdyaXRlLWNsaWVudDpzcHJpbmctc2VjdXJpdHktb2F1dGgyLXJlYWQtd3JpdGUtY2xpZW50LXBhc3N3b3JkMTIzNA=='
-F grant_type=password
-F username=admin
-F password=admin1234
-F client_id=spring-security-oauth2-read-write-client

以下是Postman的截图:

Spring boot使用Spring Security和OAuth2保护REST接口

Spring boot使用Spring Security和OAuth2保护REST接口

您应该得到类似于以下内容的响应:

{
"access_token": "e6631caa-bcf9-433c-8e54-3511fa55816d",
"token_type": "bearer",
"refresh_token": "015fb7cf-d09e-46ef-a686-54330229ba53",
"expires_in": 9472,
"scope": "read write"
}

五、访问规则配置

如果我们想要在服务层为CompanyDepartment对象提供安全接入,就必须使用@PreAuthorize注释。

@Servicepublic
class CompanyServiceImpl implements CompanyService {
    @Autowiredprivate
    CompanyRepository companyRepository;
    @Override
    @Transactional(readOnly = true)
    @PreAuthorize("hasAuthority('COMPANY_READ') and hasAuthority('DEPARTMENT_READ')")
    public Company get(Long id) {
        return companyRepository.find(id);
    }
    @Override
    @Transactional(readOnly = true)
    @PreAuthorize("hasAuthority('COMPANY_READ') and hasAuthority('DEPARTMENT_READ')")
    public Company get(String name) {
        return companyRepository.find(name);
    }
    @Override
    @Transactional(readOnly = true)
    @PreAuthorize("hasRole('COMPANY_READER')")
    public List<Company> getAll() {
        return companyRepository.findAll();
    }
    @Override
    @Transactional
    @PreAuthorize("hasAuthority('COMPANY_CREATE')")
    public void create(Company company) {
        companyRepository.create(company);
    }
    @Override
    @Transactional
    @PreAuthorize("hasAuthority('COMPANY_UPDATE')")
    public Company update(Company company) {
        return companyRepository.update(company);
    }
    @Override
    @Transactional
    @PreAuthorize("hasAuthority('COMPANY_DELETE')")
    public void delete(Long id) {
        companyRepository.delete(id);
    }
    @Override
    @Transactional
    @PreAuthorize("hasAuthority('COMPANY_DELETE')")
    public void delete(Company company) {
        companyRepository.delete(company);
    }
}

让我们来测试一下这个端点是否正常工作

curl -X GET
http://localhost:8080/secured/company/
-H 'authorization: Bearer e6631caa-bcf9-433c-8e54-3511fa55816d'

让我们看看如果我们用’spring-security-oauth2-read-client’授权会发生什么?这个客户端只定义了一个读取范围。

curl -X POST
http://localhost:8080/oauth/token
-H 'authorization: Basic c3ByaW5nLXNlY3VyaXR5LW9hdXRoMi1yZWFkLWNsaWVudDpzcHJpbmctc2VjdXJpdHktb2F1dGgyLXJlYWQtY2xpZW50LXBhc3N3b3JkMTIzNA=='
-F grant_type=password
-F username=admin
-F password=admin1234
-F client_id=spring-security-oauth2-read-client

紧接以下请求:

http://localhost:8080/secured/company
-H 'authorization: Bearer f789c758-81a0-4754-8a4d-cbf6eea69222'
-H 'content-type: application/json'
-d '{
"name": "TestCompany",
"departments": null,
"cars": null
}'

结果就是我们会接收到以下错误:

{
"error": "insufficient_scope",
"error_description": "Insufficient scope for this resource",
"scope": "write"
}

摘要

在这篇文章中,我们展示了使用Spring的OAuth2身份验证。通过在用户和权限之间建立直接连接,可以直接定义访问权限。为了增强此示例,我们可以添加一个额外的实体 – 角色 – 来改进访问权限的结构。

作者:架构师公社

来源:https://www.toutiao.com/a6719658101227651587/


版权声明:文末如注明作者和来源,则表示本文系转载,版权为原作者所有 | 本文如有侵权,请及时联系,承诺在收到消息后第一时间删除 | 如转载本文,请注明原文链接。
喜欢 (1)