早期的 Spring Boot 和 Shiro 的整合是通過改造 Spring MVC 和 Shiro 的整合而來噪猾。簡單來說公般,就是將 Spring MVC 和 Shiro 的整合在 Spring Boot 中以 Java 代碼配置的方式再寫一遍恒削。
不過捧请,后來 Shiro 官方提供了和 Spring Boot 整合的 Starter 芙贫,并提供了官方教程:https://shiro.apache.org/spring-boot.html
用 Starter 整合
依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId> <!-- 注意這里有個(gè) web -->
<version>1.7.0</version>
</dependency>
必要的兩項(xiàng)配置
# thymeleaf 的配置略
shiro:
loginUrl: "/login-page" # 未通過『認(rèn)證』時(shí)精居,跳轉(zhuǎn)顯示的頁面
unauthorizedUrl: "/403-page" # 未通過『鑒權(quán)』時(shí)锄禽,跳轉(zhuǎn)顯示的頁面
-
配置類
@Configuration public class ShiroConfig { /* * 在沒有配置 Realm 和 DefaultWebSecurityManager 的情況下, * Shiro 的 starter 默認(rèn)使用的是 IniRealm靴姿,它會(huì)要求你在 classpath 下提供一個(gè)名為 shiro.ini 的文件沃但。 */ @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition(); chain.addPathDefinition("/login-page", "anon"); chain.addPathDefinition("/403-page", "anon"); chain.addPathDefinition("/login", "anon"); chain.addPathDefinition("/logout", "logout"); chain.addPathDefinition("/hello", "anon"); chain.addPathDefinition("/**", "authc"); return chain; } /* // 不再需要。高版本的 spring boot 默認(rèn)的 aop 方案已經(jīng)指定為成 cglib 了佛吓。 @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); // 強(qiáng)制指定注解的底層實(shí)現(xiàn)使用 cglib 方案 defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } */ }
LoginController 和 PageController
@Slf4j
@Controller
public class LoginController {
@ExceptionHandler({Exception.class})
public String exception(Exception e) {
if (e instanceof IncorrectCredentialsException)
log.warn("密碼錯(cuò)誤", e);
else
log.warn("其它錯(cuò)誤", e);
return "redirect:/failure-page";
}
@RequestMapping("/login")
public String login(String username, String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
subject.login(token);
log.info("登錄成功");
return "redirect:/success-page";
}
}
@Slf4j
@Controller
public class PageController {
@RequestMapping("/login-page")
public String loginPage(Model model) {
return "login";
}
@RequestMapping("/403-page")
public String _403Page() {
return "403";
}
@RequestMapping("/success-page")
public String successPage() {
return "success";
}
@RequestMapping("/failure-page")
public String failurePage() {
return "failure";
}
@RequestMapping("/hello-page")
public String helloPage() {
return "hello";
}
}
使用注解
如果是以 Shiro 整合 Spring宵晚、Spring MVC 的方式來整合 Spring Boot垂攘,那么要在項(xiàng)目中使用 Shiro 的注解,那還是需要像之前的內(nèi)容那樣進(jìn)行配置淤刃,并且解決 $Proxy
問題晒他。
不過在 shiro 官方提供 shiro 整合 starter 的包之后,以 starter 包的方式整合 spring boot 不需要再進(jìn)行額外配置钝凶,即可使用仪芒。
按慣例:
在配置文件中配置『認(rèn)證』等規(guī)則唁影;
通過注解配置『鑒權(quán)』規(guī)則耕陷。
在使用注解的情況下,realm 中的 doGetAuthorization 方法不是靠 RolesAuthorizationFilter 和 PermissionsAuthorizationFilter 中的 subject.hasRole()
和 subject.isPermitted()
方法觸發(fā)据沈,而是『靠 AOP 反射注解信息來觸發(fā)的』哟沫。
注解實(shí)現(xiàn)鑒權(quán)的原理
在使用注解進(jìn)行鑒權(quán)的情況下,Shiro 并未使用 RolesAuthorizationFilter 和 PermissionsAuthorizationFilter锌介,而是使用 AOP 反射注解信息嗜诀,在 Controller 的代理類中使用 subject.hashRole()
和 subject.isPermitted()
方法觸發(fā)鑒權(quán)功能。
我們可以模擬這個(gè)方案:
-
自定義注解
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RequiresRole { public String[] value() default ""; }
-
在 Spring boot 中引入 AOP 依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
-
實(shí)現(xiàn) Controller 的切面類:
@Aspect @Component public class HelloControllerAspect { @Around("execution(* com.example.controller.HelloController.*(..))") public Object around(ProceedingJoinPoint jp) { try { ... return jp.proceed(); } catch (Throwable e) { e.printStackTrace(); } return null; } }
-
在切面類的方法中孔祸,獲取 Controller 的 Method隆敢,并進(jìn)一步獲取其上的注解
try { MethodSignature methodSignature = (MethodSignature) jp.getSignature(); Method method = methodSignature.getMethod(); RequiresRole requiresRole = method.getAnnotation(RequiresRole.class); if (requiresRole != null) { String[] roles = requiresRole.value(); System.out.println("執(zhí)行本方法需要 " + Arrays.toString(roles) + " 權(quán)限"); // for (String role : roles) { // if (subject.hashRole("role")) // return jp.proceed(); // } } return jp.proceed(); } catch (Throwable e) { e.printStackTrace(); }
Shiro 標(biāo)簽庫
Shiro 為頁面<small>(jsp、thymeleaf)</small>提供了一套標(biāo)簽庫崔慧,以結(jié)合權(quán)限管理功能使用拂蝎。
<!-- 引入依賴 -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
使用 Shiro 標(biāo)簽庫時(shí),在配置類中有個(gè)開關(guān)配置要打開:
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
例如:
<html lang="zh_CN" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<shiro:hasRole name="admin">
<button type="button" class="btn btn-outline btn-default">
<i class="glyphicon glyphicon-heart" aria-hidden="true"></i>
</button>
</shiro:hasRole>
部分實(shí)例匯總:
<table border="1" cellpadding="0" cellspacing="0">
<tr>
<td> shiro:guest </td>
<td> <shiro:guest>游客能看到這段內(nèi)容</shiro:guest> </td>
</tr>
<tr>
<td> shiro:user </td>
<td> <shiro:user>用戶能看到這段內(nèi)容</shiro:user> </td>
</tr>
<tr>
<td> shiro:notAuthenticated </td>
<td> <shiro:notAuthenticated>未通過『認(rèn)證』能看到這段內(nèi)容</shiro:notAuthenticated> </td>
</tr>
<tr>
<td> shiro:authenticated </td>
<td> <shiro:authenticated>通過了『認(rèn)證』能看到這段內(nèi)容</shiro:authenticated> </td>
</tr>
<tr>
<td> shiro:hashRole </td>
<td> <shiro:hasRole name="admin">有『admin』角色能看到這段內(nèi)容</shiro:hasRole> </td>
</tr>
<tr>
<td> shiro:lacksRole </td>
<td> <shiro:lacksRole name="admin">缺『admin』角色能看到這段內(nèi)容</shiro:lacksRole> </td>
</tr>
<tr>
<td> shiro:hasPermission </td>
<td> <shiro:hasPermission name="user:insert">有『user:insert』權(quán)限能看到這段內(nèi)容</shiro:hasPermission> </td>
</tr>
<tr>
<td> shiro:lacksPermission </td>
<td> <shiro:lacksPermission name="user:insert">缺『user:insert』權(quán)限能看到這段內(nèi)容</shiro:lacksPermission> </td>
</tr>
</table>
標(biāo)簽 | 說明 |
---|---|
guest | 用戶沒有身份驗(yàn)證時(shí)顯示相應(yīng)信息惶室,即游客訪問信息温自。 |
user | 用戶已經(jīng)身份驗(yàn)證/記住我登錄后顯示相應(yīng)的信息。 |
authenticated | 用戶已經(jīng)身份驗(yàn)證通過皇钞,即 Subject.login 登錄成功悼泌,不是記住我登錄的。 |
notAuthenticated | 用戶已經(jīng)身份驗(yàn)證通過夹界,即沒有調(diào)用 Subject.login 進(jìn)行登錄馆里,包括記住我自動(dòng)登錄的也屬于未進(jìn)行身份驗(yàn)證。 |
principal | 相當(dāng)于 ((User)Subject.getPrincipals()).getUsername() 可柿。 |
lacksPermission | 如果當(dāng)前 Subject 沒有權(quán)限將顯示 body 體內(nèi)容鸠踪。 |
hasRole | 如果當(dāng)前 Subject 有角色將顯示 body 體內(nèi)容。 |
hasAnyRoles | 如果當(dāng)前 Subject 有任意一個(gè)角色<small>(或的關(guān)系)</small>將顯示body體內(nèi)容趾痘。 |
lacksRole | 如果當(dāng)前 Subject 沒有角色將顯示 body 體內(nèi)容慢哈。 |
hasPermission | 如果當(dāng)前 Subject 有權(quán)限將顯示 body 體內(nèi)容 |
補(bǔ)充:Start 中包含的配置(了解、自學(xué))
Key | Default Value | Description |
---|---|---|
shiro.enabled | true | Enables Shiro’s Spring module |
shiro.web.enabled | true | Enables Shiro’s Spring web module |
shiro.annotations.enabled | true | Enables Spring support for Shiro’s annotations |
shiro.sessionManager.deleteInvalidSessions | true | Remove invalid session from session storage |
shiro.sessionManager.sessionIdCookieEnabled | true | Enable session ID to cookie, for session tracking |
shiro.sessionManager.sessionIdUrlRewritingEnabled | true | Enable session URL rewriting support |
shiro.userNativeSessionManager | false | If enabled Shiro will manage the HTTP sessions instead of the container |
shiro.sessionManager.cookie.name | JSESSIONID | Session cookie name |
shiro.sessionManager.cookie.maxAge | -1 | Session cookie max age |
shiro.sessionManager.cookie.domain | null | Session cookie domain |
shiro.sessionManager.cookie.path | null | Session cookie path |
shiro.sessionManager.cookie.secure | false | Session cookie secure flag |
shiro.rememberMeManager.cookie.name | rememberMe | RememberMe cookie name |
shiro.rememberMeManager.cookie.maxAge | one year | RememberMe cookie max age |
shiro.rememberMeManager.cookie.domain | null | RememberMe cookie domain |
shiro.rememberMeManager.cookie.path | null | RememberMe cookie path |
shiro.rememberMeManager.cookie.secure | false | RememberMe cookie secure flag |
shiro.loginUrl | / | login.jsp Login URL used when unauthenticated users are redirected to login page |
shiro.successUrl | / | Default landing page after a user logs in (if alternative cannot be found in the current session) |
shiro.unauthorizedUrl | null | Page to redirect user to if they are unauthorized (403 page) |
補(bǔ)充:不使用 Starter 整合(了解永票、自學(xué))
<!-- 自動(dòng)依賴導(dǎo)入 shiro-core 和 shiro-web -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
將 Shiro 的配置信息<small>(spring-shiro.xml 和 spring-web.xml)</small>以 Java 代碼配置的形式改寫:
@Configuration
public class ShiroConfig {
@Bean
public Realm realm() {
SimpleAccountRealm realm = new SimpleAccountRealm();
realm.addAccount("tom", "123", "admin", "user");
return realm;
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shirFilter() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
shiroFilterFactoryBean.setLoginUrl("/login-page");
shiroFilterFactoryBean.setUnauthorizedUrl("/403-page");
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/login-page", "anon");
filterChainDefinitionMap.put("/403-page", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/hello", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/* ################################################################# */
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 強(qiáng)制指定注解的底層實(shí)現(xiàn)使用 cglib 方案
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}