spring-boot 整合 shiro 自定義密碼驗證 自定義freemarker標籤根據許可權渲染不同頁面
專案裡一直用的是 spring-security ,不得不說,spring-security 真是東西太多了,學習難度太大(可能我比較菜),這篇部落格來總結一下折騰shiro的成果,分享給大家,強烈推薦shiro,真心簡單 : )
引入依賴
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
使用者,角色,許可權
就是經典的RBAC許可權系統,下面簡單給一下實體類欄位
AdminUser.java
public class AdminUser implements Serializable { private static final long serialVersionUID = 8264158018518861440L; private Integer id; private String username; private String password; private Integer roleId; // getter setter... }
Role.java
public class Role implements Serializable { private static final long serialVersionUID = 7824693669858106664L; private Integer id; private String name; // getter setter... }
Permission.java
public class Permission implements Serializable { private static final long serialVersionUID = -2694960432845360318L; private Integer id; private String name; private String value; // 許可權的父節點的id private Integer pid; // getter setter... }
自定義Realm
這貨就是查詢使用者的資訊然後放在shiro的個人使用者物件的快取裡,shiro自己有一個session的物件(不是servlet裡的session)作用就是後面使用者發起請求的時候拿來判斷有沒有許可權
另一個作用是查詢一下使用者的資訊,將使用者名稱,密碼組裝成一個AuthenticationInfo
用於後面密碼校驗的
具體程式碼如下
MyShiroRealm.java
@Component public class MyShiroRealm extends AuthorizingRealm { private Logger log = LoggerFactory.getLogger(MyShiroRealm.class); @Autowired private AdminUserService adminUserService; @Autowired private RoleService roleService; @Autowired private PermissionService permissionService; // 使用者許可權配置 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //訪問@RequirePermission註解的url時觸發 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); AdminUser adminUser = adminUserService.selectByUsername(principals.toString()); //獲得使用者的角色,及許可權進行繫結 Role role = roleService.selectById(adminUser.getRoleId()); // 其實這裡也可以不要許可權那個類了,直接用角色這個類來做鑑權, // 不過角色包含很多的許可權,已經算是大家約定的了,所以下面還是查詢許可權然後放在AuthorizationInfo裡 simpleAuthorizationInfo.addRole(role.getName()); // 查詢許可權 List<Permission> permissions = permissionService.selectByRoleId(adminUser.getRoleId()); // 將許可權具體值取出來組裝成一個許可權String的集合 List<String> permissionValues = permissions.stream().map(Permission::getValue).collect(Collectors.toList()); // 將許可權的String集合新增進AuthorizationInfo裡,後面請求鑑權有用 simpleAuthorizationInfo.addStringPermissions(permissionValues); return simpleAuthorizationInfo; } // 組裝使用者資訊 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); log.info("使用者:{} 正在登入...", username); AdminUser adminUser = adminUserService.selectByUsername(username); // 如果使用者不存在,則丟擲未知使用者的異常 if (adminUser == null) throw new UnknownAccountException(); return new SimpleAuthenticationInfo(username, adminUser.getPassword(), getName()); } }
實現密碼校驗
shiro內建了幾個密碼校驗的類,有Md5CredentialsMatcher
Sha1CredentialsMatcher
, 不過從1.1版本開始,都開始使用HashedCredentialsMatcher
這個類了,通過配置加密規則來校驗
它們都實現了一個介面CredentialsMatcher
我這裡也實現這個介面,實現一個自己的密碼校驗
說明一下,我這裡用的加密方式是Spring-Security裡的BCryptPasswordEncoder
作的加密,之所以用它,是因為同一個密碼被這貨加密後,密文都不一樣,下面是具體程式碼
public class MyCredentialsMatcher implements CredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { // 大坑!!!!!!!!!!!!!!!!!!! // 明明token跟info兩個物件的裡的Credentials型別都是Object,斷點看到的型別都是 char[] // 但是!!!!! token裡轉成String要先強轉成 char[] // 而info裡取Credentials就可以直接使用 String.valueOf() 轉成String // 醉了。。 String rawPassword = String.valueOf((char[]) token.getCredentials()); String encodedPassword = String.valueOf(info.getCredentials()); return new BCryptPasswordEncoder().matches(rawPassword, encodedPassword); } }
配置shiro
因為專案是spring-boot開發的,shiro就用java程式碼配置,不用xml配置, 具體配置如下
@Configuration public class ShiroConfig { private Logger log = LoggerFactory.getLogger(ShiroConfig.class); @Autowired private MyShiroRealm myShiroRealm; @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { log.info("開始配置shiroFilter..."); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //攔截器. Map<String,String> map = new HashMap<>(); // 配置不會被攔截的連結 順序判斷相關靜態資源 map.put("/static/**", "anon"); //配置退出 過濾器,其中的具體的退出程式碼Shiro已經替我們實現了 map.put("/admin/logout", "logout"); //<!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最為下邊 -->:這是一個坑呢,一不小心程式碼就不好使了; //<!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問--> map.put("/admin/**", "authc"); // 如果不設定預設會自動尋找Web工程根目錄下的"/login.jsp"頁面 shiroFilterFactoryBean.setLoginUrl("/adminlogin"); // 登入成功後要跳轉的連結 shiroFilterFactoryBean.setSuccessUrl("/admin/index"); //未授權介面; shiroFilterFactoryBean.setUnauthorizedUrl("/error"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); return shiroFilterFactoryBean; } // 配置加密方式 // 配置了一下,這貨就是驗證不過,,改成手動驗證算了,以後換加密方式也方便 @Bean public MyCredentialsMatcher myCredentialsMatcher() { return new MyCredentialsMatcher(); } // 安全管理器配置 @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); myShiroRealm.setCredentialsMatcher(myCredentialsMatcher()); securityManager.setRealm(myShiroRealm); return securityManager; } }
登入
都配置好了,就可以發起登入請求做測試了,一個簡單的表單即可,寫在Controller裡就行
@PostMapping("/adminlogin") public String adminLogin(String username, String password, @RequestParam(defaultValue = "0") Boolean rememberMe, RedirectAttributes redirectAttributes) { try { // 新增使用者認證資訊 Subject subject = SecurityUtils.getSubject(); if (!subject.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe); //進行驗證,這裡可以捕獲異常,然後返回對應資訊 subject.login(token); } } catch (AuthenticationException e) { // e.printStackTrace(); log.error(e.getMessage()); redirectAttributes.addFlashAttribute("error", "使用者名稱或密碼錯誤"); redirectAttributes.addFlashAttribute("username", username); return redirect("/adminlogin"); } return redirect("/admin/index"); }
從上面程式碼可以看出,記住我功能也直接都實現好了,只需要在組裝UsernamePasswordToken
的時候,將記住我欄位傳進去就可以了,值是 true, false, 如果是true,登入成功後,shiro會在本地寫一個cookie
呼叫subject.login(token);
方法後,它會去鑑權,期間會產生各種各樣的異常,有以下幾種,可以通過捕捉不同的異常然後提示頁面不同的錯誤資訊,相當的方便呀,有木有
AuthenticationToken
上面這麼多異常,shiro在處理登入的邏輯時,會自動的發出一些異常,當然你也可以手動去處理登入流程,然後根據不同的問題丟擲不同的異常,手動處理的地方就在自己寫的MyShiroRealm
裡的doGetAuthenticationInfo()
方法裡,我在上面程式碼裡只處理了一個帳戶不存在時丟擲了一個UnknownAccountException
的異常,其實還可以加更多其它的異常,這個要看個人系統的需求來定了
到這裡已經可以正常的實現登入了,下面來說一些其它相關的功能的實現
自定freemarker標籤
開發專案肯定要用到頁面模板,我這裡用的是 freemarker ,一個使用者登入後,頁面可能要根據使用者的不同許可權渲染不同的選單,github上有個開源的庫,也是可以用的,不過我覺得那個太麻煩了,就自己實現了一個,幾行程式碼就能搞定
ShiroTag.java
@Component public class ShiroTag { // 判斷當前使用者是否已經登入認證過 public boolean isAuthenticated(){ return SecurityUtils.getSubject().isAuthenticated(); } // 獲取當前使用者的使用者名稱 public String getPrincipal() { return (String) SecurityUtils.getSubject().getPrincipal(); } // 判斷使用者是否有 xx 角色 public boolean hasRole(String name) { return SecurityUtils.getSubject().hasRole(name); } // 判斷使用者是否有 xx 許可權 public boolean hasPermission(String name) { return !StringUtils.isEmpty(name) && SecurityUtils.getSubject().isPermitted(name); } }
將這個類註冊到freemarker的全域性變數裡
FreemarkerConfig.java
@Configuration public class FreemarkerConfig { private Logger log = LoggerFactory.getLogger(FreeMarkerConfig.class); @Autowired private ShiroTag shiroTag; @PostConstruct public void setSharedVariable() throws TemplateModelException { //注入全域性配置到freemarker log.info("開始配置freemarker全域性變數..."); // shiro鑑權 configuration.setSharedVariable("sec", shiroTag); log.info("freemarker自定義標籤配置完成!"); } }
有了這些配置後,就可以在頁面裡使用了,具體用法如下
<#if sec.hasPermission("topic:list")> <li <#if page_tab=='topic'>class="active"</#if>> <a href="/admin/topic/list"> <i class="fa fa-list"></i> <span>話題列表</span> </a> </li> </#if>
加上這個後,在渲染頁面的時候,就會根據當前使用者是否有檢視話題列表的許可權,然後來渲染這個選單
註解許可權
有了上面freemarker標籤判斷是否有許可權來渲染頁面,這樣做只能防君子,不能防小人,如果一個人知道後臺的某個訪問連結,但這個連結它是沒有許可權訪問的,那他只要手動輸入這個連結就還是可以訪問的,所以這裡還要在Controller層加一套防禦,具體配置如下
在ShiroConfig
里加上兩個Bean
//加入註解的使用,不加入這個註解不生效 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } @Bean @ConditionalOnMissingBean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator(); defaultAAP.setProxyTargetClass(true); return defaultAAP; }
有了這兩個Bean就可以用shiro的註解鑑權了,用法如下@RequiresPermissions("topic:list")
@Controller @RequestMapping("/admin/topic") public class TopicAdminController extends BaseAdminController { @RequiresPermissions("topic:list") @GetMapping("/list") public String list() { // TODO return "admin/topic/list"; } }
shiro除了@RequiresPermissions
註解外,還有其它幾個鑑權的註解
- @RequiresPermissions
- @RequiresRoles
- @RequiresUser
- @RequiresGuest
- @RequiresAuthentication
一般@RequiresPermissions
就夠用了
總結
spring-boot 整合 shiro 到這就結束了,是不是網上能找到的教程裡最全的!相比 spring-security 要簡單太多了,強烈推薦
希望這篇部落格能幫到正在折騰 shiro 的你
原文連結: