網上找到大部分文章都是之前SpringMVC下的整合方式,不少人都不知道shiro提供了官方的starter能夠方便地跟SpringBoot整合。本文介紹個人3種整合思路:1.徹底使用註解;2.徹底使用url配置;3.url配置和註解混用,url配置負責鑑權控制,註解負責權限控制。三種方式各有優劣,需考慮實際應用場景使用。
Talk is cheap, show you my code: elegant-shiro-boot
這個工程使用gradle構建,有三個子工程:html
請看shiro官網關於springboot整合shiro的連接:Integrating Apache Shiro into Spring-Boot Applicationsjava
好笑的是,我本身直接上去官網找,找來找去都找不到這一頁的文檔,而是經過google找出來的。
這篇文檔的介紹也至關簡單。咱們只須要按照文檔說明,引入shiro-spring-boot-starter
,而後在spring容器中注入一個咱們自定義的Realm
,shiro經過這個realm就能夠知道如何獲取用戶信息來處理鑑權(Authentication)
,如何獲取用戶角色、權限信息來處理受權(Authorization)
。python
ps:鑑權能夠理解成判斷一個用戶是否已登陸的過程,受權能夠理解成判斷一個已登陸用戶是否有訪問權限的過程。
整合過程:
1.引入starter,個人是用gradle作項目構建的,maven也是引入對應的依賴便可:c++
dependencies { //spring boot的starter compile 'org.springframework.boot:spring-boot-starter-web' compile 'org.springframework.boot:spring-boot-starter-aop' compile 'org.springframework.boot:spring-boot-devtools' testCompile 'org.springframework.boot:spring-boot-starter-test' //shiro compile 'org.apache.shiro:shiro-spring-boot-web-starter:1.4.0' }
2.編寫自定義realmgit
User.java(其它RBAC模型請看github上的代碼com.abc.entity包下的類)程序員
public class User { 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作資源權限的判斷 //getters and setters... }
UserService.javagithub
@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; } }
RoleService.javaweb
@Service public class RoleService { /** * 模擬根據用戶id查詢返回用戶的全部角色,實際查詢語句參考: * SELECT r.rval FROM role r, user_role ur * WHERE r.rid = ur.role_id AND ur.user_id = #{userId} * @param uid * @return */ public Set<String> getRolesByUserId(Long uid){ Set<String> roles = new HashSet<>(); //三種編程語言表明三種角色:js程序員、java程序員、c++程序員 roles.add("js"); roles.add("java"); roles.add("cpp"); return roles; } }
PermService.javaspring
@Service public class PermService { /** * 模擬根據用戶id查詢返回用戶的全部權限,實際查詢語句參考: * SELECT p.pval FROM perm p, role_perm rp, user_role ur * WHERE p.pid = rp.perm_id AND ur.role_id = rp.role_id * AND ur.user_id = #{userId} * @param uid * @return */ public Set<String> getPermsByUserId(Long uid){ Set<String> perms = new HashSet<>(); //三種編程語言表明三種角色:js程序員、java程序員、c++程序員 //js程序員的權限 perms.add("html:edit"); //c++程序員的權限 perms.add("hardware:debug"); //java程序員的權限 perms.add("mvn:install"); perms.add("mvn:clean"); perms.add("mvn:test"); return perms; } }
CustomRealm.javaapache
/** * 這個類是參照JDBCRealm寫的,主要是自定義瞭如何查詢用戶信息,如何查詢用戶的角色和權限,如何校驗密碼等邏輯 */ public class CustomRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private RoleService roleService; @Autowired private PermService permService; //告訴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("Null usernames are not allowed by this realm."); } User userDB = userService.findUserByName(username); if (userDB == null) { throw new UnknownAccountException("No account found for admin [" + username + "]"); } //查詢用戶的角色和權限存到SimpleAuthenticationInfo中,這樣在其它地方 //SecurityUtils.getSubject().getPrincipal()就能拿出用戶的全部信息,包括角色和權限 Set<String> roles = roleService.getRolesByUserId(userDB.getUid()); Set<String> perms = permService.getPermsByUserId(userDB.getUid()); userDB.getRoles().addAll(roles); userDB.getPerms().addAll(perms); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDB, userDB.getPwd(), getName()); if (userDB.getSalt() != null) { info.setCredentialsSalt(ByteSource.Util.bytes(userDB.getSalt())); } return info; } }
3.使用註解或url配置,來控制鑑權受權
請參照官網的示例:
//url配置 @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition(); // logged in users with the 'admin' role chainDefinition.addPathDefinition("/admin/**", "authc, roles[admin]"); // logged in users with the 'document:read' permission chainDefinition.addPathDefinition("/docs/**", "authc, perms[document:read]"); // all other paths require a logged in user chainDefinition.addPathDefinition("/**", "authc"); return chainDefinition; }
//註解配置 @RequiresPermissions("document:read") public void readDocument() { ... }
4.解決spring aop和註解配置一塊兒使用的bug。若是您在使用shiro註解配置的同時,引入了spring aop的starter,會有一個奇怪的問題,致使shiro註解的請求,不能被映射,需加入如下配置:
@Bean public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator(); /** * setUsePrefix(false)用於解決一個奇怪的bug。在引入spring aop的狀況下。 * 在@Controller註解的類的方法中加入@RequiresRole等shiro註解,會致使該方法沒法映射請求,致使返回404。 * 加入這項配置能解決這個bug */ defaultAdvisorAutoProxyCreator.setUsePrefix(true); return defaultAdvisorAutoProxyCreator; }
使用註解的優勢是控制的粒度細,而且很是適合用來作基於資源的權限控制。
關於基於資源的權限控制,建議看看這篇文章: The New RBAC: Resource-Based Access Control
只用註解的話很是簡單。咱們只須要使用url配置配置一下因此請求路徑均可以匿名訪問:
//在 ShiroConfig.java 中的代碼: @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition(); // 因爲demo1展現統一使用註解作訪問控制,因此這裏配置全部請求路徑均可以匿名訪問 chain.addPathDefinition("/**", "anon"); // all paths are managed via annotations // 這另外一種配置方式。可是仍是用上面那種吧,容易理解一點。 // or allow basic authentication, but NOT require it. // chainDefinition.addPathDefinition("/**", "authcBasic[permissive]"); return chain; }
而後在控制器類上使用shiro提供的種註解來作控制:
註解 | 功能 |
---|---|
@RequiresGuest | 只有遊客能夠訪問 |
@RequiresAuthentication | 須要登陸才能訪問 |
@RequiresUser | 已登陸的用戶或「記住我」的用戶能訪問 |
@RequiresRoles | 已登陸的用戶需具備指定的角色才能訪問 |
@RequiresPermissions | 已登陸的用戶需具備指定的權限才能訪問 |
代碼示例:(更詳細的請參考github代碼的demo1)
/** * created by CaiBaoHong at 2018/4/18 15:51<br> * 測試shiro提供的註解及功能解釋 */ @RestController @RequestMapping("/t1") 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("js") @GetMapping("/js") public String js() { return "js programmer"; } // 要求登陸的用戶具備js角色才能訪問 // 因爲UserService模擬返回的用戶信息中有該角色,因此這個接口可訪問 // 若是沒有登陸,UnauthenticatedException // 若是登陸了,可是沒有該角色,會拋出UnauthorizedException @RequiresRoles("python") @GetMapping("/python") public String python() { return "python programmer"; } }
shiro提供和多個默認的過濾器,咱們能夠用這些過濾器來配置控制指定url的權限:
配置縮寫 | 對應的過濾器 | 功能 |
---|---|---|
anon | AnonymousFilter | 指定url能夠匿名訪問 |
authc | FormAuthenticationFilter | 指定url須要form表單登陸,默認會從請求中獲取username 、password ,rememberMe 等參數並嘗試登陸,若是登陸不了就會跳轉到loginUrl配置的路徑。咱們也能夠用這個過濾器作默認的登陸邏輯,可是通常都是咱們本身在控制器寫登陸邏輯的,本身寫的話出錯返回的信息均可以定製嘛。 |
authcBasic | BasicHttpAuthenticationFilter | 指定url須要basic登陸 |
logout | LogoutFilter | 登出過濾器,配置指定url就能夠實現退出功能,很是方便 |
noSessionCreation | NoSessionCreationFilter | 禁止建立會話 |
perms | PermissionsAuthorizationFilter | 須要指定權限才能訪問 |
port | PortFilter | 須要指定端口才能訪問 |
rest | HttpMethodPermissionFilter | 將http請求方法轉化成相應的動詞來構造一個權限字符串,這個感受意義不大,有興趣本身看源碼的註釋 |
roles | RolesAuthorizationFilter | 須要指定角色才能訪問 |
ssl | SslFilter | 須要https請求才能訪問 |
user | UserFilter | 須要已登陸或「記住我」的用戶才能訪問 |
在spring容器中使用ShiroFilterChainDefinition
來控制全部url的鑑權和受權。優勢是配置粒度大,對多個Controller作鑑權受權的控制。下面是例子,具體能夠看github代碼的demo2:
@Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition(); /** * 這裏當心踩坑!我在application.yml中設置的context-path: /api/v1 * 但通過實際測試,過濾器的過濾路徑,是context-path下的路徑,無需加上"/api/v1"前綴 */ //訪問控制 chain.addPathDefinition("/user/login", "anon");//能夠匿名訪問 chain.addPathDefinition("/page/401", "anon");//能夠匿名訪問 chain.addPathDefinition("/page/403", "anon");//能夠匿名訪問 chain.addPathDefinition("/t4/hello", "anon");//能夠匿名訪問 chain.addPathDefinition("/t4/changePwd", "authc");//須要登陸 chain.addPathDefinition("/t4/user", "user");//已登陸或「記住我」的用戶能夠訪問 chain.addPathDefinition("/t4/mvnBuild", "authc,perms[mvn:install]");//須要mvn:build權限 chain.addPathDefinition("/t4/gradleBuild", "authc,perms[gradle:build]");//須要gradle:build權限 chain.addPathDefinition("/t4/js", "authc,roles[js]");//須要js角色 chain.addPathDefinition("/t4/python", "authc,roles[python]");//須要python角色 // shiro 提供的登出過濾器,訪問指定的請求,就會執行登陸,默認跳轉路徑是"/",或者是"shiro.loginUrl"配置的內容 // 因爲application-shiro.yml中配置了 shiro:loginUrl: /page/401,返回會返回對應的json內容 // 能夠結合/user/login和/t1/js接口來測試這個/t4/logout接口是否有效 chain.addPathDefinition("/t4/logout", "anon,logout"); //其它路徑均須要登陸 chain.addPathDefinition("/**", "authc"); return chain; }
就我的而言,我是很是喜歡註解方式的。可是兩種配置方式靈活結合,纔是適應不一樣應用場景的最佳實踐。只用註解或只用url配置,會帶來一些比較累的工做。
我舉兩個場景:
場景1
假如我是寫系統後臺管理系統的,並且個人java後臺是一個純粹返回json數據的後臺,不會作頁面跳轉的工做。那咱們後臺管理系統通常都是所有接口都須要登陸才能訪問。若是隻用註解,我須要在每一個Controller上加上@RequiresAuthentication
來聲明每一個Controller下每一個方法都須要登陸才能訪問。這樣顯得有點麻煩,並且往後再加Controller,仍是要加上這個註解,萬一忘記加了就會出錯。這時候其實用url配置的方式就能夠配置所有請求都須要登陸才能訪問:chain.addPathDefinition("/**", "authc");
場景2
假如我是寫商城的前臺的,並且個人java後臺是一個純粹返回json數據的後臺,可是這些接接口中,在同一個Controller下,有些是能夠匿名訪問的,有些是須要登陸才能訪問的,有些是須要特定角色、權限才能訪問的。若是隻用url配置,每一個url都須要配置,並且容易配置錯,粒度很差把控。
因此個人想法是:用url配置控制鑑權,實現粗粒度控制;用註解控制受權,實現細粒度控制
。
下面是示例代碼(詳細的請看github代碼的demo3):
ShiroConfig.java
@Configuration public class ShiroConfig { //注入自定義的realm,告訴shiro如何獲取用戶信息來作登陸或權限控制 @Bean public Realm realm() { return new CustomRealm(); } @Bean public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); /** * setUsePrefix(false)用於解決一個奇怪的bug。在引入spring aop的狀況下。 * 在@Controller註解的類的方法中加入@RequiresRole註解,會致使該方法沒法映射請求,致使返回404。 * 加入這項配置能解決這個bug */ creator.setUsePrefix(true); return creator; } /** * 這裏統一作鑑權,即判斷哪些請求路徑須要用戶登陸,哪些請求路徑不須要用戶登陸。 * 這裏只作鑑權,不作權限控制,由於權限用註解來作。 * @return */ @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition(); //哪些請求能夠匿名訪問 chain.addPathDefinition("/user/login", "anon"); chain.addPathDefinition("/page/401", "anon"); chain.addPathDefinition("/page/403", "anon"); chain.addPathDefinition("/t5/hello", "anon"); chain.addPathDefinition("/t5/guest", "anon"); //除了以上的請求外,其它請求都須要登陸 chain.addPathDefinition("/**", "authc"); return chain; } }
PageController.java
@RestController @RequestMapping("/page") public class PageController { // shiro.loginUrl映射到這裏,我在這裏直接拋出異常交給GlobalExceptionHandler來統一返回json信息, // 您也能夠在這裏json,不過這樣子就跟GlobalExceptionHandler中返回的json重複了。 @RequestMapping("/401") public Json page401() { throw new UnauthenticatedException(); } // shiro.unauthorizedUrl映射到這裏。因爲demo3統一約定了url方式只作鑑權控制,不作權限訪問控制, // 也就是說在ShiroConfig中若是沒有roles[js],perms[mvn:install]這樣的權限訪問控制配置的話, // 是不會跳轉到這裏的。 @RequestMapping("/403") public Json page403() { throw new UnauthorizedException(); } @RequestMapping("/index") public Json pageIndex() { return new Json("index",true,1,"index page",null); } }
GlobalExceptionHandler.java
/** * 統一捕捉shiro的異常,返回給前臺一個json信息,前臺根據這個信息顯示對應的提示,或者作頁面的跳轉。 */ @ControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); //不知足@RequiresGuest註解時拋出的異常信息 private static final String GUEST_ONLY = "Attempting to perform a guest-only operation"; @ExceptionHandler(ShiroException.class) @ResponseBody public Json handleShiroException(ShiroException e) { String eName = e.getClass().getSimpleName(); log.error("shiro執行出錯:{}",eName); return new Json(eName, false, Codes.SHIRO_ERR, "鑑權或受權過程出錯", null); } @ExceptionHandler(UnauthenticatedException.class) @ResponseBody public Json page401(UnauthenticatedException e) { String eMsg = e.getMessage(); if (StringUtils.startsWithIgnoreCase(eMsg,GUEST_ONLY)){ return new Json("401", false, Codes.UNAUTHEN, "只容許遊客訪問,若您已登陸,請先退出登陸", null) .data("detail",e.getMessage()); }else{ return new Json("401", false, Codes.UNAUTHEN, "用戶未登陸", null) .data("detail",e.getMessage()); } } @ExceptionHandler(UnauthorizedException.class) @ResponseBody public Json page403() { return new Json("403", false, Codes.UNAUTHZ, "用戶沒有訪問權限", null); } }
TestController.java
@RestController @RequestMapping("/t5") public class Test5Controller { // 因爲ShiroConfig中配置了該路徑能夠匿名訪問,因此這接口不須要登陸就能訪問 @GetMapping("/hello") public String hello() { return "hello spring boot"; } // 若是ShiroConfig中沒有配置該路徑能夠匿名訪問,因此直接被登陸過濾了。 // 若是配置了能夠匿名訪問,那這裏在沒有登陸的時候能夠訪問,可是用戶登陸後就不能訪問 @RequiresGuest @GetMapping("/guest") public String guest() { return "@RequiresGuest"; } @RequiresAuthentication @GetMapping("/authn") public String authn() { return "@RequiresAuthentication"; } @RequiresUser @GetMapping("/user") public String user() { return "@RequiresUser"; } @RequiresPermissions("mvn:install") @GetMapping("/mvnInstall") public String mvnInstall() { return "mvn:install"; } @RequiresPermissions("gradleBuild") @GetMapping("/gradleBuild") public String gradleBuild() { return "gradleBuild"; } @RequiresRoles("js") @GetMapping("/js") public String js() { return "js programmer"; } @RequiresRoles("python") @GetMapping("/python") public String python() { return "python programmer"; } }