- 原文地址:Protecting a Spring Boot App With Apache Shiro
- 原文做者:Brian Demers
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:lihanxiang
- 校對者:Mirosalva、HearFishle
對於 Apache Shiro,我最欣賞的一點是它可以輕易地處理應用的受權行爲。你可以使用基於角色的訪問控制模型來對用戶進行角色分配,以及對角色進行權限分配。這使得處理一些不可避免的行爲變得簡單。你不須要改動代碼,只需修改角色權限。在這篇文章中,我想展現它的易用性,用一個 Spring Boot 程序來介紹我是如何處理如下場景的:html
你的老大(最高指揮官)出如今你的桌旁並告訴你,當前的志願者(士兵)註冊應用須要針對不一樣的員工類別分配不一樣的權限。前端
首先,來看看這個 Spring Boot 的例子。它會幫助你從一些進行 CRUD 操做的 REST 接入點來管理一個士兵名單。你將用 Apache Shiro 來添加身份驗證和角色受權。全部代碼已上傳至 Github。java
要使用 Apache Shiro, 你所須要作的就是使用 Spring Boot 的 starter,只要在 pom 文件里加入你所須要的依賴(${shiro.version}
至少須要在 1.4.0 之上):android
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>${shiro.version}</version>
</dependency>
複製代碼
接下來看看代碼,從 StormtrooperController
開始,只須要添加一些註解:ios
@RestController
@RequestMapping(path = "/troopers", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class StormtrooperController {
private final StormtrooperDao trooperDao;
@Autowired
public StormtrooperController(StormtrooperDao trooperDao) {
this.trooperDao = trooperDao;
}
@GetMapping()
@RequiresRoles(logical = Logical.OR, value = {"admin", "officer", "underling"})
public Collection<Stormtrooper> listTroopers() {
return trooperDao.listStormtroopers();
}
@GetMapping(path = "/{id}")
@RequiresRoles(logical = Logical.OR, value = {"admin", "officer", "underling"})
public Stormtrooper getTrooper(@PathVariable("id") String id) throws NotFoundException {
Stormtrooper stormtrooper = trooperDao.getStormtrooper(id);
if (stormtrooper == null) {
throw new NotFoundException(id);
}
return stormtrooper;
}
@PostMapping()
@RequiresRoles(logical = Logical.OR, value = {"admin", "officer"})
public Stormtrooper createTrooper(@RequestBody Stormtrooper trooper) {
return trooperDao.addStormtrooper(trooper);
}
@PostMapping(path = "/{id}")
@RequiresRoles("admin")
public Stormtrooper updateTrooper(@PathVariable("id") String id, @RequestBody Stormtrooper updatedTrooper) throws NotFoundException {
return trooperDao.updateStormtrooper(id, updatedTrooper);
}
@DeleteMapping(path = "/{id}")
@ResponseStatus(value = HttpStatus.NO_CONTENT)
@RequiresRoles("admin")
public void deleteTrooper(@PathVariable("id") String id) {
trooperDao.deleteStormtrooper(id);
}
}
複製代碼
在以上的代碼塊中,使用 Shiro 的 @RequiresRoles
註釋來指定角色。你會看到用邏輯符 OR
來爲任何擁有這種角色的人賦予權限。這很棒,只須要添加一行註解,你的代碼就已經完成了。git
你的代碼能夠到此爲止,可是,使用角色的方式並非那麼靈活,若是直接在代碼中使用,就會致使代碼與這些名字的緊密耦合。github
想象一下,你的應用已被部署,而且正常工做了,過了一星期,你的老大來到桌旁,叫你作一些改動:web
好,你以爲這個並不難,只須要對方法簽名作一點小改動:spring
@GetMapping()
@RequiresRoles(logical = Logical.OR, value = {"emperor", "admin", "emperor", "officer", "underling"})
public Collection<Stormtrooper> listTroopers() @GetMapping(path = "/{id}") @RequiresRoles(logical = Logical.OR, value = {"emperor", "admin", "officer", "underling"}) public Stormtrooper getTrooper(@PathVariable("id") String id) throws NotFoundException @PostMapping() @RequiresRoles(logical = Logical.OR, value = {"emperor", "admin", "officer"}) public Stormtrooper createTrooper(@RequestBody Stormtrooper trooper) @PostMapping(path = "/{id}") @RequiresRoles(logical = Logical.OR, value = {"emperor", "admin", "officer"}) public Stormtrooper updateTrooper(@PathVariable("id") String id, @RequestBody Stormtrooper updatedTrooper) throws NotFoundException @DeleteMapping(path = "/{id}") @ResponseStatus(value = HttpStatus.NO_CONTENT) @RequiresRoles(logical = Logical.OR, value = {"emperor", "admin"}) public void deleteTrooper(@PathVariable("id") String id) 複製代碼
在又一輪的測試與部署以後,你的工做完成了!數據庫
等等,往回退一步,在簡單的用例中,角色可以起到很棒的做用,這種類型的變動也運行良好,然而你知道代碼還有下次改動。與其每次都由於一些小需求而修改代碼,還不如將角色從代碼中分離。替換的方式是改用賦予權限。你的方法簽名將會變成這樣:
@GetMapping()
@RequiresPermissions("troopers:read")
public Collection<Stormtrooper> listTroopers() @GetMapping(path = "/{id}") @RequiresPermissions("troopers:read") public Stormtrooper getTrooper(@PathVariable("id") String id) throws NotFoundException @PostMapping() @RequiresPermissions("troopers:create") public Stormtrooper createTrooper(@RequestBody Stormtrooper trooper) @PostMapping(path = "/{id}") @RequiresPermissions("troopers:update") public Stormtrooper updateTrooper(@PathVariable("id") String id, @RequestBody Stormtrooper updatedTrooper) throws NotFoundException @DeleteMapping(path = "/{id}") @ResponseStatus(value = HttpStatus.NO_CONTENT) @RequiresPermissions("troopers:delete") public void deleteTrooper(@PathVariable("id") String id) 複製代碼
經過使用 Shiro 的 @RequiresPermissions
註解,就可以在不進行代碼修改的同時知足原始需求和新需求。惟一要作的就是將權限映射到對應的角色,也就是咱們的用戶。這件事可以在外部程序中完成,好比數據庫,或者像本例中一個簡單的配置文件。
值得注意的是: 在這個例子中,用戶名和密碼都是明文存儲的,這對於博客的文章來講沒什麼問題,可是,嚴格來講,你須要正確地管理你的密碼!
爲了實現原來的需求,角色-權限的映射是這樣的:
role.admin = troopers:*
role.officer = troopers:create, troopers:read
role.underling = troopers:read
複製代碼
對於後續的需求,只須要在文件中加入 『emperor』 角色,以及給長官們添加 「update」 權限:
role.emperor = *
role.admin = troopers:*
role.officer = troopers:create,troopers:read,troopers:update
role.underling = troopers:read
複製代碼
若是你以爲這受權語句的語法看上去有點奇怪,能夠從 Apache Shiro 的通配符受權 文檔中來得到一些深刻的瞭解。
咱們已經介紹了 Maven 依賴和 REST 控制器,但咱們的應用還須要一個 Realm
和異常處理機制。
若是你看過 SpringBootApp
類,你就會注意到有一些不在樣例中的東西。
@Bean
public Realm realm() {
// uses 'classpath:shiro-users.properties' by default
PropertiesRealm realm = new PropertiesRealm();
// Caching isn't needed in this example, but we can still turn it on
realm.setCachingEnabled(true);
return realm;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
// use permissive to NOT require authentication, our controller Annotations will decide that
chainDefinition.addPathDefinition("/**", "authcBasic[permissive]");
return chainDefinition;
}
@Bean
public CacheManager cacheManager() {
// Caching isn't needed in this example, but we will use the MemoryConstrainedCacheManager for this example.
return new MemoryConstrainedCacheManager();
}
複製代碼
首先,你先定義一個 Shiro 的 Realm
。realm 只是一個特定的存儲用戶的 DAO,Shiro 支持多種不一樣類型的 Realm (活動目錄、LDAP、數據庫和文件等等)。
接下來看看 ShiroFilterChainDefinition
,你配置了容許基本的身份驗證功能,可是並非經過『permissive』選項來獲取這個功能。這樣你的註釋就能夠配置全部內容了。你可使用 Ant 樣式的路徑來定義 URL 映射權限,而不是使用註解(或者使用一些其餘的)。這個例子看起來是這樣子的:
chainDefinition.addPathDefinition("/troopers/**", "authcBasic, rest[troopers]");
複製代碼
這樣作將全部以 /troopers
開頭的資源映射到要求基自己份驗證,而且使用 ‘rest’ 過濾器,它基於 HTTP 請求方法,且在權限字符串後附加了一個 CRUD 操做。舉個例子,一個 HTTPGET
方法會映射到 ‘read’,因此對於一個 GET
請求的完整權限字符串爲troopers:read
(就像你用註解作的那樣)。
代碼中的最後一部分就是異常處理了
@ExceptionHandler(UnauthenticatedException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public void handleException(UnauthenticatedException e) {
log.debug("{} was thrown", e.getClass(), e);
}
@ExceptionHandler(AuthorizationException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public void handleException(AuthorizationException e) {
log.debug("{} was thrown", e.getClass(), e);
}
@ExceptionHandler(NotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public @ResponseBody ErrorMessage handleException(NotFoundException e) {
String id = e.getMessage();
return new ErrorMessage("Trooper Not Found: "+ id +", why aren't you at your post? "+ id +", do you copy?");
}
複製代碼
前兩個處理 Shiro 異常的例子,只是簡單的將狀態碼改至 401 或 403。401 針對的是用戶名/密碼的無效或缺失,403 是由於已登陸的用戶無權訪問受限資源。最後,你將要用 404 來處理 NotFoundException
,而且返回一個 JSON 序列化的 ErrorMessage
對象。
若是你把這些組合起來,或者你直接從 GitHub上把代碼搬過來,你就能用 mvn spring-boot:run
來啓動應用。一旦運行起來,你就可以開始發送請求了!
$ curl http://localhost:8080/troopers
HTTP/1.1 401
Content-Length: 0
Date: Thu, 26 Jan 2017 21:12:41 GMT
WWW-Authenticate: BASIC realm="application"
複製代碼
別忘了,你須要驗證你的身份!
$ curl --user emperor:secret http://localhost:8080/troopers
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Thu, 26 Jan 2017 21:14:17 GMT
Transfer-Encoding: chunked
[
{
"id": "FN-0128",
"planetOfOrigin": "Naboo",
"species": "Twi'lek",
"type": "Sand"
},
{
"id": "FN-1383",
"planetOfOrigin": "Hoth",
"species": "Human",
"type": "Basic"
},
{
"id": "FN-1692",
"planetOfOrigin": "Hoth",
"species": "Nikto",
"type": "Marine"
},
...
複製代碼
一個 404
是這樣的:
$ curl --user emperor:secret http://localhost:8080/troopers/TK-421
HTTP/1.1 404
Content-Type: application/json;charset=UTF-8
Date: Thu, 26 Jan 2017 21:15:54 GMT
Transfer-Encoding: chunked
{
"error": "Trooper Not Found: TK-421, why aren't you at your post? TK-421, do you copy?"
}
複製代碼
這個例子演示瞭如何輕鬆將 Apache Shiro 集成至 Spring Boot 應用,以及如何使用權限來增大角色的靈活性,全部的這些只須要在控制器中加一條註解。
咱們很高興可以爲 Apache Shiro 作出貢獻,而且將這一貢獻轉發至 Okta 了。期待咱們團隊可以推出更多 Shiro 的內容,包括給 Okta 和 OAuth 的 Shiro 使用手冊以及如何在此志願者應用程序中添加 AngularJS 前端代碼。請繼續關注,帝國須要你!
關於這個例子,若是你有任何疑問,請將它們發送至 Apache Shiro 的用戶列表或者是個人 Twitter 帳戶,也能夠直接在下方評論區留言!
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。