使用Spring Data REST和Java 8构建安全的REST API

REST API是后端到后端通信以及非常流行的单页应用程序(SPA)的绝佳接口。 在techdev,我们构建了tracker ,这是我们自己的工具,可以跟踪我们的工作时间,休假请求,差旅费用,发票等。

这是一个具有Java 8和Spring 4后端的AngularJS应用程序。 该API通过OAuth2保护。 如果您有兴趣, tracker是开源的,可以在此处(后端)此处(前端)获得代码

还请参见: 如何从REST导出数据

诸如Ruby On Rails, Play等框架允许快速开发现代Web应用程序。 在本文中,我将展示我如何使用tracker

为什么是春天?

我时不时地阅读有关Spring如何过分“企业化”的陈述,有关所有巨型XML文件, AbstractSingletonProxyFactorys等的陈述。 根据我的经验,这些都是错误的。 是的,这是一个框架,将迫使您提出一些意见。 但同时,它会尽量避免使用(注释与基类的继承)。

另外,它提供了可能会在您的应用程序中出现的几乎所有功能,包括消息传递,计划或批处理。

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

Spring Boot在许多帮助您编写Spring应用程序的任务上都令人难以置信。 Spring Boot为Maven提供元数据包,以捆绑常见的依赖项。 这意味着您的依赖项部分不再被所有那些Spring依赖项所困扰。 有一个插件负责构建可部署的JAR文件。 最后但也许最重要的是,Spring Boot在配置方面有很多约定。 我们将看到很多例子。

在大多数代码示例中,我会跳过一些不太有趣的行,例如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-boot插件还可以处理依赖项的版本!

@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);
}
}

Spring Boot的许多魔力来自@EnableAutoConfiguration批注。

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

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

当我使用Gradle的构建任务时,我将获得一个独立的JAR文件,该文件将启动并运行完整的Spring应用程序。 使用Java作为微服务,可以轻松地将JAR文件部署在Docker容器中! 如果它实际上会提供某种服务。

现在,我已经有了Spring应用程序的基础,是时候添加持久性,然后再添加一个通过HTTP公开实体的REST服务了。 此步骤将仅添加三个依赖项!

当我将HSQL添加到类路径时,Spring Boot自动配置将自动选择它并从中创建主数据源。 从我的角度来看,绝对不需要任何配置,我可以立即从存储库开始并使用代码。

要将Spring Data存储库添加到我的应用程序中,只需将其添加到build.gradle中的依赖项中:

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

由于JPA中的实体仍然需要是Java Bean,并且我不想处理所有这些样板化的getter和setter方法,因此我添加了Lombok 。 这意味着其他开发人员现在需要一个IDE插件,但我认为这是值得的。

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

@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<Person, Long> {
List<Person> findByFirstNameLike(String firstName);
}

这样一来,我现在可以访问包含有人员表的数据库,并且可以按其名字(以及Spring Data JPA提供的所有其他基本方法)查询他们。

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

  1. 通过HTTP创建,更新和删除人员
  2. 查询分页完整的人
  3. 使用发现者

所以,这是变化

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

PersonRepository需要一个新的注释:

List<Person> findByFirstNameLike(@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%25
curl -X PUT localhost:8080/persons/1 -d "{\"firstName\": \"Jane\"}" -H "Content-Type:
application/json"
curl -X DELETE localhost:8080/persons/1

注意:这里的实体模型真的很简单,实体之间没有任何关系,尽管Spring Data REST也很容易处理。

简短回顾

我的IDE告诉我,我有104行代码(包括导入-否则只有78行)。 我们拥有功能齐全的REST API,可以非常轻松地添加更多实体和存储库。 我们拥有唾手可得的Spring Web MVC的全部功能,可以添加自定义控制器。 Gradle仍可以创建单个JAR文件,而无需servlet容器即可对其进行部署。

尽管Spring Boot承担了我们很多工作,但我们仍然非常灵活。 有时,您只需要向属性文件中添加一些配置(例如,用于另一个非嵌入式数据源),或扩展一些配置基类和覆盖方法。

第三步:使用Spring Security保护REST服务

我的REST API是完全公开的,让我们对其进行保护,以便您需要某种登录。 您可能已经猜到了,我只是添加了另一个依赖关系,Spring Boot将为我完成基本的设置。

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

当我启动我的应用程序时,现在将获得如下日志条目:

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

用户就是用户,整个REST API将得到保护。 所以现在我可以像这样使用cURL

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

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

这将需要更多的参与,我不能期望Spring Boot弄清用户的位置以及他们映射到的角色。 首先,我添加自己的安全性配置:

@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保护。 FakeUserDetailsS​​ervice是如何将用户名映射到我们现有人员的非常简单且不太灵活的实现。

@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<? extends GrantedAuthority> getGrantedAuthorities(String
username) {
Collection<? extends GrantedAuthority> authorities;
if (username.equals("John")) {
authorities = asList(() -> "ROLE_ADMIN", () -> "ROLE_BASIC");
} else {
authorities = asList(() -> "ROLE_BASIC");
}
return authorities;
}
}

在这里,您可以看到我们第一次真正使用Java 8:Lambda。 在创建这样的模拟程序时,我发现它们确实很有帮助。 当然,您只保存几行,但是我认为这是有偿的。 最后,我更改Spring Data存储库以代表我们的新安全要求。

@Override
@PreAuthorize("hasRole('ROLE_ADMIN')")
Page<Person> findAll(Pageable pageable);
@Override
@PostAuthorize("returnObject.firstName == principal.username or
hasRole('ROLE_ADMIN')")
Person findOne(Long aLong);
@PreAuthorize("hasRole('ROLE_ADMIN')")
List<Person> findByFirstNameLike(@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"}

整洁–如果尝试使用Mary的帐户访问John ,我会得到403!

(您可能会注意到“默认安全密码”日志条目仍然存在,但不再起作用。)

您可能已经注意到我只保护了GET请求。 POST,PUT,DELETE和PATCH呢? 我可以覆盖存储库的save和delete方法并添加安全注释。 对于POST和PUT,这有一个缺点:我无法区分它们! 我发现使用Spring Data REST事件处理程序是解决这些安全要求的好方法。

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

同样,我可以为CREATE和DELETE添加安全检查。 现在,玛丽无法更新任何人!

第四步:添加OAuth

到现在为止,这只是儿童的游戏。 该软件可以轻松地通过其他实体,暴露于网络的查找器等进行扩展。 每种方法都可以保证。 但是我想进一步推动REST API:多个客户端呢? OAuth2非常适合于此 。 因此,让我们添加它。

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

OAuth授权服务器应用程序具有较少的依赖性。 我省略了日志记录,Spring Data和Spring Data 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");
}
}

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

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

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

@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");
}
}

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

@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的另一个功能,范围,这样我就可以创建只读客户端。

旧的安全配置几乎可以完全丢弃,我仅将其保留用于方法安全配置。

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

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

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

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

我为cURL客户端赋予了管理员角色和读写作用域,因此可以执行所有操作。

接下来,是Web客户端。 我在浏览器中访问URL http:// localhost:8081 / oauth / authorize?client_id = web&response_type = token。 现在,我以John身份登录并进入授权页面。 如果我有一个实际的Web客户端,我将配置返回URL,以便将我带回到那里。

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

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

翻译自: https://jaxenter.com/rest-api-spring-java-8-112289.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值