1. 程式人生 > 實用技巧 >SpringBoot【六】 Shiro

SpringBoot【六】 Shiro

Shiro

  • Apache Shiro 是一個 java 的安全(許可權)框架
  • 可以容易的開發出足夠好的應用,不僅可以用在 JavaSE 環境,也可以用在 JavaEE 環境
  • 可以完成認證、授權、加密、會話管理、Web 整合、快取等

官網:https://shiro.apache.org/

功能

https://shiro.apache.org/introduction.html

Shiro 架構

外部

https://shiro.apache.org/architecture.html

從外部來看 Shiro,即從應用程式角度來觀察如何使用 Shrio 完成工作

  • Subject 使用者
  • SecurityManager 管理所有使用者
  • Realm 連線資料

內部

快速入門

參考:https://github.com/apache/shiro/tree/master/samples/quickstart

  1. 匯入依賴

    <!-- 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>
    
  2. 配置檔案

    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
    
  3. 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

環境搭建

  1. 新建 SpringBoot 專案,新增 web、thymeleaf 依賴

  2. 編寫 Controller

    @Controller
    public class MyController {
    
        @RequestMapping({"/","/index"})
        public String toIndex(Model model){
            model.addAttribute("msg", "hello");
            return "index";
        }
    }
    
  3. 前端頁面

    <!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>
    
  4. 測試,環境 OK!

Shiro 核心配置

  1. 匯入 shrio 整合 Spring 的依賴

    <!--匯入 shiro 整合 Spring -->
    <dependency>
       <groupId>org.apache.shiro</groupId>
       <artifactId>shiro-spring</artifactId>
       <version>1.5.3</version>
    </dependency>
    
  2. 編寫 Shiro 的配置類 ShiroConfig.class,配置三個 Bean,即三大核心要素(對應三大核心物件):

    • ShiroFilterFactoryBean:過濾物件【第 3 步:連到前端】-------> Subject 使用者
    • DefaultWebSecurityManager:安全物件【第 2 步:接管】--------> SecurityManager 管理所有使用者
    • Realm 物件,需要自定義【第 1 步:建立物件】-------> Realm 連線資料
    @Configuration
    public class ShiroConfig {
        // ShiroFilterFactoryBean:3
        
        // DefaultWebSecurityManager:2
     
        // 建立 realm 物件,需要自定義:1
    }  
    
  3. 自定義 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;
        }
    }
    
  4. 將自定義的 Realm 物件注入 Bean 中

    // 建立 realm 物件,需要自定義:1
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }
    
  5. 建立 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;
    }
    
  6. 建立 ShiroFilterFactoryBean 物件並注入 Bean 中,需要關聯 securityManager(通過傳參)

    // ShiroFilterFactoryBean:3
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 設定安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
        return bean;
    }
    

登入攔截

  1. 編寫兩個前端頁面 /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>
    
  2. Controller 跳轉

    @RequestMapping("/user/add")
    public String add(){
        return "user/add";
    }
    
    @RequestMapping("/user/update")
    public String update(){
        return "user/update";
    }
    
  3. 首頁加入兩個跳轉的超連結

    <a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a>
    
  4. 測試,點選首頁的 add 和 update,兩個頁面都可以進去

  5. 增加需求:對於某個頁面,有些使用者可以訪問,有些使用者不可以訪問

  6. 在 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);
    }
    
  7. 測試,點選 add 和 update 都會跳轉到錯誤頁碼,證明攔截成功

  8. 想要攔截之後跳轉到登入頁面,需要先編寫一個登入頁面

    <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>
    
  9. 對應的需要在 Controller 中進行跳轉

    @RequestMapping("/toLogin")
    public String toLogin(){
        return "login";
    }
    
  10. 在 ShiroFilterFactoryBean 中配置,如果沒有許可權,讓其跳轉到登入頁面

    // 設定登入的請求
    bean.setLoginUrl("/toLogin");
    
  11. 測試成功,點選 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 物件中進行設定,然後和其他兩個核心物件進行聯動。

  1. 在 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";
        }
    }
    
  2. 在登入頁面的表單上方新增一個提示資訊,如果使用者名稱或密碼錯誤會提示

    <p th:text="${msg}" style="color:red;"></p>
    
  3. 測試,在表單中填寫資訊,會提示錯誤資訊,因為我們還沒有認證使用者名稱和密碼

    注意: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;
    
  4. 因為執行了認證方法,所以我們可以在該方法中做一些操作:取出資料庫中真實的使用者資訊,用於和使用者填寫的資訊進行比對

    // 認證
    @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,"");
    }
    
  5. 測試,使用者名稱和密碼分別填寫 root 和 123456,登入成功可以訪問 add 和 update 頁面。

使用者認證:連線資料庫,整合 MyBatis
  1. 匯入依賴,使用 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>
    
  2. 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
    
  3. 將資料庫繫結到 IDEA 中

  4. 配置 MyBatis,application.properties

    mybatis.type-aliases-package=com.song.pojo
    mybatis.mapper-locations=classpath:mapper/*.xml
    
  5. 根據資料庫表的資訊編寫實體類,提前匯入 Lombok

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private int id;
        private String name;
        private String pwd;
        private String perms;
    }
    
  6. 編寫 mapper 介面 UserMapper.class

    @Repository
    @Mapper
    public interface UserMapper {
    
        public User queryUserByName(String name);
    }
    
  7. 編寫對應的 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>
    
  8. 編寫 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);
        }
    }
    
  9. 測試,成功輸出資料庫中的“張三”物件,說明前面編寫的程式碼沒有問題

    @SpringBootTest
    class ShiroSpringbootApplicationTests {
       @Autowired
       UserService userService;
        
       @Test
       void contextLoads() {
          System.out.println(userService.queryUserByName("張三"));
       }
    }
    
  10. 使用者認證: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(),"");
    }
    
  11. 測試,成功

使用者授權

  1. 在 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;
        }
    
  2. Controller 跳轉到未授權頁面

    @RequestMapping("/noauth")
    @ResponseBody
    public String unauthrized(){
        return "未經授權無法訪問此頁面";
    }
    
  3. 測試,點選 add 會跳轉到未授權頁面,並且所有使用者都是未授權,接下來要給使用者授予訪問的許可權!

  4. ShiroFilterFactoryBean 只是設定了許可權,但是怎麼把這個許可權賦給使用者呢?真正的授權操作在 Rleam 的授權方法中

    // 授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("執行了 授權 doGetAuthorizationInfo");
    
        // SimpleAuthorizationInfo
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    	// 授權
    	info.addStringPermission("user:add");
    
        return info;
    }
    
  5. 測試,/user/add 頁面每個使用者都可以進去,因為每個使用者進入授權方法後都被授予了訪問的許可權,而在實際中不應該這樣硬編碼授權操作,應該根據資料庫中的許可權資訊進行授權操作!

  6. 為資料庫中的 User 表新增許可權欄位,對應實體類的屬性欄位也應該修改

  7. 根據資料庫的 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;
    }
    
  8. 測試,使用者“root”有訪問 update 頁面的許可權,使用者“張三”有訪問 add 頁面的許可權,其他使用者都不能訪問這兩個頁面。

頁面優化

讓使用者登入之後顯示的資訊不一樣,比如:使用者“root”登入之後只顯示 update,而使用者“張三”登入只顯示 add,其他使用者什麼都不顯示。可以使用 thymeleaf 進行操作。

  1. 匯入 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>
    
  2. 在 ShiroConfig 中裝配 Bean 來整合 shiro 和 thymeleaf

    // 整合 ShiroDialect:用來整合 shiro thymeleaf
    @Bean
    public ShiroDialect getShiroDialect(){
        return new ShiroDialect();
    }
    
  3. 修改前端 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>
    
  4. 測試,需求實現,但是,當沒有許可權時 add 和 update 都沒有,那麼也就無法跳轉到登入頁面,說明缺少一個登入按鈕,所以要增加一個登入按鈕,讓其在未登入時顯示,登入之後不顯示

  5. 編寫前端程式碼,新增登入按鈕

    <!--從 session 中判斷值-->
    <!--<div shiro:guest="true">-->
    <div th:if="${session.loginUser==null}">
        <a th:href="@{/toLogin}" >登入</a>
    </div>
    
  6. 在認證時查出來使用者之後將資訊傳到前端,用來控制登入按鈕的顯示效果【這一步操作可以不寫,可以在前端直接用 shiro 標籤 shiro:guest="true" 來實現】

    Subject currentSubject = SecurityUtils.getSubject();
    Session session = currentSubject.getSession();
    session.setAttribute("loginUser",user); //前端可以拿到這個 user
    
  7. 測試,登入按鈕的顯示效果成功實現

總結

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>