在本文中,我将会演示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()public class CompanyController {private CompanyService companyService;publicList<Company> getAll() {return companyService.getAll();}publicCompany get( Long id) {return companyService.get(id);}publicCompany get( String name) {return companyService.get(name);}public ResponseEntity<?> create( 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);}public void update( Company company) {companyService.update(company);}public void delete( Long id) {companyService.delete(id);}}
PasswordEncoders
由于我们要为OAuth2客户端和用户使用不同的加密,因此我们需要为加密定义单独的密码编码器:
注意:OAuth2客户端密码 -BCrypt(4轮) 用户密码 - BCrypt(8轮)
@Configurationpublic class Encoders {public PasswordEncoder oauthClientPasswordEncoder() {return new BCryptPasswordEncoder(4);}public PasswordEncoder userPasswordEncoder() {return new BCryptPasswordEncoder(8);
Spring安全配置
如果我们想要从数据库中获取用户和权限,那么我们就要告诉Spring Security如何获取这些数据。所以,我们必须提供UserDetailsService接口来实现:
@Servicepublic class UserDetailsServiceImpl implements UserDetailsService {private UserRepository userRepository;(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> {("SELECT DISTINCT user FROM User user " +"INNER JOIN FETCH user.authorities AS authorities " +"WHERE user.username = :username")User findByUsername(("username") String username);}
设置Spring Security
@EnableWebSecurity注释和WebSecurityConfigurerAdapter为应用程序提供安全性。@Order注释用来指定首先要考虑哪个WebSecurityConfigurerAdapter。
@Configuration@EnableWebSecurity(SecurityProperties.ACCESS_OVERRIDE_ORDER)(Encoders.class)public class ServerSecurityConfig extends WebSecurityConfigurerAdapter {private UserDetailsService userDetailsService;private PasswordEncoder userPasswordEncoder;public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}protected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(userPasswordEncoder);}}
OAuth2配置
首先,我们需要实现以下两个组件:
授权服务器和资源服务器
授权服务器
授权服务器:负责验证用户身份并提供令牌。

SpringSecurity处理身份验证和SpringSecurity OAuth2处理授权。 要配置和启用OAuth 2.0授权服务器,我们必须使用@EnableAuthorizationServer注释。
@Configuration@EnableAuthorizationServer(prePostEnabled = true)(ServerSecurityConfig.class)public class AuthServerOAuth2Config extends AuthorizationServerConfigurerAdapter {("dataSource")private DataSource dataSource;private AuthenticationManager authenticationManager;private UserDetailsService userDetailsService;private PasswordEncoder oauthClientPasswordEncoder;public TokenStore tokenStore() {return new JdbcTokenStore(dataSource);}public OAuth2AccessDeniedHandler oauthAccessDeniedHandler() {return new OAuth2AccessDeniedHandler();}public void configure(AuthorizationServerSecurityConfigurer oauthServer) {oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()").passwordEncoder(oauthClientPasswordEncoder);}public void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.jdbc(dataSource);}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/**";public void configure(ResourceServerSecurityConfigurer resources) {resources.resourceId(RESOURCE_ID);}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 POSThttp://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"}
访问规则配置
如果我们想要在服务层为Company和Department对象提供安全接入,就必须使用@PreAuthorize注释。
@Servicepublic class CompanyServiceImpl implements CompanyService {private CompanyRepository companyRepository;public Company get(Long id) {return companyRepository.find(id);}public Company get(String name) {return companyRepository.find(name);}public List<Company> getAll() {return companyRepository.findAll();}public void create(Company company) {companyRepository.create(company);}public Company update(Company company) {return companyRepository.update(company);}public void delete(Long id) {companyRepository.delete(id);}public void delete(Company company) {companyRepository.delete(company);}}
让我们来测试一下这个端点是否正常工作:
curl -X GEThttp://localhost:8080/secured/company/-H 'authorization: Bearer e6631caa-bcf9-433c-8e54-3511fa55816d'
让我们看看如果我们用'spring-security-oauth2-read-client'授权会发生什么?这个客户端只定义了一个读取范围。
curl -X POSThttp://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身份验证。通过在用户和权限之间建立直接连接,可以直接定义访问权限。为了增强此示例,我们可以添加一个额外的实体 - 角色 - 来改进访问权限的结构。

长按二维码 ▲
订阅「架构师小秘圈」公众号
如有启发,帮我点个在看,谢谢↓
