發(fā)布于:2021-01-19 10:11:45
0
116
0
restapi對于后端到后端的通信和非常流行的單頁應(yīng)用程序(spa)都是一個很好的接口。在techdev,我們構(gòu)建了trackr,這是我們自己的工具,用于跟蹤我們的工作時間、休假請求、差旅費、發(fā)票等等。
它是一個AngularJS應(yīng)用程序,具有java8和spring4支持的后端。API通過OAuth2進行保護。如果您感興趣,trackr是開源的,代碼可以在這里(后端)和這里(前端)找到。
rubyonrails、Play等框架允許快速開發(fā)現(xiàn)代web應(yīng)用程序。在本文中,我將展示如何使用trackr。
我時不時地讀到一些關(guān)于Spring如何過于“進取”的言論,這些言論都是關(guān)于一些巨大的XML文件,AbstractSingletonProxyFactorys
等等。以我的經(jīng)驗,這些都是假的。是的,這是一個框架,將迫使你的一些意見。但與此同時,它試圖避開您的方式(注釋與基類的繼承)
此外,它還提供了應(yīng)用程序中可能出現(xiàn)的幾乎瘋狂的功能,包括消息傳遞、調(diào)度或批處理。
讓我們從一個啟動Spring應(yīng)用程序上下文的非?;镜膽?yīng)用程序(根據(jù)需要的設(shè)置)開始。有兩個工具可以幫助我:Gradle(我更喜歡Maven,因為它不那么冗長)和springboot。
springboot在很多幫助您編寫Spring應(yīng)用程序的任務(wù)中都是不可思議的。springboot為Maven提供了元包,捆綁了公共依賴項。這意味著您的依賴項部分不再與所有這些Spring依賴項混在一起。有一個插件負責構(gòu)建一個可部署的JAR文件。最后但也許是最重要的一點,springboot在配置上有很多約定。我們會看到很多這樣的例子。
在大多數(shù)代碼示例中,我跳過了像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引導(dǎo)插件還處理依賴項的版本!
@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";
}
}
這個非常基本的示例已經(jīng)可以用更多的功能進行擴展,例如,我可以向應(yīng)用程序類添加@EnableScheduling注釋,然后聲明一個@Scheduled方法來定期運行某些任務(wù)。
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。這意味著其他開發(fā)者現(xiàn)在需要一個IDE插件,但我認為這是值得的。
然后添加第二個配置類(在第一步中不一定需要),告訴Spring Boot我需要Spring數(shù)據(jù)存儲庫。
@Configuration
@EnableJpaRepositories
public class PersistenceConfiguration extends JpaRepositoryConfigExtension {
// I added some code to put two persons into the database here.
}
由于我在我的應(yīng)用程序中啟用了組件掃描,它將自動被拾取?,F(xiàn)在我為它添加一個實體和一個存儲庫。
@Entity
@Data
public class Person {
@Id
@GeneratedValue
private Long id;
private String firstName;
}
public interface PersonRepository extends JpaRepository{
ListfindByFirstNameLike(String firstName);
}
現(xiàn)在我可以訪問一個數(shù)據(jù)庫,其中包含一個包含人員的表,我可以通過他們的名字(以及springdatajpa提供的所有其他基本方法)來查詢他們。
現(xiàn)在可能是最令人震驚的一步。我將再添加一個依賴項,更改存儲庫中的一行,然后我將能夠。
通過HTTP創(chuàng)建、更新和刪除人員
查詢?nèi)藛T并完成分頁
使用查找程序
所以,這里是變化。
compile("org.springframework.boot:spring-boot-starter-data-rest")
PersonRepository
需要新注釋:
ListfindByFirstNameLike(@Param("firstName") String firstName);
如果我啟動這個應(yīng)用程序,下面的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
注意:我的實體模型在這里非常簡單,實體之間沒有任何關(guān)系,盡管Spring數(shù)據(jù)REST也可以處理這一點很簡單。
我的IDE告訴我我有104行代碼(包括導(dǎo)入代碼——否則只有78行)。我們有一個功能齊全的restapi,可以很容易地添加更多的實體和存儲庫。我們擁有springwebmvc的全部功能,可以添加自定義控制器。Gradle仍然可以創(chuàng)建一個JAR文件,可以在不需要servlet容器的情況下進行部署。
雖然春季啟動需要我們做很多工作,我們?nèi)匀浑y以置信的靈活。有時您只需向?qū)傩晕募砑右恍┡渲茫ɡ?,對于另一個非嵌入式數(shù)據(jù)源),或者擴展一些配置基類和覆蓋方法。
我的restapi是完全公開的,我們來保護它,這樣您就需要某種登錄。您可能已經(jīng)猜到了,我只是添加了另一個依賴項,SpringBoot將為我處理一個基本設(shè)置。
compile("org.springframework.boot:spring-boot-starter-security")
當我啟動我的應(yīng)用程序時,我現(xiàn)在會得到這樣一個日志條目:
Using default security password: ed727172-deff-4789-8f79-e743e5342356
用戶是user,整個restapi將受到保護。所以現(xiàn)在我可以用這樣的卷曲。
curl user:ed727172-deff-4789-8f79-e743e5342356@localhost:8080/persons
當然,對于真正的安全性,這不是很有幫助。假設(shè)我需要多個用戶和一些可以應(yīng)用于某些方法的角色。例如,只有管理員才能列出所有人或搜索他們。
這將需要更多的參與,我不能期望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是一個非常簡單但不太靈活的實現(xiàn),它實現(xiàn)了如何將用戶名映射到現(xiàn)有人員。
@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。我發(fā)現(xiàn)它們在創(chuàng)建這樣的模擬時非常有用。當然,你只省了幾行,但我覺得值得。最后,我修改了Spring數(shù)據(jù)存儲庫來表示我們新的安全需求。
@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);
只有管理員可以查詢所有人或按姓名搜索。如果我的用戶名是一個被查詢?nèi)说拿?,我就可以訪問這個對象。我們來試試:
% 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如果我試圖訪問約翰與瑪麗的帳戶!
你可能已經(jīng)注意到我只保護了GET請求。發(fā)布、放置、刪除和修補呢?我可以重寫存儲庫的save和delete方法并添加安全注釋。對于POST和PUT這有一個缺點:我無法區(qū)分它們!我發(fā)現(xiàn)使用Spring數(shù)據(jù)REST事件處理程序是處理這些安全需求的一個好方法
@Component
@RepositoryEventHandler(Person.class)
public class PersonEventHandler {
@PreAuthorize("hasRole('ROLE_ADMIN')")
@HandleBeforeSave
public void checkPUTAuthority(Person person) {
// only security check
}
}
類似地,我可以為CREATE和DELETE添加安全檢查?,F(xiàn)在瑪麗不能更新任何人!
到目前為止,這是兒戲。該軟件可以很容易地擴展與進一步的實體,網(wǎng)絡(luò)公開的搜索等。每種方法都是安全的。但我想進一步推動restapi:多個客戶機呢?OAuth2非常適合這種情況。所以讓我們加上它。
OAuth通常有一個授權(quán)服務(wù)器和資源服務(wù)器。我的API是一個資源服務(wù)器?,F(xiàn)在,我可以在同一個應(yīng)用程序中添加身份驗證服務(wù)器,但我不喜歡這種方法。我將使用不同的應(yīng)用程序。這兩個應(yīng)用程序需要通信的唯一方式是通過授權(quán)令牌的共享數(shù)據(jù)庫,我將使用SQLite。
OAuth授權(quán)服務(wù)器應(yīng)用程序的依賴性較少。我省略了日志、Spring數(shù)據(jù)和Spring數(shù)據(jù)REST、HSQL和Lombok。
當然,我必須使用Spring-Security-OAuth。
配置非常簡單:一個用于令牌的數(shù)據(jù)庫和一些我在內(nèi)存中定義的示例客戶機。因為這不是一篇關(guān)于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");
}
}
上面的配置用于分發(fā)令牌以訪問資源服務(wù)器。例如,具有clientu憑證授權(quán)的客戶機可以直接從/oauth/token端點獲取令牌。
具有隱式授權(quán)的客戶機將用戶發(fā)送到/oauth/authorize頁(下一步將對其進行保護),在該頁中,用戶可以授權(quán)客戶機訪問資源服務(wù)器上的數(shù)據(jù)。最酷的是,所有這些端點和所需的網(wǎng)頁都在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");
}
}
授權(quán)服務(wù)器現(xiàn)在已完成。我還需要讓我們的restapi應(yīng)用程序知道,它現(xiàn)在是一個資源服務(wù)器,使用與授權(quán)服務(wù)器相同的令牌數(shù)據(jù)庫。
@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());
正如您所看到的,這個配置文件現(xiàn)在接管了HttpSecurity的配置。我使用OAuth的另一個特性scopes,這樣就可以創(chuàng)建只讀客戶機。
舊的安全配置幾乎可以完全丟棄,我只保留它作為方法安全配置。
哇,那真是太多工作了。讓我們看看它是否管用。兩個應(yīng)用程序現(xiàn)在都必須啟動。我配置了授權(quán)服務(wù)器,使其在端口8081上運行,并在必要時初始化令牌數(shù)據(jù)庫。當授權(quán)服務(wù)器運行時,我可以使用以下使用基本身份驗證的請求請求令牌。
curl curl:password@localhost:8081/oauth/token?grant_type=client_credentials
作為回應(yīng),我將得到一個令牌,我可以這樣使用。
curl -H "Authorization: Bearer $token" localhost:8080
我給cURL客戶機分配了admin角色和讀寫范圍,所以一切都可以執(zhí)行。
接下來是web客戶端。我訪問URLhttp://localhost:8081/oauth/authorize?瀏覽器中的clientu id=web&responseu type=token?,F(xiàn)在我以John的身份登錄并進入授權(quán)頁面。如果我有一個實際的web客戶端,我會配置返回URL,這樣我就可以回到那里。
因為我沒有,所以我使用了我公司的GitHub頁面,但是令牌將包含在重定向URL中,我可以手動提取它以在cURL中使用它。這次令牌沒有寫作用域,如果我以Mary身份登錄,我就不會是管理員。當對相應(yīng)的請求使用令牌時,這兩種方法都能正常工作!
我不得不添加很多東西(甚至是第二個應(yīng)用程序!),但您可能已經(jīng)注意到我?guī)缀鯖]有觸及restapi。事實上,我只增加了一個配置,減少了另一個。