用springdatarest和java8构建安全restapi

发布于:2021-01-19 10:11:45

0

102

0

angularjs gradle java java8 restapi springdatarest

restapi对于后端到后端的通信和非常流行的单页应用程序(spa)都是一个很好的接口。在techdev,我们构建了trackr,这是我们自己的工具,用于跟踪我们的工作时间、休假请求、差旅费、发票等等。

它是一个AngularJS应用程序,具有java8和spring4支持的后端。API通过OAuth2进行保护。如果您感兴趣,trackr是开源的,代码可以在这里(后端)和这里(前端)找到。

rubyonrails、Play等框架允许快速开发现代web应用程序。在本文中,我将展示如何使用trackr。

为什么是春天?

我时不时地读到一些关于Spring如何过于“进取”的言论,这些言论都是关于一些巨大的XML文件,AbstractSingletonProxyFactorys等等。以我的经验,这些都是假的。是的,这是一个框架,将迫使你的一些意见。但与此同时,它试图避开您的方式(注释与基类的继承)

此外,它还提供了应用程序中可能出现的几乎疯狂的功能,包括消息传递、调度或批处理。

第一步:使用Gradle的基本Spring引导应用程序

让我们从一个启动Spring应用程序上下文的非常基本的应用程序(根据需要的设置)开始。有两个工具可以帮助我:Gradle(我更喜欢Maven,因为它不那么冗长)和springboot。

springboot在很多帮助您编写Spring应用程序的任务中都是不可思议的。springboot为Maven提供了元包,捆绑了公共依赖项。这意味着您的依赖项部分不再与所有这些Spring依赖项混在一起。有一个插件负责构建一个可部署的JAR文件。最后但也许是最重要的一点,springboot在配置上有很多约定。我们会看到很多这样的例子。

在大多数代码示例中,我跳过了像maven存储库配置那样不太有趣的行。您可以在GitHub上找到完整的代码。

apply plugin: 'java'
apply plugin: 'spring-boot'
jar {
baseName = 'jaxenter-example'
version = '1.0'
}
dependencies {
compile("org.springframework.boot:spring-boot-starter")
compile("org.springframework.boot:spring-boot-starter-logging")
}

只需要两个依赖项(和一个Gradle插件)。spring引导插件还处理依赖项的版本!

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application implements CommandLineRunner {
private Logger logger = LoggerFactory.getLogger(Application.class);
@Autowired
private SomeService someService;
@Override
public void run(String... args) throws Exception {
String foo = someService.foo();
logger.info("SomeService returned {}", foo);
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

springboot的许多魔力来自@EnableAutoConfiguration注释。

@Service
public class SomeService {
private Logger logger = LoggerFactory.getLogger(SomeService.class);
public String foo() {
logger.debug("Foo has been called");
return "bar";
}
}

这个非常基本的示例已经可以用更多的功能进行扩展,例如,我可以向应用程序类添加@EnableScheduling注释,然后声明一个@Scheduled方法来定期运行某些任务。

compile("org.springframework.boot:spring-boot-starter-data-jpa")
runtime("org.hsqldb:hsqldb")
compile("org.projectlombok:lombok:1.14.8")

由于JPA中的实体仍然需要是javabean,而且我不想处理所有这些样板getter和setter,所以我添加了Lombok。这意味着其他开发者现在需要一个IDE插件,但我认为这是值得的。

然后添加第二个配置类(在第一步中不一定需要),告诉Spring Boot我需要Spring数据存储库。

@Configuration
@EnableJpaRepositories
public class PersistenceConfiguration extends JpaRepositoryConfigExtension {
// I added some code to put two persons into the database here.
}

由于我在我的应用程序中启用了组件扫描,它将自动被拾取。现在我为它添加一个实体和一个存储库。

@Entity
@Data
public class Person {
@Id
@GeneratedValue
private Long id;
private String firstName;
}
public interface PersonRepository extends JpaRepository{
ListfindByFirstNameLike(String firstName);
}

现在我可以访问一个数据库,其中包含一个包含人员的表,我可以通过他们的名字(以及springdatajpa提供的所有其他基本方法)来查询他们。

现在可能是最令人震惊的一步。我将再添加一个依赖项,更改存储库中的一行,然后我将能够。

  1. 通过HTTP创建、更新和删除人员

  2. 查询人员并完成分页

  3. 使用查找程序

所以,这里是变化。

compile("org.springframework.boot:spring-boot-starter-data-rest")

PersonRepository需要新注释:

ListfindByFirstNameLike(@Param("firstName") String firstName);

如果我启动这个应用程序,下面的cURL语句将起作用。

curl localhost:8080
curl localhost:8080/persons
curl -X POST -H "Content-Type: application/json" -d "{"firstName": "John"}"
localhost:8080/persons
curl localhost:8080/persons/search/findByFirstNameLike?firstName=J%
curl -X PUT localhost:8080/persons/1 -d "{"firstName": "Jane"}" -H "Content-Type:
application/json"
curl -X DELETE localhost:8080/persons/1

注意:我的实体模型在这里非常简单,实体之间没有任何关系,尽管Spring数据REST也可以处理这一点很简单。

简短回顾

我的IDE告诉我我有104行代码(包括导入代码——否则只有78行)。我们有一个功能齐全的restapi,可以很容易地添加更多的实体和存储库。我们拥有springwebmvc的全部功能,可以添加自定义控制器。Gradle仍然可以创建一个JAR文件,可以在不需要servlet容器的情况下进行部署。

虽然春季启动需要我们做很多工作,我们仍然难以置信的灵活。有时您只需向属性文件添加一些配置(例如,对于另一个非嵌入式数据源),或者扩展一些配置基类和覆盖方法。

第三步:使用springsecurity保护REST服务

我的restapi是完全公开的,我们来保护它,这样您就需要某种登录。您可能已经猜到了,我只是添加了另一个依赖项,SpringBoot将为我处理一个基本设置。

compile("org.springframework.boot:spring-boot-starter-security")

当我启动我的应用程序时,我现在会得到这样一个日志条目:

Using default security password: ed727172-deff-4789-8f79-e743e5342356

用户是user,整个restapi将受到保护。所以现在我可以用这样的卷曲。

curl user:ed727172-deff-4789-8f79-e743e5342356@localhost:8080/persons

当然,对于真正的安全性,这不是很有帮助。假设我需要多个用户和一些可以应用于某些方法的角色。例如,只有管理员才能列出所有人或搜索他们。

这将需要更多的参与,我不能期望springboot能够找出我们的用户所在的位置以及他们映射到的角色。首先,我添加了自己的安全配置:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private FakeUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().fullyAuthenticated();
http.httpBasic();
http.csrf().disable();
}
}

为了简单起见,在本文中我禁用了CSRF保护。FakeUserDetailsService是一个非常简单但不太灵活的实现,它实现了如何将用户名映射到现有人员。

@Service
public class FakeUserDetailsService implements UserDetailsService {
@Autowired
private PersonRepository personRepository;
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
Person person = personRepository.findByFirstNameEquals(username);
if (person == null) {
throw new UsernameNotFoundException("Username " + username + " not
found");
}
return new User(username, "password", getGrantedAuthorities(username));
}
private Collection getGrantedAuthorities(String
username) {
Collection authorities;
if (username.equals("John")) {
authorities = asList(() -> "ROLE_ADMIN", () -> "ROLE_BASIC");
} else {
authorities = asList(() -> "ROLE_BASIC");
}
return authorities;
}
}

在这里您可以看到我们第一次真正使用Java8:Lambdas。我发现它们在创建这样的模拟时非常有用。当然,你只省了几行,但我觉得值得。最后,我修改了Spring数据存储库来表示我们新的安全需求。

@Override
@PreAuthorize("hasRole('ROLE_ADMIN')")
PagefindAll(Pageable pageable);
@Override
@PostAuthorize("returnObject.firstName == principal.username or
hasRole('ROLE_ADMIN')")
Person findOne(Long aLong);
@PreAuthorize("hasRole('ROLE_ADMIN')")
ListfindByFirstNameLike(@Param("firstName") String firstName);

只有管理员可以查询所有人或按姓名搜索。如果我的用户名是一个被查询人的名字,我就可以访问这个对象。我们来试试:

% curl Mary:password@localhost:8080/persons/1
{"timestamp":1414951322459,"status":403,"error":"Forbidden","exception":"org.springfra
mework.security.access.AccessDeniedException","message":"Access is
denied","path":"/persons/1"}

整洁-我得到一个403如果我试图访问约翰与玛丽的帐户!

你可能已经注意到我只保护了GET请求。发布、放置、删除和修补呢?我可以重写存储库的save和delete方法并添加安全注释。对于POST和PUT这有一个缺点:我无法区分它们!我发现使用Spring数据REST事件处理程序是处理这些安全需求的一个好方法

@Component
@RepositoryEventHandler(Person.class)
public class PersonEventHandler {
@PreAuthorize("hasRole('ROLE_ADMIN')")
@HandleBeforeSave
public void checkPUTAuthority(Person person) {
// only security check
}
}

类似地,我可以为CREATE和DELETE添加安全检查。现在玛丽不能更新任何人!

第四步:添加OAuth

到目前为止,这是儿戏。该软件可以很容易地扩展与进一步的实体,网络公开的搜索等。每种方法都是安全的。但我想进一步推动restapi:多个客户机呢?OAuth2非常适合这种情况。所以让我们加上它。

OAuth通常有一个授权服务器和资源服务器。我的API是一个资源服务器。现在,我可以在同一个应用程序中添加身份验证服务器,但我不喜欢这种方法。我将使用不同的应用程序。这两个应用程序需要通信的唯一方式是通过授权令牌的共享数据库,我将使用SQLite。

OAuth授权服务器应用程序的依赖性较少。我省略了日志、Spring数据和Spring数据REST、HSQL和Lombok。

当然,我必须使用Spring-Security-OAuth。

配置非常简单:一个用于令牌的数据库和一些我在内存中定义的示例客户机。因为这不是一篇关于OAuth的文章,所以我不会解释像authorizedGrantTypes这样的东西是什么意思。

@Configuration
@EnableAuthorizationServer
public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws
Exception {
endpoints.tokenStore(tokenStore());
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("curl")
.authorities("ROLE_ADMIN")
.resourceIds("jaxenter")
.scopes("read", "write")
.authorizedGrantTypes("client_credentials")
.secret("password")
.and()
.withClient("web")
.redirectUris("http://github.com/techdev-solutions/")
.resourceIds("jaxenter")
.scopes("read")
.authorizedGrantTypes("implicit");
}
}

上面的配置用于分发令牌以访问资源服务器。例如,具有clientu凭证授权的客户机可以直接从/oauth/token端点获取令牌。

具有隐式授权的客户机将用户发送到/oauth/authorize页(下一步将对其进行保护),在该页中,用户可以授权客户机访问资源服务器上的数据。最酷的是,所有这些端点和所需的网页都在SpringSecurityOAuth中包含的默认版本中!

让我们添加一个安全配置,以便以前的人员可以登录:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("John").roles("ADMIN").password("password")
.and()
.withUser("Mary").roles("BASIC").password("password");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").authenticated()
.and().httpBasic().realmName("OAuth Server");
}
}

授权服务器现在已完成。我还需要让我们的restapi应用程序知道,它现在是一个资源服务器,使用与授权服务器相同的令牌数据库。

@Configuration
@EnableResourceServer
public class OAuthConfiguration extends ResourceServerConfigurerAdapter {
@Value("${oauth_db}")
private String oauthDbJdbc;
@Bean
public TokenStore tokenStore() {
DataSource tokenDataSource =
DataSourceBuilder.create().driverClassName("org.sqlite.JDBC").url(oauthDbJdbc).build()
;
return new JdbcTokenStore(tokenDataSource);
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception
{
resources.resourceId("jaxenter")
.tokenStore(tokenStore());

正如您所看到的,这个配置文件现在接管了HttpSecurity的配置。我使用OAuth的另一个特性scopes,这样就可以创建只读客户机。

旧的安全配置几乎可以完全丢弃,我只保留它作为方法安全配置。

哇,那真是太多工作了。让我们看看它是否管用。两个应用程序现在都必须启动。我配置了授权服务器,使其在端口8081上运行,并在必要时初始化令牌数据库。当授权服务器运行时,我可以使用以下使用基本身份验证的请求请求令牌。

curl curl:password@localhost:8081/oauth/token?grant_type=client_credentials

作为回应,我将得到一个令牌,我可以这样使用。

curl -H "Authorization: Bearer $token" localhost:8080

我给cURL客户机分配了admin角色和读写范围,所以一切都可以执行。

接下来是web客户端。我访问URLhttp://localhost:8081/oauth/authorize?浏览器中的clientu id=web&responseu type=token。现在我以John的身份登录并进入授权页面。如果我有一个实际的web客户端,我会配置返回URL,这样我就可以回到那里。

因为我没有,所以我使用了我公司的GitHub页面,但是令牌将包含在重定向URL中,我可以手动提取它以在cURL中使用它。这次令牌没有写作用域,如果我以Mary身份登录,我就不会是管理员。当对相应的请求使用令牌时,这两种方法都能正常工作!

我不得不添加很多东西(甚至是第二个应用程序!),但您可能已经注意到我几乎没有触及restapi。事实上,我只增加了一个配置,减少了另一个。