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

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

配置Spring Security +数据库

创建授权服务器

创建资源服务器

获取访问令牌和刷新令牌

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


引言


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


OAuth角色

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

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

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

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

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


授权类型

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

授权码
密码
客户凭证
隐式


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





应用


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


业务数据

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



基于公司和部门对象的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对象的权限。

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



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

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轮)

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


Spring安全配置

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

@Servicepublic 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来分离服务和存储库层

@Repositorypublic 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配置


首先,我们需要实现以下两个组件:
授权服务器和资源服务器


授权服务器

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





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 OAuth2提供了一种认证过滤器。 @EnableResourceServer注释启用SpringSecurity过滤器,过滤器通过传入的OAuth2令牌对请求进行身份验证。

@Configuration@EnableResourceServerpublic 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的截图:





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


{    "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 {    @Autowired    private 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"}
摘要

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

长按二维码 ▲

订阅「架构师小秘圈」公众号

如有启发,帮我点个在看,谢谢↓