SpringBoot整合Shiro

  兜兜轉轉,轉眼已經進入12月中旬了,馬上就是2020年了,回顧這一年,技術上,真的提升很少很少,項目中使用的技術都是很老套的SpringMvc+MyBatis,有的老項目還使用的是Struts2+Hibernate,公司對技術也不重視,在穩定的基礎上,不求有功,但求無過;也沒有什麼技術分享。我經歷過上家公司的快速節奏,現在也放鬆下來了,去年我還自學python、學習數據結構和算法、碰到的技術問題都會去深究。今年,雖然還保持博客輸出,但是,明顯懈怠了,基本是工作中碰到的問題纔會輸出,問題的根本原因,除了那些比較明顯的,會寫一寫;隱藏的比較深的那些問題,都是沒有下文的。

  上個月,老大說老闆要搞聚合支付,然後不知道從哪弄來的一套支付代碼,讓我們幾個開發熟悉代碼,爲將來的開發任務做準備。這項目使用了SpringCloud的很多組件,spring-cloud-bus、spring-cloud-consul、spring-cloud-feign、spring-cloud-hystrix等等,項目被拆分爲9個相互關聯的小項目,環境搭建還是使用的docker技術,別的不說,光是能在本地把項目跑起來,前前後後都花了近一天時間。

  正好閒了快一年了,又燃起了對技術的熱枕,準備把這支付項目使用的技術,都整理輸出。第一篇輸出是Shiro。爲什麼是Shiro呢?因爲權限驗證,是網站的基礎,它是用戶維度下,用戶跟網站關聯的第一步,所以,它是第一個輸出。

理論部分

Shiro是什麼?

  Shiro是一款簡單易用的java安全框架,提供了認證、授權、會話管理等功能,由Apache組織開源,在業內被廣泛使用。官網是: http://shiro.apache.org

核心組件

  核心組件有三個,分別是Subject、Realm、SecurityManager。

  Subject:當前操作的主體,可以是自然人,也可以是爬蟲。比如,如果張三在網站通過登陸頁面登陸,進入系統,那當前操作的主體就是張三;如果是爬蟲S模擬登陸、進入系統,那當前操作的主體就是S。

  Realm:域,對登陸時的身份驗證、對訪問的權限控制,這些都是要由我們自己來實現。

  SecurityManager:Shiro框架中,調節者的功能,充當大主管,各種“瑣事”都要交給它,比如,實現的realm要交給SM(SecurityManager下面統一簡稱SM)來管理;使用緩存管理器記錄用戶權限信息,也要交給SM;開啓“記住我”功能,也要通知SM。

  再附上Shiro的完整架構圖 Shiro的完整架構圖

實踐部分

  上面都是說的理論,說的不是非常全面,想了解更多的,可以自行百度或者谷歌。

1.數據庫支持

  我這裏使用的是MySQL,建表語句,這裏就不貼出來了,太長了。 可以戳我下載

  數據持久層使用的是MyBatis框架,對應表的XML文件,都是自動生成的。

2.引入Maven依賴

  這裏使用的是SpringBoot,對SpringBoot不瞭解的也沒關係,就當它是一個普通Spring項目,只不過引入了特定的依賴。

  這裏是繼承 spring-boot-starter-parent 的方式來構建SpringBoot應用。

<dependencies>
    <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.3.1</version>
    </dependency>

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

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.20</version>
    </dependency>

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

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
</dependencies>

3.ShiroConfig配置

  顧名思義,ShiroConfig就是Shiro的配置相關,比如:定義SM、定義攔截和不攔截的url、添加註解支持、添加緩存支持等,具體代碼如下:

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 設置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 登錄的url
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登錄成功後跳轉的url
        shiroFilterFactoryBean.setSuccessUrl("/index");
        // 未授權url
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        // 定義filterChain,靜態資源不攔截
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        // druid數據源監控頁面不攔截
        filterChainDefinitionMap.put("/druid/**", "anon");
        // 配置退出過濾器,其中具體的退出代碼Shiro已經替我們實現了 
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/", "anon");
        // 除上以外所有url都必須認證通過纔可以訪問,未通過認證自動訪問LoginUrl
// filterChainDefinitionMap.put("/**", "authc"); // authc表示:需要通過認證
        filterChainDefinitionMap.put("/**", "user"); // user表示:通過認證,或者,remeber me記住了登錄狀態

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /** * 安全管理器,Shiro的大主管,各種”瑣事“處理都要委託給它<br/> * 比如設置realm域、設置“記住我”、設置緩存等 * * @return */
    @Bean
    public SecurityManager securityManager() {
        // 配置SecurityManager,並注入shiroRealm
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm());
        securityManager.setRememberMeManager(rememberMeManager());
        securityManager.setCacheManager(getEhCacheManager());
        return securityManager;
    }

    /** * 自己實現的Realm * * @return */
    @Bean
    public ShiroRealm shiroRealm() {
        ShiroRealm shiroRealm = new ShiroRealm();
        return shiroRealm;
    }

    /** * Shiro註解支持 * <li>表示當前Subject已經通過login進行了身份驗證;即Subject.isAuthenticated()返回true。 @RequiresAuthentication </li> * <li>表示當前Subject已經身份驗證或者通過記住我登錄的。 @RequiresUser </li> * <li>表示當前Subject沒有身份驗證或通過記住我登錄過,即是遊客身份。 @RequiresGuest </li> * <li>表示當前Subject需要角色admin和user。 @RequiresRoles(value={"admin", "user"}, logical= Logical.AND)</li> * <li>表示當前Subject需要權限user:a或user:b。 @RequiresPermissions (value={"user:a", "user:b"}, logical= Logical.OR)</li> * * @param securityManager * @return */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /** * 緩存支持,這裏是ehcache * * @return */
    @Bean
    public EhCacheManager getEhCacheManager() {
        EhCacheManager em = new EhCacheManager();
        em.setCacheManagerConfigFile("classpath:config/shiro-ehcache.xml");
        return em;
    }

    /** * 返回cookie管理對象 * <li>1.默認cookie對象設置</li> * <li>2.放入cookie管理器</li> * * @return */
    private CookieRememberMeManager rememberMeManager() {
        // 設置cookie名稱,對應login.html頁面的<input type="checkbox" name="rememberMe"/>
        SimpleCookie cookie = new SimpleCookie("rememberMe");
        // 設置cookie的過期時間,單位爲秒,這裏爲一天
        cookie.setMaxAge(86400);

        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(cookie);
        // rememberMe cookie加密的密鑰
        cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
        return cookieRememberMeManager;
    }
}

4.realm配置

  該類需要繼承 AuthorizingRealm ,需要實現兩個方法,一個是 doGetAuthenticationInfo(),它是登錄認證(登錄時調用),驗證用戶名和密碼;另外一個是 doGetAuthorizationInfo() ,它是獲取用戶角色和權限(訪問控制),獲取用戶的權限,對訪問的放行或者阻攔。

  具體代碼如下:

public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private UserRoleMapper userRoleMapper;
    @Autowired
    private UserPermissionMapper userPermissionMapper;

    /** * 獲取用戶角色和權限(驗證權限時調用) */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        String userName = user.getUserName();

        System.out.println("用戶" + userName + "獲取權限-----ShiroRealm.doGetAuthorizationInfo");
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        // 獲取用戶角色集
        List<Role> roleList = userRoleMapper.findByUserName(userName);
        Set<String> roleSet = new HashSet<String>();
        for (Role r : roleList) {
            roleSet.add(r.getName());
        }
        simpleAuthorizationInfo.setRoles(roleSet);

        // 獲取用戶權限集
        List<Permission> permissionList = userPermissionMapper.findByUserName(userName);
        Set<String> permissionSet = new HashSet<String>();
        for (Permission p : permissionList) {
            permissionSet.add(p.getName());
        }
        simpleAuthorizationInfo.setStringPermissions(permissionSet);
        return simpleAuthorizationInfo;
    }

    /** * 登錄認證(登錄時調用) */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 獲取用戶輸入的用戶名和密碼
        String userName = (String) token.getPrincipal();
        String password = new String((char[]) token.getCredentials());

        // 通過用戶名到數據庫查詢用戶信息
        User user = userMapper.findByUserName(userName);

        if (user == null) {
            throw new UnknownAccountException("用戶名或密碼錯誤!");
        }
        if (!password.equals(user.getPassword())) {
            throw new IncorrectCredentialsException("用戶名或密碼錯誤!");
        }
        if (user.getStatus().equals("0")) {
            throw new LockedAccountException("賬號已被鎖定,請聯繫管理員!");
        }
        return new SimpleAuthenticationInfo(user, password, getName());
    }
}

5.前端頁面和對應controller

1.login.html 登陸頁面

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登錄</title>
    <link rel="stylesheet" th:href="@{/css/login.css}" type="text/css">
    <script th:src="@{/js/jquery-1.11.1.min.js}"></script>
</head>
<body>
<div class="login-page">
    <div class="form">
        <input type="text" placeholder="用戶名" name="username" required="required"/>
        <input type="password" placeholder="密碼" name="password" required="required"/>
        <p><input type="checkbox" name="rememberMe"/>記住我</p>
        <button onclick="login()">登錄</button>
    </div>
</div>
</body>
<script th:inline="javascript">
    var ctx = '/';

    function login() {
        var username = $("input[name='username']").val();
        var password = $("input[name='password']").val();
        var rememberMe = $("input[name='rememberMe']").is(':checked');
        $.ajax({
            type: "post",
            url: ctx + "login",
            data: {"username": username, "password": password, "rememberMe": rememberMe},
            dataType: "json",
            success: function (r) {
                if (r.code == 0) {
                    location.href = ctx + 'index';
                } else {
                    alert(r.msg);
                }
            },
            error: function (r) {
                console.log("-------------" + r);
            }
        });
    }
</script>
</html>

2.LoginController 登陸控制器

@Controller
public class LoginController {

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

    @PostMapping("/login")
    @ResponseBody
    public R login(String username, String password, Boolean rememberMe) {
        // 密碼MD5加密
        password = MD5Utils.encrypt(username, password);
        UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
        // 獲取Subject對象
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            return R.ok();
        } catch (UnknownAccountException e) {
            return R.error(e.getMessage());
        } catch (IncorrectCredentialsException e) {
            return R.error(e.getMessage());
        } catch (LockedAccountException e) {
            return R.error(e.getMessage());
        } catch (AuthenticationException e) {
            return R.error("認證失敗!");
        }
    }

    @RequestMapping("/")
    public String redirectIndex() {
        return "redirect:/index";
    }

    @RequestMapping("/index")
    public String index(Model model) {
        // 登錄成後,即可通過Subject獲取登錄的用戶信息
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        model.addAttribute("user", user);
        return "index";
    }

    @GetMapping("/403")
    public String forbid() {
        return "403";
    }
}

6.驗證

  啓動項目,訪問login頁面,我本地是 http://localhost:8888/login ,可以看到,輸入用戶名和密碼(用戶名、密碼都是test)後,跳進了首頁

Shiro登陸成功跳轉

 

發佈了157 篇原創文章 · 獲贊 191 · 訪問量 147萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章