二、SpringBoot應用整合Shiro
-
JavaSE應用中使用
-
web應用中使用
-
SSM整合Shiro(配置多,用的少)
-
SpringBoot應用整合Shiro
-
2.1 創建SpringBoot應用
-
lombok
-
spring web
-
thymeleaf
2.2 整合Druid和MyBatis
-
依賴
-
<!-- druid starter --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!--mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <!-- mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency>
- 配置
-
spring: datasource: druid: url: jdbc:mysql://47.96.11.185:3306/test # MySQL如果是8.x com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver username: root password: admin123 initial-size: 1 min-idle: 1 max-active: 20 mybatis: mapper-locations: classpath:mappers/*Mapper.xml type-aliases-package: com.qfedu.springbootssm.beans
-
2.3 整合Shiro
-
-
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.1</version> </dependency>
-
-
@Configuration public class ShiroConfig { @Bean public JdbcRealm getJdbcRealm(DataSource dataSource){ JdbcRealm jdbcRealm = new JdbcRealm(); //JdbcRealm會自行從數據庫查詢用戶及權限數據(數據庫的表結構要符合JdbcRealm的規范) jdbcRealm.setDataSource(dataSource); //JdbcRealm默認開啟認證功能,需要手動開啟授權功能 jdbcRealm.setPermissionsLookupEnabled(true); return jdbcRealm; } @Bean public DefaultWebSecurityManager getDefaultWebSecurityManager(JdbcRealm jdbcRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(jdbcRealm); return securityManager; } @Bean public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager){ ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean(); //過濾器就是shiro就行權限校驗的核心,進行認證和授權是需要SecurityManager的 filter.setSecurityManager(securityManager); Map<String,String> filterMap = new HashMap<>(); filterMap.put("/","anon"); filterMap.put("/login.html","anon"); filterMap.put("/regist.html","anon"); filterMap.put("/user/login","anon"); filterMap.put("/user/regist","anon"); filterMap.put("/static/**","anon"); filterMap.put("/**","authc"); filter.setFilterChainDefinitionMap(filterMap); filter.setLoginUrl("/login.html"); //設置未授權訪問的頁面路徑 filter.setUnauthorizedUrl("/login.html"); return filter; } }
-
@Service public class UserServiceImpl { public void checkLogin(String userName,String userPwd) throws Exception{ Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(userName,userPwd); subject.login(token); } }
UserController.java
-
@Controller @RequestMapping("user") public class UserController { @Resource private UserServiceImpl userService; @RequestMapping("login") public String login(String userName,String userPwd){ try { userService.checkLogin(userName,userPwd); System.out.println("------登錄成功!"); return "index"; } catch (Exception e) { System.out.println("------登錄失敗!"); return "login"; } } }
-
- 自定義Realm
- 自定義Realm
- 自定義Real
- 自定義Realm
/** * 1.創建一個類繼承AuthorizingRealm類(實現了Realm接口的類) * 2.重寫doGetAuthorizationInfo和doGetAuthenticationInfo方法 * 3.重寫getName方法返回當前realm的一個自定義名稱 */ public class MyRealm extends AuthorizingRealm { @Resource private UserDAO userDAO; @Resource private RoleDAO roleDAO; @Resource private PermissionDAO permissionDAO; public String getName() { return "myRealm"; } /** * 獲取授權數據(將當前用戶的角色及權限信息查詢出來) */ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //獲取用戶的用戶名 String username = (String) principalCollection.iterator().next(); //根據用戶名查詢當前用戶的角色列表 Set<String> roleNames = roleDAO.queryRoleNamesByUsername(username); //根據用戶名查詢當前用戶的權限列表 Set<String> ps = permissionDAO.queryPermissionsByUsername(username); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setRoles(roleNames); info.setStringPermissions(ps); return info; } /** * 獲取認證的安全數據(從數據庫查詢的用戶的正確數據) */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //參數authenticationToken就是傳遞的 subject.login(token) // 從token中獲取用戶名 UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); //根據用戶名,從數據庫查詢當前用戶的安全數據 User user = userDAO.queryUserByUsername(username); AuthenticationInfo info = new SimpleAuthenticationInfo( username, //當前用戶用戶名 user.getUserPwd(), //從數據庫查詢出來的安全密碼 getName()); return info; } }
- 自定義Realm
-
二、加密
-
明文-----(加密規則)-----密文
-
加密規則可以自定義,在項目開發中我們通常使用BASE64和MD5編碼方式
-
BASE64:可反編碼的編碼方式(對稱)
-
明文----密文
-
密文----明文
-
-
MD5: 不可逆的編碼方式(非對稱)
-
明文----密文
-
-
-
如果數據庫用戶的密碼存儲的密文,Shiro該如何完成驗證呢?
-
使用Shiro提供的加密功能,對輸入的密碼進行加密之后再進行認證。
2.1 加密介紹
-
-
-
2.2 Shiro使用加密認證
-
-
@Configuration public class ShiroConfig { //... @Bean public HashedCredentialsMatcher getHashedCredentialsMatcher(){ HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); //matcher就是用來指定加密規則 //加密方式 matcher.setHashAlgorithmName("md5"); //hash次數 matcher.setHashIterations(1); //此處的循環次數要與用戶注冊是密碼加密次數一致 return matcher; } //自定義Realm @Bean public MyRealm getMyRealm( HashedCredentialsMatcher matcher ){ MyRealm myRealm = new MyRealm(); myRealm.setCredentialsMatcher(matcher); return myRealm; } //... }
2.3 用戶注冊密碼加密處理
-
-
<form action="/user/regist" method="post"> <p>帳號:<input type="text" name="userName"/></p> <p>密碼:<input type="text" name="userPwd"/></p> <p><input type="submit" value="提交注冊"/></p> </form>
UserController
-
@Controller @RequestMapping("user") public class UserController { @Resource private UserServiceImpl userService; @RequestMapping("/regist") public String regist(String userName,String userPwd) { System.out.println("------注冊"); //注冊的時候要對密碼進行加密存儲 Md5Hash md5Hash = new Md5Hash(userPwd); System.out.println("--->>>"+ md5Hash.toHex()); //加鹽加密 int num = new Random().nextInt(90000)+10000; //10000—99999 String salt = num+""; Md5Hash md5Hash2 = new Md5Hash(userPwd,salt); System.out.println("--->>>"+md5Hash2); //加鹽加密+多次hash Md5Hash md5Hash3 = new Md5Hash(userPwd,salt,3); System.out.println("--->>>"+md5Hash3); //SimpleHash hash = new SimpleHash("md5",userPwd,num,3); //將用戶信息保存到數據庫時,保存加密后的密碼,如果生成的隨機鹽,鹽也要保存 return "login"; } }
2.4 如果密碼進行了加鹽處理,則Realm在返回認證數據時需要返回鹽
-
在自定義Realm中:
-
-
-
public class MyRealm extends AuthorizingRealm { @Resource private UserDAO userDAO; @Resource private RoleDAO roleDAO; @Resource private PermissionDAO permissionDAO; public String getName() { return "myRealm"; } /** * 獲取認證的安全數據(從數據庫查詢的用戶的正確數據) */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //參數authenticationToken就是傳遞的 subject.login(token) // 從token中獲取用戶名 UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); //根據用戶名,從數據庫查詢當前用戶的安全數據 User user = userDAO.queryUserByUsername(username); // AuthenticationInfo info = new SimpleAuthenticationInfo( // username, //當前用戶用戶名 // user.getUserPwd(), //從數據庫查詢出來的安全密碼 // getName()); //如果數據庫中用戶的密碼是加了鹽的 AuthenticationInfo info = new SimpleAuthenticationInfo( username, //當前用戶用戶名 user.getUserPwd(), //從數據庫查詢出來的安全密碼 ByteSource.Util.bytes(user.getPwdSalt()), getName()); return info; } }
三、退出登錄
-
-
filterMap.put("/exit","logout");
在頁面的“退出”按鈕上,跳轉到logout對應的url
-
-
<a href="exit">退出</a>
四、授權
用戶登錄成功之后,要進行響應的操作就需要有對應的權限;在進行操作之前對權限進行檢查—授權
權限控制通常有兩類做法:
-
不同身份的用戶登錄,我們現在不同的操作菜單(沒有權限的菜單不現實)
-
對所有用戶顯示所有菜單,當用戶點擊菜單以后再驗證當前用戶是否有此權限,如果沒有則提示權限不足
4.1 HTML授權
-
在菜單頁面只顯示當前用戶擁有權限操作的菜單
-
shiro標簽
-
-
<shiro:hasPermission name="sys:c:save"> <dd><a href="javascript:;">入庫</a></dd> </shiro:hasPermission>
4.2 過濾器授權
-
-
filterMap.put("/c_add.html","perms[sys:c:save]"); //設置未授權訪問的頁面路徑—當權限不足時顯示此頁面 filter.setUnauthorizedUrl("/lesspermission.html");
-
-
4.3 注解授權
-
配置Spring對Shiro注解的支持:ShiroConfig.java
-
@Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); autoProxyCreator.setProxyTargetClass(true); return autoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor( DefaultWebSecurityManager securityManager){ AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; }
- 在請求的控制器添加權限注解
-
@Controller @RequestMapping("customer") public class CustomerController { @RequestMapping("list") //如果沒有 sys:k:find 權限,則不允許執行此方法 @RequiresPermissions("sys:k:find") // @RequiresRoles("") public String list(){ System.out.println("----------->查詢客戶信息"); return "customer_list"; } }
- 通過全局異常處理,指定權限不足時的頁面跳轉
-
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler public String doException(Exception e){ if(e instanceof AuthorizationException){ return "lesspermission"; } return null; } }
4.4 手動授權
-
在代碼中進行手動的權限校驗
-
Subject subject = SecurityUtils.getSubject(); if(subject.isPermitted("sys:k:find")){ System.out.println("----------->查詢客戶信息"); return "customer_list"; }else{ return "lesspermission"; }
-
-
-
-
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.4.0</version> </dependency>
5.2 配置緩存策略
-
-
<?xml version="1.0" encoding="UTF-8"?> <ehcache updateCheck="false" dynamicConfig="false"> <diskStore path="C:\TEMP" /> <cache name="users" timeToLiveSeconds="300" maxEntriesLocalHeap="1000"/> <defaultCache name="defaultCache" maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="false" maxElementsOnDisk="100000" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"/> <!--緩存淘汰策略:當緩存空間比較緊張時,我們要存儲新的數據進來,就必然要刪除一些老的數據 LRU 最近最少使用 FIFO 先進先出 LFU 最少使用 --> </ehcache>
5.3 加入緩存管理
-
-
@Bean public EhCacheManager getEhCacheManager(){ EhCacheManager ehCacheManager = new EhCacheManager(); ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml"); return ehCacheManager; } @Bean public DefaultWebSecurityManager getDefaultWebSecurityManager(MyRealm myRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myRealm); securityManager.setCacheManager(getEhCacheManager()); return securityManager; }
-
-
-
- Shiro進行認證和授權是基於session實現的,Shiro包含了對session的管理
-
-
自定義session管理器
-
將自定義的session管理器設置給SecurityManager
-
-
@Bean public DefaultWebSessionManager getDefaultWebSessionManager(){ DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); System.out.println("----------"+sessionManager.getGlobalSessionTimeout()); // 1800000 //配置sessionManager sessionManager.setGlobalSessionTimeout(5*60*1000); return sessionManager; } @Bean public DefaultWebSecurityManager getDefaultWebSecurityManager(MyRealm myRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myRealm); securityManager.setCacheManager(getEhCacheManager()); securityManager.setSessionManager(getDefaultWebSessionManager()); return securityManager; }
-
-
-
-
未認證—可訪問的頁面—(陌生人)—問候
-
login.html、regist.html
-
記住我—可訪問的頁面—(前女友)—朋友間的擁抱
-
info.html
-
已認證—可訪問的頁面—(現女友)—牽手
-
-
-
-
// anon 表示未認證可訪問的url // user 表示記住我可訪問的url(已認證也可以訪問) //authc 表示已認證可訪問的url //perms 表示必須具備指定的權限才可訪問 //logout 表示指定退出的url filterMap.put("/","anon"); filterMap.put("/index.html","user"); filterMap.put("/login.html","anon"); filterMap.put("/regist.html","anon"); filterMap.put("/user/login","anon"); filterMap.put("/user/regist","anon"); filterMap.put("/layui/**","anon"); filterMap.put("/**","authc"); filterMap.put("/c_add.html","perms[sys:c:save]"); filterMap.put("/exit","logout");
-
-
@Bean public CookieRememberMeManager cookieRememberMeManager(){ CookieRememberMeManager rememberMeManager = new CookieRememberMeManager(); //cookie必須設置name SimpleCookie cookie = new SimpleCookie("rememberMe"); cookie.setMaxAge(30*24*60*60); rememberMeManager.setCookie(cookie); return rememberMeManager; } @Bean public DefaultWebSecurityManager getDefaultWebSecurityManager(MyRealm myRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myRealm); securityManager.setCacheManager(getEhCacheManager()); securityManager.setSessionManager(getDefaultWebSessionManager()); //設置remember管理器 securityManager.setRememberMeManager(cookieRememberMeManager()); return securityManager; }
-
2.3 登錄認證時設置token“記住我”
-
-
<form action="/user/login" method="post"> <p>帳號:<input type="text" name="userName"/></p> <p>密碼:<input type="text" name="userPwd"/></p> <p>記住我:<input type="checkbox" name="rememberMe"/></p> <p><input type="submit" value="登錄"/></p> </form>
- 控制器
-
@Controller @RequestMapping("user") public class UserController { @Resource private UserServiceImpl userService; @RequestMapping("login") public String login(String userName,String userPwd,boolean rememberMe){ try { userService.checkLogin(userName,userPwd,rememberMe); System.out.println("------登錄成功!"); return "index"; } catch (Exception e) { System.out.println("------登錄失敗!"); return "login"; } } //... }
- service
-
@Service public class UserServiceImpl { public void checkLogin(String userName, String userPwd,boolean rememberMe) throws Exception { //Shiro進行認證 ——入口 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(userName,userPwd); token.setRememberMe(rememberMe); subject.login(token); } }
-
三、Shiro多Realm配置
-
當shiro進行權限管理,數據來自於不同的數據源時,我們可以給SecurityManager配置多個Realm
-
3.2 多個Realm的處理方式
3.2.1 鏈式處理
-
多個Realm依次進行認證
3.2.2 分支處理
-
根據不同的條件從多個Realm中選擇一個進行認證處理
3.3 多Realm配置(鏈式處理)
-
定義多個Realm
-
UserRealm
-
public class UserRealm extends AuthorizingRealm { Logger logger = LoggerFactory.getLogger(UserRealm.class); @Override public String getName() { return "UserRealm"; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { logger.info("--------------------------------UserRealm"); //從token中獲取username UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); //根據username從users表中查詢用戶信息 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,"123456",getName()); return info; } }
- ManagerRealm
-
public class ManagerRealm extends AuthorizingRealm { Logger logger = LoggerFactory.getLogger(ManagerRealm.class); @Override public String getName() { return "ManagerRealm"; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { logger.info("--------------------------------ManagerRealm"); //從token中獲取username UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String username = token.getUsername(); //根據username從嗎managers表中查詢用戶信息 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,"222222",getName()); return info; } }
- 在ShiroConfig.java中為SecurityManager配置多個Realm
-
@Configuration public class ShiroConfig { @Bean public UserRealm userRealm(){ UserRealm userRealm = new UserRealm(); return userRealm; } @Bean public ManagerRealm managerRealm(){ ManagerRealm managerRealm = new ManagerRealm(); return managerRealm; } @Bean public DefaultWebSecurityManager getDefaultWebSecurityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //securityManager中配置多個realm Collection<Realm> realms = new ArrayList<>();
realms.add(userRealm()); realms.add(managerRealm());
securityManager.setRealms(realms); return securityManager; } //... }
-
-
-
-
- 根據不同的條件執行不同的Realm
-
-
自定義Realm(UserRealm\ManagerRealm)
-
當在登錄頁面選擇“普通用戶”登錄,則執行UserRealm的認證
-
當在登錄頁面選擇“管理員”登錄,則執行ManagerRealm的認證
-
-
Realm的聲明及配置
-
-
public class MyToken extends UsernamePasswordToken { private String loginType; public MyToken(String userName,String userPwd, String loginType) { super(userName,userPwd); this.loginType = loginType; } public String getLoginType() { return loginType; } public void setLoginType(String loginType) { this.loginType = loginType; } }
- 自定義認證器
-
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator { Logger logger = LoggerFactory.getLogger(MyModularRealmAuthenticator.class); @Override protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { logger.info("------------------------------MyModularRealmAuthenticator"); this.assertRealmsConfigured(); Collection<Realm> realms = this.getRealms(); MyToken token = (MyToken) authenticationToken; String loginType = token.getLoginType(); // User logger.info("------------------------------loginType:"+loginType); Collection<Realm> typeRealms = new ArrayList<>(); for(Realm realm:realms){ if(realm.getName().startsWith(loginType)){ //UserRealm typeRealms.add(realm); } } if(typeRealms.size()==1){ return this.doSingleRealmAuthentication((Realm)typeRealms.iterator().next(), authenticationToken); }else{ return this.doMultiRealmAuthentication(typeRealms, authenticationToken); } } }
- 配置自定義認證器
-
@Configuration public class ShiroConfig { @Bean public UserRealm userRealm(){ UserRealm userRealm = new UserRealm(); return userRealm; } @Bean public ManagerRealm managerRealm(){ ManagerRealm managerRealm = new ManagerRealm(); return managerRealm; } @Bean public MyModularRealmAuthenticator myModularRealmAuthenticator(){ MyModularRealmAuthenticator myModularRealmAuthenticator = new MyModularRealmAuthenticator(); return myModularRealmAuthenticator; } @Bean public DefaultWebSecurityManager getDefaultWebSecurityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //配置自定義認證器(放在realms設置之前) securityManager.setAuthenticator(myModularRealmAuthenticator()); //securityManager中配置多個realm Collection<Realm> realms = new ArrayList<>(); realms.add(userRealm()); realms.add(managerRealm()); securityManager.setRealms(realms); return securityManager; } //... }
-
-
-
<form action="user/login" method="post"> <p>帳號:<input type="text" name="userName"/></p> <p>密碼:<input type="text" name="userPwd"/></p> <p><input type="radio" name="loginType" value="User" checked/>普通用戶 <input type="radio" name="loginType" value="Manager"/>管理員</p> <p><input type="submit" value="登錄"/></p> </form>
- UserController.java
-
@Controller @RequestMapping("user") public class UserController { Logger logger = LoggerFactory.getLogger(UserController.class); @RequestMapping("login") public String login(String userName,String userPwd, String loginType){ logger.info("~~~~~~~~~~~~~UserController-login"); try{ //UsernamePasswordToken token = new UsernamePasswordToken(userName,userPwd); MyToken token = new MyToken(userName,userPwd,loginType); Subject subject = SecurityUtils.getSubject(); subject.login(token); return "index"; }catch (Exception e){ return "login"; } } }
-
-
-