Shiro:連接數據庫實現認證、授權 一、用戶認證與攔截器 二、用戶授權與前端整合 三、拓展整合Thymeleaf引擎

Apache Shiro 一個簡單的Java安全框架,在此框架基礎上可以輕鬆實現網站的用戶認證授權

  • Shiro的三大核心對象:
    1. Subject (代表當前用戶對象)
    2. ShiroSecurityManager(管理所有Subject對象)
    3. Realm(管理數據:具體數據的授權與認證)

一、用戶認證與攔截器

1.SpringBoot項目準備

  • 導入案例涉及依賴
    <dependencies>
        <!--mybatis druid數據原-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.12</version>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>


        <!--shiro spring-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.7.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--整合thymeleaf-->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>
    </dependencies>
  • 配置Druid數據源,連接數據庫
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.jdbc.Driver

    #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,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
  • 配置Mybatis別名,mapper位置
mybatis:
  type-aliases-package: com.ht.springbootshiro01.pojo
  mapper-locations: classpath:mapper/*.xml
# 資源位置resources/mapper/*.xml
  • 編寫Dao、Service層 測試Mybatis正常使用


(1)Dao(Mapper)層邏輯代碼(省略實體類)
@Repository
@Mapper
// Dao 層
public interface UserMapper {

    /**
     * 根據用戶名 查詢指定用戶
     * @param name
     * @return
     */
    public User queryUserByName(@Param("name") String name);
}
(2)Mapper.xml文件編寫(查詢數據庫)
<?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.ht.springbootshiro01.mapper.UserMapper">

    <select id="queryUserByName" parameterType="String" resultType="User">
        select * from mybatis.user where name=#{name};
    </select>

</mapper>
(3)Service層業務邏輯(調用Dao層)
@Service
public class UserService {
    // service 調用 dao層
    @Autowired
    UserMapper userMapper;

    public User queryUserByName(String name){
        return userMapper.queryUserByName(name);
    }
}
(4)測試代碼(查詢用戶結果)
@SpringBootTest
class SpringbootShiro01ApplicationTests {

    @Autowired
    UserService userService;

    @Test
    void contextLoads() {
        // mybatis 測試
        User admin = userService.queryUserByName("admin");
        System.out.println(admin.toString());
    }
}

至此查詢成功後,SpringBoot 整合MyBatis數據庫準備工作完成

2.基礎網頁路由配置

  • 首頁,登錄頁,管理員權限下的顯示所有用戶網頁


  • 跳轉路由代碼(Controller)
/**
 * 管理路由跳轉的Controller
 */
@Controller
public class RouteController {

    @RequestMapping({"/index","/"})
    public String index(Model model){
        model.addAttribute("msg","Hello Shiro");
        return "index";
    }

    @RequestMapping("/toLogin")
    public String toLogin(){
        return "login";
    }

    //user
    @RequestMapping("/user/add")
    public String add(){
        return "user/add";
    }

    @RequestMapping("/user/update")
    public String update(){
        return "user/update";
    }

    // show
    @RequestMapping("/show")
    public String show(){
        return "show";
    }

}

3.編寫Shiro配置文件,自定義Realm(管理授權、認證)

  • Shiro配置文件 配置三大要素:自定義Realm規則,SecurityManager安全管理對象,Shiro Filter攔截器相關信息配置

  • shiro的內置過濾器:

       * anon: 無需認證就可訪問
       * authc: 認證後纔可訪問
       * user: 必須擁有記住我功能纔可訪問
       * perms:擁有對某個資源的權限纔可訪問
       * role: 擁有某個角色的權限纔可訪問
    
@Configuration
public class ShiroConfig {

    //1.創建Realm對象 自定義 注入bean
    @Bean(name = "getUserRealm")
    public UserRealm getUserRealm(){
        return new UserRealm();
    }

    //2.SecurityManager Spring 注入參數 bean Qualifier(方法名)
    @Bean(name = "getSecurityManager")
    public DefaultWebSecurityManager defaultSecurityManager(@Qualifier("getUserRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 關聯 Realm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    //3.Shiro Filter
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("getSecurityManager") DefaultSecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 設置 安全管理
        bean.setSecurityManager(securityManager);
        //過濾器配置
        Map<String, String> filterMap = new LinkedHashMap<>();

        // /user 請求 攔截 認證後可訪問
        filterMap.put("/user/**","authc");

        // 開放登錄接口 首頁
        filterMap.put("/toLogin","anon");
        filterMap.put("/index","anon");
        filterMap.put("/","anon");

        // 關閉其它路由 需授權訪問
        //filterMap.put("/**","authc");

        bean.setFilterChainDefinitionMap(filterMap);
        //System.out.println("Shiro攔截器注入成功");

        // 自定義登錄頁面路徑 實現登錄攔截
        bean.setLoginUrl("/toLogin");

        return bean;
    }

}

至此實現了Shiro的初步登錄攔截,發現user/下的路由都不能訪問

4.編寫自定義Realm規則,認證 繼承AuthorizingRealm

~ token令牌實際應用規則:獲取前端登錄傳遞的用戶名、密碼信息封裝令牌,Shiro框架會自動經過自定義Realm 認證規則檢驗,與數據庫中用戶信息匹配比較

// 自定義UserRealm
public class UserRealm extends AuthorizingRealm {

    @Autowired
    UserService userService;

    // 授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("執行了 授權 ==》" + principals.getRealmNames().toString());
        return null;
    }

    // token 認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("執行了 認證 ==》" + token.getPrincipal().toString());

        // 數據庫取數據 進行認證
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        // 前端傳遞 用戶名 查詢 數據庫
        User user = userService.queryUserByName(userToken.getUsername());

        if (null == user){
            // 前端傳遞 與 數據庫查詢用戶名 不一致
            return null; // 拋出異常 未知用戶名
        }

        // 默認密碼不加密(不安全) 可以做 md5加密 或 md5鹽值加密
        // 密碼認證 shiro做
        return new SimpleAuthenticationInfo("",user.getPwd(),"");
    }
}
  • 登錄請求處理:獲取前端表單傳遞數據,封裝令牌==》執行登錄操作==》處理登錄異常
@Controller
public class LoginController {

    /**
     * 登錄請求 獲取表單傳遞數據 封裝令牌 token
     * @param username
     * @param password
     * @param model
     * @return
     */
    @PostMapping("/login.do")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Model model){
        System.out.println("登錄信息:username: " + username + " ; password: " + password);
        // 獲取當前用戶
        Subject user = SecurityUtils.getSubject();
        // 封裝令牌 用於 Realm驗證
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);

        // 執行登錄
        try {
            user.login(token); // 調用自定義 UserRealm 認證方法
            return "index";
        } catch (UnknownAccountException uae) {
            // 用戶不存在
            System.out.println("There is no user with username of " + token.getPrincipal());
            model.addAttribute("msg","用戶名不存在");
            return "login";
        } catch (IncorrectCredentialsException ice) {
            // 密碼錯誤
            System.out.println("Password for account " + token.getPrincipal() + " was incorrect!");
            model.addAttribute("msg","密碼錯誤");
            return "login";
        } catch (LockedAccountException lae) {
            // 當前用戶鎖定
            System.out.println("The account for username " + token.getPrincipal() + " is locked.  " +
                    "Please contact your administrator to unlock it.");
            model.addAttribute("msg","用戶鎖定");
            return "login";
        } catch (AuthenticationException ae) {
            //unexpected condition?  error?
            System.out.println("其它錯誤");
            model.addAttribute("msg","其它錯誤");
            return "login";
        }

    }
}

~ 測試Shiro的用戶認證


二、用戶授權與前端整合

1.FilterMap中添加指定路由權限

       Map<String, String> filterMap = new LinkedHashMap<>();
        // 管理員開放權限
        //filterMap.put("/user/**","perms[admin]");
        // 授權 只有 擁有 user:add的權限纔可訪問 失敗401 未授權
        filterMap.put("/user/add","perms[user:add]");
        filterMap.put("/user/update","perms[user:update]");

        // /user 請求 攔截 認證後可訪問
        filterMap.put("/user/**","authc");

        // 開放登錄接口 首頁
        filterMap.put("/toLogin","anon");
        filterMap.put("/index","anon");
        filterMap.put("/","anon");
        // 關閉其它路由 需授權訪問
        //filterMap.put("/**","authc");

        bean.setFilterChainDefinitionMap(filterMap);
        //System.out.println("Shiro攔截器注入成功");

        // 自定義登錄頁面路徑 實現登錄攔截
        bean.setLoginUrl("/toLogin");
        // 設置未授權請求
        bean.setUnauthorizedUrl("/unauthorized");

2.編寫定製未授權跳轉頁面及路由控制器

/***
 * 未授權控制器
 */
@Controller
public class UnauthorizedController {
    /**
     * 未授權頁面
     * @return
     */
    @RequestMapping("/unauthorized")
    public String toUnauthorized(){
        return "error/unauthor";
    }
}

*自定義未授權頁面 /error/unauthor.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>未授權</title>
</head>
<body>
<div>
    <h3 style="color: darkslategrey">未授權,請
        <a th:href="@{/toLogin}">切換用戶</a>,或進行授權操作</h3>
</div>
</body>
</html>

3.根據數據庫中用戶權限 爲用戶授權

  • 自定義Realm 認證 傳遞給授權 攜帶(Principal == 令牌負責人即登錄用戶)
  • 授權方法 取出負責人用戶,根據數據庫中用戶權限授權
// 自定義UserRealm
public class UserRealm extends AuthorizingRealm {

    @Autowired
    UserService userService;

    // 授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("執行了 授權 ==》" + principals.getRealmNames().toString());

        // 增加授權 所有用戶都賦予權限
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        Subject subject = SecurityUtils.getSubject();
        User principal = (User) subject.getPrincipal();

        // 通過數據庫字段 增加 權限
        info.addStringPermission(principal.getPerms());

        return info;
    }

    // token 認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("執行了 認證 ==》" + token.getPrincipal().toString());

        // 數據庫取數據 進行認證
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        // 前端傳遞 用戶名 查詢 數據庫
        User user = userService.queryUserByName(userToken.getUsername());

        if (null == user){
            // 前端傳遞 與 數據庫查詢用戶名 不一致
            return null; // 拋出異常 未知用戶名
        }

        // 密碼認證 shiro做 登錄成功後將用戶信息 傳遞給授權 
        return new SimpleAuthenticationInfo(user,user.getPwd(),"");
    }
}

4.增加前端顯示用戶信息 與 註銷功能

  • 首頁
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>首頁</title>
</head>
<body>
<div>
  <h1>首頁</h1>
    <!---->
  <p th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

    <!--用戶信息-->
    <p th:text="${user_name}"
       th:if="${not #strings.isEmpty(user_name)}"></p>
</div>
<div>
  <a th:href="@{/user/add}">增加</a>
  <a th:href="@{/user/update}">修改</a>
  <a th:href="@{/show}">展示</a>
  <a th:href="@{/loginout.do}">註銷</a>
</div>

</body>
</html>
  • 註銷控制器
    @RequestMapping("/loginout.do")
    public String loginOut(){
        Subject subject = SecurityUtils.getSubject();
        // 註銷
        subject.logout();
        return "login";
    }
  • 登錄成功 封裝用戶名
   user.login(token); // 調用自定義 UserRealm 認證方法
   model.addAttribute("user_name",token.getUsername());
   return "index";

三、拓展整合Thymeleaf引擎

  • 實現不同權限的用戶顯示不同操作
  • 實現未登錄用戶 顯示 登錄按鈕;已登錄顯示註銷按鈕
    重要標籤:
<!--頭引用-->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
shiro:hasPermission="xxx" 擁有某權限
shiro:guest="true" 未登錄
shiro:authenticated="true" 已登陸
  • 完整首頁代碼
<!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>
</head>
<body>
<div>
  <h1>首頁</h1>
    <!---->
  <p th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

    <!--用戶信息-->
    <p th:text="${user_name}"
       th:if="${not #strings.isEmpty(user_name)}"></p>
</div>
<div>
  <!--整合 shiro 根據權限顯示-->
  <div shiro:hasPermission="user:add">
    <a th:href="@{/user/add}">增加</a>
  </div>
  <div shiro:hasPermission="user:update">
    <a th:href="@{/user/update}">修改</a>
  </div>
  <a th:href="@{/show}">展示</a>
  <!--遊客顯示登錄按鈕 登錄成功不顯示-->
  <div shiro:guest="true">
    <a th:href="@{/toLogin}">登錄</a>
  </div>
  <div shiro:authenticated>
    <a th:href="@{/loginout.do}">註銷</a>
  </div>
</div>

</body>
</html>

~體驗到Shiro框架的便捷之處,告別過濾器

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章