Spring Boot整合Shiro框架進行身份驗證

Spring Boot整合Shiro框架進行身份驗證

一.什麼是Shiro

Apache Shiro 是 Java 的一個安全框架,Shiro 可以幫助我們完成:認證、授權、加密、會話管理等。相比較Spring Security 她更加的小巧易用。其基本功能點如下圖所示:
這裏寫圖片描述

Authentication:身份認證 / 登錄,驗證用戶是不是擁有相應的身份;

Authorization:授權,即權限驗證,驗證某個已認證的用戶是否擁有某個權限;即判斷用戶是否能做事情,常見的如:驗證某個用戶是否擁有某個角色。或者細粒度的驗證某個用戶對某個資源是否具有某個權限;

Session Manager:會話管理,即用戶登錄後就是一次會話,在沒有退出之前,它的所有信息都在會話中;會話可以是普通 JavaSE 環境的,也可以是如 Web 環境的;

Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲;

Web Support:Web 支持,可以非常容易的集成到 Web 環境;

Caching:緩存,比如用戶登錄後,其用戶信息、擁有的角色 / 權限不必每次去查,這樣可以提高效率;

Concurrency:shiro 支持多線程應用的併發驗證,即如在一個線程中開啓另一個線程,能把權限自動傳播過去;

Testing:提供測試支持;

Run As:允許一個用戶假裝爲另一個用戶(如果他們允許)的身份進行訪問;

Remember Me:記住我,這個是非常常見的功能,即一次登錄後,下次再來的話不用登錄了。

Shiro 不會去維護用戶、維護權限;這些需要我們自己去設計和提供;然後通過相應的接口注入給 Shiro。下面我們就來看看在Spring Boot中怎麼進行身份的驗證。

二.創建Spring Boot項目

創建spring boot項目,需要加入的組件有:web,mysql,thymeleaf,shiro,devtools。
下面是創建完項目後pom文件的依賴:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

三.創建實體類及建表

進行身份的驗證或者權限的授權,大體思路是把用戶賦予角色,將角色賦予相應的權限。所以我們需要創建用戶類,角色類,和權限類。
1.用戶類

public class UserInfo implements Serializable {
    private Integer uid;
    private String name;
    private String password; 
    //祕鑰
    private String salt;
    // 一個用戶具有多個角色
    private List<Role> roleList;
   //省略get、set方法 
}

2.角色類

public class Role {

    private Integer id;
    // 角色標識程序中判斷使用,如"admin"
    private String role;
    // 一個角色可以有多個權限
    private List<Permission> permissions;
    // 一個角色對應多個用戶
    private List<UserInfo> userInfos;

    //省略get、set方法 
}

3.權限類

public class Permission implements Serializable {
    private Integer id;
    private String name;
    // 資源路徑.
    private String url;
    // 權限字符串如 role:create,role:update,role:delete,role:view
    private String permission;
    // 一個權限可以包含多個角色
    private List<Role> roles;

    //省略get、set方法 
}

4.用戶表

CREATE TABLE `user_info` (
  `uid` int(11) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `salt` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`uid`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO `user_info` VALUES ('1', 'ADMIN', '5b48c185e886ff93be5244f979b2864b', 'asdfghjkl');

5.角色表

CREATE TABLE `role` (
  `id` int(11) NOT NULL,
  `role` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO `role` VALUES ('1', 'admin');
INSERT INTO `role` VALUES ('2', 'vip');

6.權限表

CREATE TABLE `permission` (
  `id` int(11) NOT NULL,
  `name` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
  `permission` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
  `url` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO `permission` VALUES ('1', 'UserManger', 'userInfo:view', 'userInfo/userList');
INSERT INTO `permission` VALUES ('2', 'UserAdd', 'userInfo:add', 'userInfo/userAdd');
INSERT INTO `permission` VALUES ('3', 'UserDelete', 'userInfo:del', 'userInfo/userDel');

7.用戶角色關聯表

CREATE TABLE `user_role` (
  `role_id` int(11) NOT NULL,
  `uid` int(11) NOT NULL,
  KEY `FKgkmyslkrfeyn9ukmolvek8b8f` (`uid`),
  KEY `FKhh52n8vd4ny9ff4x9fb8v65qx` (`role_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO `user_role` VALUES ('1', '1');
INSERT INTO `user_role` VALUES ('1', '2');

8.角色權限關聯表

CREATE TABLE `role_permission` (
  `role_id` int(11) NOT NULL,
  `permission_id` int(11) NOT NULL,
  KEY `FKomxrs8a388bknvhjokh440waq` (`permission_id`),
  KEY `FK9q28ewrhntqeipl1t04kh1be7` (`role_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO `role_permission` VALUES ('1', '1');
INSERT INTO `role_permission` VALUES ('1', '2');

四.配置application.properties文件

##mapper文件位置
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
mybatis.type-aliases-package=com.example.demo.entity

##配置數據源
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8
spring.datasource.username = root
spring.datasource.password = 000000

spring.thymeleaf.cache = false
spring.thymeleaf.mode = LEGACYHTML5

五.創建mapper類

UserMapper:

package com.example.demo.mapper;

import com.example.demo.entity.UserInfo;

public interface UserMapper {

    public UserInfo getUserByName(String name);

    public List<Role> getRoles(int uid);

    public List<Permission> getPermissions(int roleId);

}

UserMapper.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.example.demo.mapper.UserMapper">


    <select id="getUserByName" parameterType="com.example.demo.entity.UserInfo" resultType="com.example.demo.entity.UserInfo">
        select * from user_info where name = #{name}
    </select>

    <select id="getRoles" parameterType="java.lang.Integer" resultType="com.example.demo.entity.Role">
        select * from role  where id in (select role_id from user_role where uid = #{uid})
    </select>

    <select id="getPermissions" parameterType="java.lang.Integer" resultType="com.example.demo.entity.Permission">
        select * from permission where id in(select permission_id from role_permission where role_id = #{roleId})
    </select>
</mapper>

這個可以參考上一篇Spring Boot整合Mybatis

六.控制器

package com.example.demo.controller;

import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class UserController {

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

    @RequestMapping("/login")
    public String login(HttpServletRequest request, Map<String, Object> map) throws Exception {
        System.out.println("HomeController.login()");
        // 登錄失敗從request中獲取shiro處理的異常信息。
        // shiroLoginFailure:就是shiro異常類的全類名.
        String exception = (String) request.getAttribute("shiroLoginFailure");
        System.out.println("exception=" + exception);
        String msg = "";
        if (exception != null) {
            if (UnknownAccountException.class.getName().equals(exception)) {
                System.out.println("UnknownAccountException -- > 賬號不存在:");
                msg = "UnknownAccountException -- > 賬號不存在:";
            } else if (IncorrectCredentialsException.class.getName().equals(exception)) {
                System.out.println("IncorrectCredentialsException -- > 密碼不正確:");
                msg = "IncorrectCredentialsException -- > 密碼不正確:";
            } else if ("kaptchaValidateFailed".equals(exception)) {
                System.out.println("kaptchaValidateFailed -- > 驗證碼錯誤");
                msg = "kaptchaValidateFailed -- > 驗證碼錯誤";
            } else {
                msg = "else >> " + exception;
                System.out.println("else -- >" + exception);
            }
        }
        map.put("msg", msg);
        // 此方法不處理登錄成功,由shiro進行處理
        return "/login";
    }

    @RequestMapping("/test")
    public String test() {
        System.out.println("------test-------");
        return "test";
    }

}

在src/main/resources的templates目錄下創建一個index.html和login.html文件
login.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
錯誤信息:<h4 th:text="${msg}"></h4>
<form action="" method="post">
    <p>賬號:<input type="text" name="username" value="admin"/></p>
    <p>密碼:<input type="text" name="password" value="123456"/></p>
    <p><input type="submit" value="登錄"/></p>
</form>
</body>
</html>

七.引入Shiro

到目前爲止並沒有Shiro的參與,啓動項目我們現在可以隨便的訪問index.html頁面,現在我們要求訪問index的時候必須要先登錄,如果沒有登錄則跳轉到login.html頁面。下面就該Shiro登場了。

這裏寫圖片描述

Subject:主體,代表了當前 “用戶”,這個用戶不一定是一個具體的人,與當前應用交互的任何東西都是 Subject,如網絡爬蟲,機器人等;即一個抽象概念;所有 Subject 都綁定到 SecurityManager,與 Subject 的所有交互都會委託給 SecurityManager;可以把 Subject 認爲是一個門面;SecurityManager 纔是實際的執行者;

SecurityManager:安全管理器;即所有與安全有關的操作都會與 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它負責與後邊介紹的其他組件進行交互,如果學習過 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;

Realm:域,Shiro 從從 Realm 獲取安全數據(如用戶、角色、權限),就是說 SecurityManager 要驗證用戶身份,那麼它需要從 Realm 獲取相應的用戶進行比較以確定用戶身份是否合法;也需要從 Realm 得到用戶相應的角色 / 權限進行驗證用戶是否能進行操作;可以把 Realm 看成 DataSource,即安全數據源。

也就是說對於我們而言,最簡單的一個 Shiro 應用:
應用代碼通過 Subject 來進行認證和授權,而 Subject 又委託給 SecurityManager;
我們需要給 Shiro 的 SecurityManager 注入 Realm,從而讓 SecurityManager 能得到合法的用戶及其權限進行判斷。

理論看完了看看具體項目中應該怎麼做吧:
1.Shiro 配置
Apache Shiro 核心通過 Filter 來實現,就好像SpringMVC 通過DispachServlet 來主控制一樣。 所以我們需要定義一系列關於URL的規則和訪問權限。

package com.example.demo.config;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        System.out.println("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 攔截器.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不會被攔截的鏈接 順序判斷
        filterChainDefinitionMap.put("/static/**", "anon");
        // 配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了
        filterChainDefinitionMap.put("/logout", "logout");
        // <!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最爲下邊 -->:這是一個坑呢,一不小心代碼就不好使了;
        // <!-- authc:所有url都必須認證通過纔可以訪問; anon:所有url都都可以匿名訪問-->
        filterChainDefinitionMap.put("/**", "authc");
        // 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面,設置的話沒經過驗證會發送test請求到控制器,由控制器決定轉到對應視圖
        //shiroFilterFactoryBean.setLoginUrl("/test");
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登錄成功後要跳轉的鏈接
        shiroFilterFactoryBean.setSuccessUrl("/index");

        // 未授權界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 憑證匹配器 (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了 )
     * 
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:這裏使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(1);// 散列的次數,比如散列兩次,相當於md5(md5(""));

        return hashedCredentialsMatcher;
    }

    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }


}

這些配置套路都是一樣的,主要的校驗邏輯是在realm域裏面,所以我們主要的工作就是寫這個域。
看下Shiro 默認提供的 Realm
這裏寫圖片描述

以後一般繼承 AuthorizingRealm(授權)即可;其繼承了 AuthenticatingRealm(身份驗證),而且也間接繼承了 CachingRealm(帶有緩存實現)。繼承AuthorizingRealm類重寫doGetAuthenticationInfo(驗證)和doGetAuthorizationInfo(授權)兩個方法。

package com.example.demo.config;

import javax.annotation.Resource;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

import com.example.demo.entity.UserInfo;
import com.example.demo.service.UserService;


public class MyShiroRealm extends AuthorizingRealm {

    @Resource
    private UserService userService;

    /**
     * 認證信息.(身份驗證) : Authentication 是用來驗證用戶身份
     * 
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("MyShiroRealm.doGetAuthenticationInfo()");

        // 獲取用戶的輸入的賬號.
        String name = (String) token.getPrincipal();
        System.out.println(token.getCredentials());

        // 通過name和password從數據庫中查找 User對象,
        UserInfo userInfo = userService.getUserByName(name);
        if (userInfo == null) {
            return null;
        }

        int uid = userInfo.getUid();
        List<Role> roles = userService.getRoles(uid);
        int roleId = roles.get(0).getId();
        List<Permission> permissions = userService.getPermissions(roleId);
        roles.get(0).setPermissions(permissions);
        userInfo.setRoleList(roles);
        // 加密方式;
        // 交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配
        String password = userInfo.getPassword();
        // 祕鑰
        ByteSource salt = ByteSource.Util.bytes(userInfo.getSalt());
        // 當前域的名稱(MyShiroRealm)
        String realmName = getName();
        // 認證信息
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userInfo, password, salt, realmName);

        return authenticationInfo;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // TODO Auto-generated method stub
        return null;
    }

}

到此工作已經全部結束了,啓動項目看看效果吧。瀏覽器輸入http://localhost:8080/index,試圖訪問index.html頁面,但是沒有登錄自動跳轉到了登錄頁面。
這裏寫圖片描述

輸入正確的賬號密碼後就可以訪問到index.html頁面了

附:用戶表中的密碼是進行加過密,Shiro提供一整套的加密方法,這個後面再說。現在提供一個密碼加密的工具類供測試使用:

package com.example.demo.util;

import org.apache.shiro.crypto.hash.Md5Hash;

/**
 * 加解密工具類
 *
 */
public class CryptographyUtil {


    public static String md5(String str, String salt) {
        return new Md5Hash(str, salt).toString();
    }

    public static void main(String[] args) {
        String password = "123456";
        System.out.println(CryptographyUtil.md5(password, "asdfghjkl"));
    }

}

項目的整體結構:
這裏寫圖片描述

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