一起來學SpringBoot(十六)優雅的整合Shiro
Apache Shiro是一個功能強大且易於使用的Java安全框架,可執行身份驗證,授權,加密和會話管理。藉助Shiro易於理解的API,您可以快速輕鬆地保護任何應用程式 - 從最小的移動應用程式到最大的Web和企業應用程式。網上找到大部分文章都是以前SpringMVC下的整合方式,很多人都不知道shiro提供了官方的starter可以方便地跟SpringBoot整合。
整合準備
這篇文件的介紹也相當簡單。我們只需要按照文件說明,然後在spring容器中注入一個我們自定義的Realm
,shiro通過這個realm就可以知道如何獲取使用者資訊來處理鑑權(Authentication)
,如何獲取使用者角色、許可權資訊來處理授權(Authorization)
shiro-spring-boot-web-starter
,單獨的應用程式的話則引入shiro-spring-boot-starter
。
依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0-RC2</version>
</dependency>
使用者實體
首先建立一個使用者的實體,用來做認證
package com.maoxs.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
@Data
public class User implements Serializable {
private Long uid; // 使用者id
private String uname; // 登入名,不可改
private String nick; // 使用者暱稱,可改
private String pwd; // 已加密的登入密碼
private String salt; // 加密鹽值
private Date created; // 建立時間
private Date updated; // 修改時間
private Set<String> roles = new HashSet<>(); //使用者所有角色值,用於shiro做角色許可權的判斷
private Set<String> perms = new HashSet<>(); //使用者所有許可權值,用於shiro做資源許可權的判斷
}
這裡了為了方便,就不去資料庫讀取了,方便測試我們把,許可權資訊,角色資訊,認證資訊都靜態模擬下。
Resources
package com.maoxs.service;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.Set;
@Service
public class ResourcesService {
/**
* 模擬根據使用者id查詢返回使用者的所有許可權
*
* @param uid
* @return
*/
public Set<String> getResourcesByUserId(Long uid) {
Set<String> perms = new HashSet<>();
//三種程式語言代表三種角色:js程式設計師、java程式設計師、c++程式設計師
//docker的許可權
perms.add("docker:run");
perms.add("docker:ps");
//maven的許可權
perms.add("mvn:debug");
perms.add("mvn:test");
perms.add("mvn:install");
//node的許可權
perms.add("npm:clean");
perms.add("npm:run");
perms.add("npm:test");
return perms;
}
}
Role
package com.maoxs.service;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.Set;
@Service
public class RoleService {
/**
* 模擬根據使用者id查詢返回使用者的所有角色
*
* @param uid
* @return
*/
public Set<String> getRolesByUserId(Long uid) {
Set<String> roles = new HashSet<>();
//這裡用三個工具代表角色
roles.add("docker");
roles.add("maven");
roles.add("node");
return roles;
}
}
User
package com.maoxs.service;
import com.maoxs.pojo.User;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Random;
@Service
public class UserService {
/**
* 模擬查詢返回使用者資訊
*
* @param uname
* @return
*/
public User findUserByName(String uname) {
User user = new User();
user.setUname(uname);
user.setNick(uname + "NICK");
user.setPwd("J/ms7qTJtqmysekuY8/v1TAS+VKqXdH5sB7ulXZOWho=");//密碼明文是123456
user.setSalt("wxKYXuTPST5SG0jMQzVPsg==");//加密密碼的鹽值
user.setUid(new Random().nextLong());//隨機分配一個id
user.setCreated(new Date());
return user;
}
}
認證
Shiro 從從Realm獲取安全資料(如使用者、角色、許可權),就是說SecurityManager要驗證使用者身份,那麼它需要從Realm獲取相應的使用者進行比較以確定使用者身份是否合法;也需要從Realm得到使用者相應的角色/許可權進行驗證使用者是否能進行操作;可以把Realm看成DataSource , 即安全資料來源。
Realm
package com.maoxs.realm;
import com.maoxs.cache.MySimpleByteSource;
import com.maoxs.pojo.User;
import com.maoxs.service.ResourcesService;
import com.maoxs.service.RoleService;
import com.maoxs.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Set;
/**
* 這個類是參照JDBCRealm寫的,主要是自定義瞭如何查詢使用者資訊,如何查詢使用者的角色和許可權,如何校驗密碼等邏輯
*/
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private ResourcesService resourcesService;
//告訴shiro如何根據獲取到的使用者資訊中的密碼和鹽值來校驗密碼
{
//設定用於匹配密碼的CredentialsMatcher
HashedCredentialsMatcher hashMatcher = new HashedCredentialsMatcher();
hashMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
hashMatcher.setStoredCredentialsHexEncoded(false);
hashMatcher.setHashIterations(1024);
this.setCredentialsMatcher(hashMatcher);
}
//定義如何獲取使用者的角色和許可權的邏輯,給shiro做許可權判斷
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//null usernames are invalid
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
User user = (User) getAvailablePrincipal(principals);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
System.out.println("獲取角色資訊:" + user.getRoles());
System.out.println("獲取許可權資訊:" + user.getPerms());
info.setRoles(user.getRoles());
info.setStringPermissions(user.getPerms());
return info;
}
//定義如何獲取使用者資訊的業務邏輯,給shiro做登入
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
// Null username is invalid
if (username == null) {
throw new AccountException("請輸入使用者名稱");
}
User userDB = userService.findUserByName(username);
if (userDB == null) {
throw new UnknownAccountException("使用者不存在");
}
//查詢使用者的角色和許可權存到SimpleAuthenticationInfo中,這樣在其它地方
//SecurityUtils.getSubject().getPrincipal()就能拿出使用者的所有資訊,包括角色和許可權
Set<String> roles = roleService.getRolesByUserId(userDB.getUid());
Set<String> perms = resourcesService.getResourcesByUserId(userDB.getUid());
userDB.getRoles().addAll(roles);
userDB.getPerms().addAll(perms);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDB, userDB.getPwd(), getName());
if (userDB.getSalt() != null) {
info.setCredentialsSalt(new MySimpleByteSource(userDB.getSalt()));
}
return info;
}
}
相關配置
然後呢在只需要吧這個Realm註冊到Spring容器中就可以啦
@Bean
public CustomRealm customRealm() {
CustomRealm realm = new CustomRealm();
return realm;
}
為了保證實現了Shiro內部lifecycle函式的bean執行 也是shiro的生命週期,注入LifecycleBeanPostProcessor
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
緊接著配置安全管理器,SecurityManager是Shiro框架的核心,典型的Facade模式,Shiro通過SecurityManager來管理內部元件例項,並通過它來提供安全管理的各種服務。
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
return securityManager;
}
除此之外Shiro是一堆一堆的過濾鏈,所以要對shiro 的過濾進行設定,
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("favicon.ico", "anon");
chainDefinition.addPathDefinition("/login", "anon");
chainDefinition.addPathDefinition("/**", "user");
return chainDefinition;
}
yml
這裡要說明下由於我們引入的是shiro-spring-boot-web-starter
,官方對配置進行了一系列的簡化,並加入了一些自動配置項,所以我們要在yml中加入
shiro:
web:
enabled: true
loginUrl: /login
除此之外呢還有這些屬性
鍵 | 預設值 | 描述 |
---|---|---|
shiro.enabled | true |
啟用Shiro的Spring模組 |
shiro.web.enabled | true |
啟用Shiro的Spring Web模組 |
shiro.annotations.enabled | true |
為Shiro的註釋啟用Spring支援 |
shiro.sessionManager.deleteInvalidSessions | true |
從會話儲存中刪除無效會話 |
shiro.sessionManager.sessionIdCookieEnabled | true |
啟用會話ID到cookie,用於會話跟蹤 |
shiro.sessionManager.sessionIdUrlRewritingEnabled | true |
啟用會話URL重寫支援 |
shiro.userNativeSessionManager | false |
如果啟用,Shiro將管理HTTP會話而不是容器 |
shiro.sessionManager.cookie.maxAge | -1 |
會話cookie最大年齡 |
shiro.sessionManager.cookie.domain | 空值 | 會話cookie域 |
shiro.sessionManager.cookie.path | 空值 | 會話cookie路徑 |
shiro.sessionManager.cookie.secure | false |
會話cookie安全標誌 |
shiro.rememberMeManager.cookie.maxAge | 一年 | RememberMe cookie最大年齡 |
shiro.rememberMeManager.cookie.domain | 空值 | RememberMe cookie域名 |
shiro.rememberMeManager.cookie.path | 空值 | RememberMe cookie路徑 |
shiro.rememberMeManager.cookie.secure | false |
RememberMe cookie安全標誌 |
shiro.loginUrl | /login.jsp |
未經身份驗證的使用者重定向到登入頁面時使用的登入URL |
shiro.successUrl | / |
使用者登入後的預設登入頁面(如果在當前會話中找不到替代) |
shiro.unauthorizedUrl | 空值 | 頁面將使用者重定向到未授權的位置(403頁) |
在Controller中新增登入方法
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public Result login(@RequestParam("username") String userName, @RequestParam("password") String Password) throws Exception {
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(userName, Password);
token.setRememberMe(true);// 預設不記住密碼
try {
currentUser.login(token); //登入
log.info("==========登入成功=======");
return new Result(true, "登入成功");
} catch (UnknownAccountException e) {
log.info("==========使用者名稱不存在=======");
return new Result(false, "使用者名稱不存在");
} catch (DisabledAccountException e) {
log.info("==========您的賬戶已經被凍結=======");
return new Result(false, "您的賬戶已經被凍結");
} catch (IncorrectCredentialsException e) {
log.info("==========密碼錯誤=======");
return new Result(false, "密碼錯誤");
} catch (ExcessiveAttemptsException e) {
log.info("==========您錯誤的次數太多了吧,封你半小時=======");
return new Result(false, "您錯誤的次數太多了吧,封你半小時");
} catch (RuntimeException e) {
log.info("==========執行異常=======");
return new Result(false, "執行異常");
}
}
@RequestMapping("/logout")
public String logOut() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "index";
}
這樣就實現了整合認證的流程,,如果token資訊與資料庫表總username和password資料一致,則該使用者身份認證成功。
鑑權
只用註解控制鑑權授權
使用註解的優點是控制的粒度細,並且非常適合用來做基於資源的許可權控制。
只用註解的話非常簡單。我們只需要使用url配置配置一下所以請求路徑都可以匿名訪問:
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
//這裡配置所有請求路徑都可以匿名訪問
chain.addPathDefinition("/**", "anon");
// 這另一種配置方式。但是還是用上面那種吧,容易理解一點。
// chainDefinition.addPathDefinition("/**", "authcBasic[permissive]");
return chain;
}
然後在控制器類上使用shiro提供的種註解來做控制:
註解 | 功能 |
---|---|
@RequiresGuest | 只有遊客可以訪問 |
@RequiresAuthentication | 需要登入才能訪問 |
@RequiresUser | 已登入的使用者或“記住我”的使用者能訪問 |
@RequiresRoles | 已登入的使用者需具有指定的角色才能訪問 |
@RequiresPermissions | 已登入的使用者需具有指定的許可權才能訪問 |
示例
/**
* created by CaiBaoHong at 2018/4/18 15:51<br>
* 測試shiro提供的註解及功能解釋
*/
@RestController
public class Test1Controller {
// 由於TestController類上沒有加@RequiresAuthentication註解,
// 不要求使用者登入才能呼叫介面。所以hello()和a1()介面都是可以匿名訪問的
@GetMapping("/hello")
public String hello() {
return "hello spring boot";
}
// 遊客可訪問,這個有點坑,遊客的意思是指:subject.getPrincipal()==null
// 所以使用者在未登入時subject.getPrincipal()==null,介面可訪問
// 而使用者登入後subject.getPrincipal()!=null,介面不可訪問
@RequiresGuest
@GetMapping("/guest")
public String guest() {
return "@RequiresGuest";
}
// 已登入使用者才能訪問,這個註解比@RequiresUser更嚴格
// 如果使用者未登入呼叫該介面,會丟擲UnauthenticatedException
@RequiresAuthentication
@GetMapping("/authn")
public String authn() {
return "@RequiresAuthentication";
}
// 已登入使用者或“記住我”的使用者可以訪問
// 如果使用者未登入或不是“記住我”的使用者呼叫該介面,UnauthenticatedException
@RequiresUser
@GetMapping("/user")
public String user() {
return "@RequiresUser";
}
// 要求登入的使用者具有mvn:build許可權才能訪問
// 由於UserService模擬返回的使用者資訊中有該許可權,所以這個介面可以訪問
// 如果沒有登入,UnauthenticatedException
@RequiresPermissions("mvn:install")
@GetMapping("/mvnInstall")
public String mvnInstall() {
return "mvn:install";
}
// 要求登入的使用者具有mvn:build許可權才能訪問
// 由於UserService模擬返回的使用者資訊中【沒有】該許可權,所以這個介面【不可以】訪問
// 如果沒有登入,UnauthenticatedException
// 如果登入了,但是沒有這個許可權,會報錯UnauthorizedException
@RequiresPermissions("gradleBuild")
@GetMapping("/gradleBuild")
public String gradleBuild() {
return "gradleBuild";
}
// 要求登入的使用者具有js角色才能訪問
// 由於UserService模擬返回的使用者資訊中有該角色,所以這個介面可訪問
// 如果沒有登入,UnauthenticatedException
@RequiresRoles("docker")
@GetMapping("/docker")
public String docker() {
return "docker programmer";
}
// 要求登入的使用者具有js角色才能訪問
// 由於UserService模擬返回的使用者資訊中有該角色,所以這個介面可訪問
// 如果沒有登入,UnauthenticatedException
// 如果登入了,但是沒有該角色,會丟擲UnauthorizedException
@RequiresRoles("python")
@GetMapping("/python")
public String python() {
return "python programmer";
}
}
注意 解決spring aop和註解配置一起使用的bug。如果您在使用shiro註解配置的同時,引入了spring aop的starter,會有一