SpringBoot【六】 Shiro
Shiro
- Apache Shiro 是一個 java 的安全(許可權)框架
- 可以容易的開發出足夠好的應用,不僅可以用在 JavaSE 環境,也可以用在 JavaEE 環境
- 可以完成認證、授權、加密、會話管理、Web 整合、快取等
功能
Shiro 架構
外部
從外部來看 Shiro,即從應用程式角度來觀察如何使用 Shrio 完成工作
- Subject 使用者
- SecurityManager 管理所有使用者
- Realm 連線資料
內部
快速入門
參考:https://github.com/apache/shiro/tree/master/samples/quickstart
-
匯入依賴
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.3</version> </dependency> <!-- configure logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.21</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.21</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
-
配置檔案
log4j.properties
log4j.rootLogger=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n # General Apache libraries log4j.logger.org.apache=WARN # Spring log4j.logger.org.springframework=WARN # Default Shiro logging log4j.logger.org.apache.shiro=INFO # Disable verbose logging log4j.logger.org.apache.shiro.util.ThreadContext=WARN log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
shiro.ini(IDEA 中要先加入 Ini 外掛)
[users] # user 'root' with password 'secret' and the 'admin' role root = secret, admin # user 'guest' with the password 'guest' and the 'guest' role guest = guest, guest # user 'presidentskroob' with password '12345' ("That's the same combination on # my luggage!!!" ;)), and role 'president' presidentskroob = 12345, president # user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz' darkhelmet = ludicrousspeed, darklord, schwartz # user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz' lonestarr = vespa, goodguy, schwartz # ----------------------------------------------------------------------------- # Roles with assigned permissions # # Each line conforms to the format defined in the # org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc # ----------------------------------------------------------------------------- [roles] # 'admin' role has all permissions, indicated by the wildcard '*' admin = * # The 'schwartz' role can do anything (*) with any lightsaber: schwartz = lightsaber:* # The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with # license plate 'eagle5' (instance specific id) goodguy = winnebago:drive:eagle5
-
QuickStart.class 分析
一些常用方法:
// 獲取當前使用者物件 Subject currentUser = SecurityUtils.getSubject(); // 根據當前使用者拿到 session Session session = currentUser.getSession(); // 判斷當前使用者是否被認證 currentUser.isAuthenticated() currentUser.getPrincipal() currentUser.hasRole("schwartz") currentUser.isPermitted("lightsaber:wield") // 登出 currentUser.logout();
SpringBoot 整合 Shiro
環境搭建
-
新建 SpringBoot 專案,新增 web、thymeleaf 依賴
-
編寫 Controller
@Controller public class MyController { @RequestMapping({"/","/index"}) public String toIndex(Model model){ model.addAttribute("msg", "hello"); return "index"; } }
-
前端頁面
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>首頁</h1> <p th:text="${msg}"></p> </body> </html>
-
測試,環境 OK!
Shiro 核心配置
-
匯入 shrio 整合 Spring 的依賴
<!--匯入 shiro 整合 Spring --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.3</version> </dependency>
-
編寫 Shiro 的配置類 ShiroConfig.class,配置三個 Bean,即三大核心要素(對應三大核心物件):
- ShiroFilterFactoryBean:過濾物件【第 3 步:連到前端】-------> Subject 使用者
- DefaultWebSecurityManager:安全物件【第 2 步:接管】--------> SecurityManager 管理所有使用者
- Realm 物件,需要自定義【第 1 步:建立物件】-------> Realm 連線資料
@Configuration public class ShiroConfig { // ShiroFilterFactoryBean:3 // DefaultWebSecurityManager:2 // 建立 realm 物件,需要自定義:1 }
-
自定義 Realm 物件,需要繼承 AuthorizingRealm,重寫兩個方法:認證和授權
// 自定義的realm extends AuthorizingRealm public class UserRealm extends AuthorizingRealm{ // 授權 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("執行了 授權 doGetAuthorizationInfo"); return null; } // 認證 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("執行了 認證 doGetAuthenticationInfo"); return null; } }
-
將自定義的 Realm 物件注入 Bean 中
// 建立 realm 物件,需要自定義:1 @Bean public UserRealm userRealm(){ return new UserRealm(); }
-
建立 DefaultWebSecurityManager 物件並注入 Bean 中,需要關聯 Realm 物件(通過傳參實現),因為它要對 Realm 物件進行管理
// DefaultWebSecurityManager:2 @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 關聯 userRealm securityManager.setRealm(userRealm); return securityManager; }
-
建立 ShiroFilterFactoryBean 物件並注入 Bean 中,需要關聯 securityManager(通過傳參)
// ShiroFilterFactoryBean:3 @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 設定安全管理器 bean.setSecurityManager(defaultWebSecurityManager); return bean; }
登入攔截
-
編寫兩個前端頁面 /user/add 和 /user/update
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>add</h1> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>update</h1> </body> </html>
-
Controller 跳轉
@RequestMapping("/user/add") public String add(){ return "user/add"; } @RequestMapping("/user/update") public String update(){ return "user/update"; }
-
首頁加入兩個跳轉的超連結
<a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a>
-
測試,點選首頁的 add 和 update,兩個頁面都可以進去
-
增加需求:對於某個頁面,有些使用者可以訪問,有些使用者不可以訪問
-
在 ShiroFilterFactoryBean 物件,新增 Shiro 的內建過濾器
anon:無需認證就可以訪問 authc:必須認證才可以訪問 user:必須擁有 記住我 功能才能用 perms: 擁有對某個資源的許可權才能訪問 role:擁有某個角色的許可權才能訪問
Map<String, String> filterMap = new LinkedHashMap<>(); // 登入攔截 filterMap.put("/user/add", "authc"); filterMap.put("/user/update", "authc"); // filterMap.put("/user/*", "authc"); // 總和上面兩個的作用,*為萬用字元 bean.setFilterChainDefinitionMap(filterMap);
Shiro 的內建過濾器原始碼: 執行 Web 應用時,Shiro會建立一些有用的預設 Filter 例項,並自動地在 [main] 項中將它們置為可用, 這些可用的預設的 Filter 例項是被 DefaultFilter 列舉類定義的(列舉的名稱欄位就是可供配置的名稱) public enum DefaultFilter { anon(AnonymousFilter.class), authc(FormAuthenticationFilter.class), authcBasic(BasicHttpAuthenticationFilter.class), authcBearer(BearerHttpAuthenticationFilter.class), logout(LogoutFilter.class), noSessionCreation(NoSessionCreationFilter.class), perms(PermissionsAuthorizationFilter.class), port(PortFilter.class), rest(HttpMethodPermissionFilter.class), roles(RolesAuthorizationFilter.class), ssl(SslFilter.class), user(UserFilter.class); }
-
測試,點選 add 和 update 都會跳轉到錯誤頁碼,證明攔截成功
-
想要攔截之後跳轉到登入頁面,需要先編寫一個登入頁面
<h1>登入</h1> <p th:text="${msg}" style="color:red;"></p> <form th:action="@{/login}"> <p>使用者名稱:<input type="text" name="username"></p> <p>密碼:<input type="text" name="password"></p> <p><input type="submit"></p> </form>
-
對應的需要在 Controller 中進行跳轉
@RequestMapping("/toLogin") public String toLogin(){ return "login"; }
-
在 ShiroFilterFactoryBean 中配置,如果沒有許可權,讓其跳轉到登入頁面
// 設定登入的請求 bean.setLoginUrl("/toLogin");
-
測試成功,點選 add 和 update 都會被攔截,而且跳轉到自己編寫的登入頁面
登入攔截的 ShiroFilterFactoryBean 中配置總結
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 設定安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
// 登入攔截
Map<String, String> filterMap = new LinkedHashMap<>();
// 新增 shiro 的內建過濾器
filterMap.put("/user/*", "authc");
bean.setFilterChainDefinitionMap(filterMap);
// 設定登入的請求
bean.setLoginUrl("/toLogin");
return bean;
}
使用者認證
使用者的認證和授權在 Realm 物件中進行設定,然後和其他兩個核心物件進行聯動。
-
在 Controller 中通過前端提交的表單資料,獲取當前使用者資訊並封裝為令牌,對令牌執行登入的方法,如果資訊錯誤則丟擲異常(這些異常是 Shiro 已經定義好的)
@RequestMapping("/login") public String login(String username, String password, Model model){ // 獲取當前的使用者 Subject subject = SecurityUtils.getSubject(); // 封裝使用者的登入資料 UsernamePasswordToken token = new UsernamePasswordToken(username, password); try{ subject.login(token); // 執行登入的方法,如果沒有異常就說明 OK 了 return "index"; // 登入成功,返回首頁 } catch (UnknownAccountException e){ //使用者名稱不存在 model.addAttribute("msg", "使用者名稱錯誤"); return "login"; } catch (IncorrectCredentialsException ice){ //密碼不存在 model.addAttribute("msg", "密碼錯誤"); return "login"; } }
-
在登入頁面的表單上方新增一個提示資訊,如果使用者名稱或密碼錯誤會提示
<p th:text="${msg}" style="color:red;"></p>
-
測試,在表單中填寫資訊,會提示錯誤資訊,因為我們還沒有認證使用者名稱和密碼
注意:IDEA 控制檯會顯示 “執行了 認證 doGetAuthenticationInfo”,說明執行了 UserRealm 的認證方法!
原始碼分析:在使用 SecurityUtils 的靜態方法返回 getSubject() 之前,靜態變數 securityManager 已經被載入,因為 securityManager 中管理著 Realm 物件,所以會執行 Realm 中的方法,但是為什麼是執行了認證方法呢?是因為執行了 login() 方法,會將 token 傳到 Authentication 吧
// 原始碼: public abstract class SecurityUtils { private static SecurityManager securityManager; public static Subject getSubject() { ... return subject; } } /* @param token * the token encapsulating the subject's principals and credentials to be passed to the Authentication subsystem for verification. */ void login(AuthenticationToken token) throws AuthenticationException;
-
因為執行了認證方法,所以我們可以在該方法中做一些操作:取出資料庫中真實的使用者資訊,用於和使用者填寫的資訊進行比對
// 認證 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("執行了 認證 doGetAuthenticationInfo"); // 使用者名稱,密碼,資料庫中取 String name = "root"; String password = "123456"; UsernamePasswordToken userToken = (UsernamePasswordToken) token; if (!userToken.getUsername().equals(name)){ return null; //丟擲異常 UnknownAccountException } // 可以加密 MD5 、MD5鹽值加密 // 密碼認證 shiro 做,加密了 return new SimpleAuthenticationInfo("",password,""); }
-
測試,使用者名稱和密碼分別填寫 root 和 123456,登入成功可以訪問 add 和 update 頁面。
使用者認證:連線資料庫,整合 MyBatis
-
匯入依賴,使用 Druid 資料來源
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- Druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.21</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> </dependency> <!-- mybatis-spring-boot-starter:整合 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency>
-
Druid 資料來源資訊 application.yaml
spring: datasource: username: root password: root url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.jdbc.Driver # 自定義資料來源 type: com.alibaba.druid.pool.DruidDataSource #Spring Boot 預設是不注入這些屬性值的,需要自己繫結 #druid 資料來源專有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true #配置監控統計攔截的filters,stat:監控統計、log4j:日誌記錄、wall:防禦sql注入 #如果允許時報錯 java.lang.ClassNotFoundException: org.apache.log4j.Priority #則匯入 log4j 依賴即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
-
將資料庫繫結到 IDEA 中
-
配置 MyBatis,application.properties
mybatis.type-aliases-package=com.song.pojo mybatis.mapper-locations=classpath:mapper/*.xml
-
根據資料庫表的資訊編寫實體類,提前匯入 Lombok
@Data @AllArgsConstructor @NoArgsConstructor public class User { private int id; private String name; private String pwd; private String perms; }
-
編寫 mapper 介面 UserMapper.class
@Repository @Mapper public interface UserMapper { public User queryUserByName(String name); }
-
編寫對應的 mapper 配置檔案 UserMapper.xml,放在 resources/mapper 資料夾下
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.song.mapper.UserMapper"> <select id="queryUserByName" resultType="User" parameterType="String"> select * from User where name = #{name} </select> </mapper>
-
編寫 service 層,UserService 介面及實現類
public interface UserService { public User queryUserByName(String name); }
@Service public class UserServiceImpl implements UserService{ @Autowired UserMapper userMapper; @Override public User queryUserByName(String name) { return userMapper.queryUserByName(name); } }
-
測試,成功輸出資料庫中的“張三”物件,說明前面編寫的程式碼沒有問題
@SpringBootTest class ShiroSpringbootApplicationTests { @Autowired UserService userService; @Test void contextLoads() { System.out.println(userService.queryUserByName("張三")); } }
-
使用者認證:Realm 的認證方法中使用連線的資料庫中的真實的資料
@Autowired UserService userService; // 認證 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("執行了 認證 doGetAuthenticationInfo"); UsernamePasswordToken userToken = (UsernamePasswordToken) token; // 連線真實資料庫 User user = userService.queryUserByName(userToken.getUsername()); if (user == null){ // 沒有這個人 return null; //UnknownAccountException } // 密碼可以加密: MD5 、MD5鹽值加密 return new SimpleAuthenticationInfo("",user.getPwd(),""); }
-
測試,成功
使用者授權
-
在 ShiroFilterFactoryBean 設定許可權,並設定跳轉的未授權頁面,當進入設定許可權的頁面 /user/add 和 /user/update 時,會自動執行 Rleam 中的授權方法,所以要在 Rleam 的授權方法中做具體的授權操作
// ShiroFilterFactoryBean:3 @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 設定安全管理器 bean.setSecurityManager(defaultWebSecurityManager); Map<String, String> filterMap = new LinkedHashMap<>(); // 通過 shiro 的內建過濾器 // 授權,正常情況下沒有授權會跳轉到未授權頁面 filterMap.put("/user/add","perms[user:add]"); filterMap.put("/user/update","perms[user:update]"); // 登入攔截 filterMap.put("/user/*", "authc"); bean.setFilterChainDefinitionMap(filterMap); // 設定登入的請求 bean.setLoginUrl("/toLogin"); // 未授權頁 bean.setUnauthorizedUrl("/noauth"); return bean; }
-
Controller 跳轉到未授權頁面
@RequestMapping("/noauth") @ResponseBody public String unauthrized(){ return "未經授權無法訪問此頁面"; }
-
測試,點選 add 會跳轉到未授權頁面,並且所有使用者都是未授權,接下來要給使用者授予訪問的許可權!
-
ShiroFilterFactoryBean 只是設定了許可權,但是怎麼把這個許可權賦給使用者呢?真正的授權操作在 Rleam 的授權方法中
// 授權 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("執行了 授權 doGetAuthorizationInfo"); // SimpleAuthorizationInfo SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 授權 info.addStringPermission("user:add"); return info; }
-
測試,/user/add 頁面每個使用者都可以進去,因為每個使用者進入授權方法後都被授予了訪問的許可權,而在實際中不應該這樣硬編碼授權操作,應該根據資料庫中的許可權資訊進行授權操作!
-
為資料庫中的 User 表新增許可權欄位,對應實體類的屬性欄位也應該修改
-
根據資料庫的 perms 欄位設定許可權
// 授權 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("執行了 授權 doGetAuthorizationInfo"); // SimpleAuthorizationInfo SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // info.addStringPermission("user:add"); // 拿到當前登入的這個物件 Subject subject = SecurityUtils.getSubject(); // user <= return new SimpleAuthenticationInfo(user,user.getPwd(),""); // 第一個引數user,getPrincipal() 取出的是認證方法返回的物件中存的這個user User currentUser = (User) subject.getPrincipal(); // 拿到User物件 // 設定當前使用者的許可權 info.addStringPermission(currentUser.getPerms()); return info; }
// SimpleAuthenticationInfo 原始碼 /* @param principal the 'primary' principal associated with the specified realm. * @param credentials the credentials that verify the given principal. * @param realmName the realm from where the principal and credentials were acquired. */ public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) { this.principals = new SimplePrincipalCollection(principal, realmName); this.credentials = credentials; }
-
測試,使用者“root”有訪問 update 頁面的許可權,使用者“張三”有訪問 add 頁面的許可權,其他使用者都不能訪問這兩個頁面。
頁面優化
讓使用者登入之後顯示的資訊不一樣,比如:使用者“root”登入之後只顯示 update,而使用者“張三”登入只顯示 add,其他使用者什麼都不顯示。可以使用 thymeleaf 進行操作。
-
匯入 thymeleaf 整合包(和 Spring Security 很相似 thymeleaf-extras-springsecurity4)
<!--thymeleaf 和 shiro 整合包--> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
-
在 ShiroConfig 中裝配 Bean 來整合 shiro 和 thymeleaf
// 整合 ShiroDialect:用來整合 shiro thymeleaf @Bean public ShiroDialect getShiroDialect(){ return new ShiroDialect(); }
-
修改前端 index.html 頁面
先匯入 shrio 的的名稱空間
xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"
修改之前的 add 和 update 頁面
<div shiro:hasPermission="user:add"> <a th:href="@{/user/add}">add</a> </div> <div shiro:hasPermission="user:update"> <a th:href="@{/user/update}">update</a> </div>
-
測試,需求實現,但是,當沒有許可權時 add 和 update 都沒有,那麼也就無法跳轉到登入頁面,說明缺少一個登入按鈕,所以要增加一個登入按鈕,讓其在未登入時顯示,登入之後不顯示
-
編寫前端程式碼,新增登入按鈕
<!--從 session 中判斷值--> <!--<div shiro:guest="true">--> <div th:if="${session.loginUser==null}"> <a th:href="@{/toLogin}" >登入</a> </div>
-
在認證時查出來使用者之後將資訊傳到前端,用來控制登入按鈕的顯示效果【這一步操作可以不寫,可以在前端直接用 shiro 標籤
shiro:guest="true"
來實現】Subject currentSubject = SecurityUtils.getSubject(); Session session = currentSubject.getSession(); session.setAttribute("loginUser",user); //前端可以拿到這個 user
-
測試,登入按鈕的顯示效果成功實現
總結
1、Controller 層
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex(Model model){
model.addAttribute("msg", "hello");
return "index";
}
@RequestMapping("/user/add")
public String add(){
return "user/add";
}
@RequestMapping("/user/update")
public String update(){
return "user/update";
}
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
@RequestMapping("/login")
public String login(String username, String password, Model model){
// 獲取當前的使用者
Subject subject = SecurityUtils.getSubject();
// 封裝使用者的登入資料
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try{
subject.login(token); // 執行登入的方法,如果沒有異常就說明 OK 了
return "index";
} catch (UnknownAccountException e){ //使用者名稱不存在
model.addAttribute("msg", "使用者名稱錯誤");
return "login";
} catch (IncorrectCredentialsException ice){ //密碼不存在
model.addAttribute("msg", "密碼錯誤");
return "login";
}
}
@RequestMapping("/noauth")
@ResponseBody
public String unauthrized(){
return "未經授權無法訪問此頁面";
}
}
2、自定義的 Realm
// 自定義的realm extends AuthorizingRealm
public class UserRealm extends AuthorizingRealm{
@Autowired
UserService userService;
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("執行了 授權 doGetAuthorizationInfo");
// SimpleAuthorizationInfo
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 每個使用者都會被授予 user:add 許可權
// info.addStringPermission("user:add");
// 拿到當前登入的這個物件
Subject subject = SecurityUtils.getSubject();
// user <= return new SimpleAuthenticationInfo(user,user.getPwd(),"");
// 第一個引數user,getPrincipal() 取出的就是認證方法返回的物件中存的這個user
User currentUser = (User) subject.getPrincipal();
// 設定當前使用者的許可權
info.addStringPermission(currentUser.getPerms());
return info;
}
// 認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("執行了 認證 doGetAuthenticationInfo");
/* // 使用者名稱,密碼,資料庫中取,這裡未連線資料可
String name = "root";
String password = "123456";
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
if (!userToken.getUsername().equals(name)){
return null; //丟擲異常 UnknownAccountException
}
// 密碼認證 shiro 做,加密了
return new SimpleAuthenticationInfo("",password,"");*/
// 連線真實資料庫
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
User user = userService.queryUserByName(userToken.getUsername());
if (user == null){ // 沒有這個人
return null; //UnknownAccountException
}
// 用於登入按鈕是否顯示
Subject currentSubject = SecurityUtils.getSubject();
Session session = currentSubject.getSession();
session.setAttribute("loginUser",user); //前端可以拿到這個 user
// return new SimpleAuthenticationInfo("",user.getPwd(),"");
return new SimpleAuthenticationInfo(user,user.getPwd(),"");//第一個引數用於授權
}
}
3、ShrioConfig 配置類
@Configuration
public class ShiroConfig {
// ShiroFilterFactoryBean:3
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 設定安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
// 新增 shiro 的內建過濾器
Map<String, String> filterMap = new LinkedHashMap<>();
// 授權,正常情況下沒有授權會跳轉到未授權頁面
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/update","perms[user:update]");
// 登入攔截
// filterMap.put("/user/add", "authc");
// filterMap.put("/user/update", "authc");
filterMap.put("/user/*", "authc");
bean.setFilterChainDefinitionMap(filterMap);
// 設定登入的請求
bean.setLoginUrl("/toLogin");
// 未授權頁面
bean.setUnauthorizedUrl("/noauth");
return bean;
}
// DefaultWebSecurityManager:2
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 關聯 userRealm
securityManager.setRealm(userRealm);
return securityManager;
}
// 建立 realm 物件,需要自定義:1
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
// 整合 ShiroDialect:用來整合 shiro thymeleaf
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
}
4、前端頁面 index.html 和 login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首頁</h1>
<!--從 session 中判斷值-->
<!--<div shiro:guest="true">-->
<div th:if="${session.loginUser==null}">
<a th:href="@{/toLogin}" >登入</a>
</div>
<p th:text="${msg}"></p>
<hr>
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}">add</a>
</div>
<div shiro:hasPermission="user:update">
<a th:href="@{/user/update}">update</a>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登入</h1>
<p th:text="${msg}" style="color:red;"></p>
<form th:action="@{/login}">
<p>使用者名稱:<input type="text" name="username"></p>
<p>密碼:<input type="text" name="password"></p>
<p><input type="submit"></p>
</form>
</body>
</html>