Spring Boot 整合 Shiro 實現登陸認證與權限控制

本文首發於:https://antoniopeng.comjavascript

用戶角色權限數據庫設計

數據庫這裏以 MySQL 爲例css

建立數據庫

所需表以下:html

  • user:用戶表
  • role:角色表
  • perm:權限菜單表
  • user_role:用戶與角色關聯的中間表
  • role_prem:角色與權限菜單關聯的中間表

執行數據庫腳本

/*
 Navicat Premium Data Transfer

 Source Server         : 127.0.0.1
 Source Server Type    : MySQL
 Source Server Version : 50718
 Source Host           : 127.0.0.1:3306
 Source Schema         : shiro

 Target Server Type    : MySQL
 Target Server Version : 50718
 File Encoding         : 65001
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for perm
-- ----------------------------
DROP TABLE IF EXISTS `perm`;
CREATE TABLE `perm`  (
  `perm_id` int(32) NOT NULL COMMENT '權限主鍵',
  `perm_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '權限url',
  `perm_description` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '權限描述',
  PRIMARY KEY (`perm_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of perm
-- ----------------------------
INSERT INTO `perm` VALUES (1, '/user/*', '擁有對用戶的全部操做權限');

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `role_id` int(32) NOT NULL COMMENT '角色主鍵',
  `role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '角色名',
  `role_description` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '角色描述',
  PRIMARY KEY (`role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, '超級管理員', '超級管理員');

-- ----------------------------
-- Table structure for role_perm
-- ----------------------------
DROP TABLE IF EXISTS `role_perm`;
CREATE TABLE `role_perm`  (
  `role_id` int(32) NOT NULL COMMENT '角色主鍵',
  `perm_id` int(32) DEFAULT NULL COMMENT '權限主鍵'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role_perm
-- ----------------------------
INSERT INTO `role_perm` VALUES (1, 1);

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `user_id` int(32) NOT NULL COMMENT '用戶主鍵',
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '用戶名',
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '密碼(存儲加密後的密碼)',
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'root', '5dbc683c53b7f317fa45c05bf9499fdd');

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`  (
  `user_id` int(32) NOT NULL COMMENT '用戶主鍵',
  `role_id` int(32) NOT NULL COMMENT '角色主鍵'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1);

SET FOREIGN_KEY_CHECKS = 1;

數據庫設計完成之後,將相對應的實體類和 mapper 文件加入到項目當中java

業務代碼

這裏咱們須要定義一個業務接口查詢用戶的相關信息(包括用戶關聯的角色與權限)jquery

這裏不闡述具體的 SQL 語句git

UserService

public interface UserService {

   /**
     * 根據用戶名查詢用戶信息(包含角色及權限信息)
     * @param username 用戶名
     * @return User
     */
    User selectByUsername(String username);
}

UserServiceImpl

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User selectByUsername(String username) {

        return userMapper.selectByUsername(username);
    }
}

引入依賴

pox.xml 中添加 org.apache.shiro:shiro-springcom.github.theborakompanioni:thymeleaf-extras-shiro 依賴github

<properties>
    <thymeleaf-extras-shiro.version>2.0.0</thymeleaf-extras-shiro.version>
    <shiro.version>1.4.0</shiro.version>
</properties>

<dependencies>
    <!-- Shiro核心依賴 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>${shiro.version}</version>
    </dependency>

    <!-- Thymeleaf對Shiro的支持 -->
    <dependency>
      <groupId>com.github.theborakompanioni</groupId>
      <artifactId>thymeleaf-extras-shiro</artifactId>
      <version>${thymeleaf-extras-shiro.version}</version>
    </dependency>
</dependencies>

自定義認證和受權

建立 MyRealm 類實現認證與受權web

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;

/**
 * 自定義Realm,實現受權與認證
 */
public class MyRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    /**
     * 用戶認證
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        User user = userService.selectByUsername(token.getUsername());
        if (user == null) {
            throw new UnknownAccountException();
        }
        return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
    }

    /**
     * 用戶受權
     **/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        Subject subject = SecurityUtils.getSubject();
        User user = (User) subject.getPrincipal();
        if (user != null) {
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            List<String> roles = new LinkedList<>();
            List<String> perms = new LinkedList<>();
            for (Role role : user.getRoleList()) {
                roles.add(role.getRoleName());
            }
            for (Perm perm : user.getPermList()) {
                perms.add(perm.getPermUrl());
            }
            simpleAuthorizationInfo.addRoles(roles);
            simpleAuthorizationInfo.addStringPermissions(perms);
            return simpleAuthorizationInfo;
        }
        return null;
    }
}

Shiro 配置類

建立 ShiroConfig 配置類ajax

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

@Configuration
public class ShiroConfig {

    /**
     * 配置密碼加密
     */
    @Bean("hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {

        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 散列算法(加密)
        credentialsMatcher.setHashAlgorithmName("MD5");
        // 散列次數(加密次數)
        credentialsMatcher.setHashIterations(1);
        // storedCredentialsHexEncoded 默認是true,此時用的是密碼加密用的是Hex編碼;false時用Base64編碼
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    /**
     * 注入自定義的 Realm
     */
    @Bean("MyRealm")
    public MyRealm MyRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {

        MyRealm MyRealm = new MyRealm();
        MyRealm.setCredentialsMatcher(matcher);
        return MyRealm;
    }

    /**
     * 配置自定義權限過濾規則
     */
    @Bean
    public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {

        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        bean.setSuccessUrl("/index.html");
        bean.setLoginUrl("/login.html");
        bean.setUnauthorizedUrl("/unauthorized.html");

        /**
         * anon:匿名用戶可訪問
         * authc:認證用戶可訪問
         * user:使用rememberMe可訪問
         * perms:對應權限可訪問
         * role:對應角色權限可訪問
         **/
        Map<String, String> filterMap = new LinkedHashMap<>();
        /**
         * 容許匿名訪問靜態資源
         */
        filterMap.put("/image/**", "anon");
        filterMap.put("/css/**", "anon");
        filterMap.put("/js/**", "anon");
        filterMap.put("/plugin/**", "anon");
        /**
         * 容許匿名訪問登陸頁面和登陸操做
         */
        filterMap.put("/login.html", "anon");
        filterMap.put("/login.do", "anon");
        /**
         * 其它全部請求須要登陸認證後才能訪問
         */
        filterMap.put("/**", "authc");
        bean.setFilterChainDefinitionMap(filterMap);
        return bean;
    }

    /**
     * 注入 securityManager
     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(HashedCredentialsMatcher hashedCredentialsMatcher, @Qualifier("sessionManager") DefaultWebSessionManager defaultWebSessionManager) {

        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(MyRealm(hashedCredentialsMatcher));
        securityManager.setSessionManager(defaultWebSessionManager);
        return securityManager;
    }

    /**
     * 開啓權限註解
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {

        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {

        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 配置異常跳轉頁面
     */
    @Bean
    public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {

        SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
        Properties properties = new Properties();
        // 未認證跳轉頁面(跳轉路徑爲項目裏的頁面相對路徑,並不是 URL)
        properties.setProperty("org.apache.shiro.authz.UnauthenticatedException", "login");
        // 權限不足跳轉頁面
        properties.setProperty("org.apache.shiro.authz.UnauthorizedException", "unauthorized");
        resolver.setExceptionMappings(properties);
        return resolver;
    }

    /**
     * 會話管理器
     */
    @Bean("sessionManager")
    public DefaultWebSessionManager defaultWebSessionManager() {

        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        // 設置用戶登陸信息失效時間爲一天(單位:ms)
        defaultWebSessionManager.setGlobalSessionTimeout(1000L * 60L * 60L * 24L);
        return defaultWebSessionManager;
    }

    /**
     * 重置 ShiroDialect,省略此步將不能在 Thymeleaf 頁面使用 Shiro 標籤
     */
    @Bean(name = "shiroDialect")
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }
}

Controller

@Controller
public class IndexController {

    @Autowired
    private UserService userService;

    @RequestMapping(value = "login.html")
    public String loginView() {

        // 判斷當前用戶是否經過認證
        if (SecurityUtils.getSubject().isAuthenticated()) {
            // 認證經過,重定向到首頁
            return "redirect:index.html";
        } else {
            // 未認證或認證失敗,轉發到登陸頁
            return "login";
        }
    }

    @RequestMapping(value = "login.do")
    @ResponseBody
    public AppReturn loginDo(@RequestParam String username, @RequestParam String password) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
        try {
            // 執行認證
            subject.login(usernamePasswordToken);
        } catch (UnknownAccountException e) {
            return AppReturn.defeated("帳號不存在");
        } catch (IncorrectCredentialsException e) {
            return AppReturn.defeated("密碼錯誤");
        }
        return AppReturn.succeed("登陸成功");
    }

    @RequestMapping(value = "index.html")
    public String indexView() {
        return "index";
    }

    @RequestMapping(value = "logout.do")
    public String logoutDo() {

        if (SecurityUtils.getSubject().isAuthenticated()) {
            // 退出
            SecurityUtils.getSubject().logout();
        }
        return "redirect:login.html";
    }

    @RequestMapping(value = "unauthorized.html")
    public String unauthorizedView() {

        return "unauthorized";
    }
}
@Controller
public class IndexController {

    @Autowired
    private UserService userService;

    @RequestMapping(value = "login.html")
    public String loginView() {

        // 判斷當前用戶是否經過認證
        if (SecurityUtils.getSubject().isAuthenticated()) {
            // 認證經過,重定向到首頁
            return "redirect:index.html";
        } else {
            // 未認證或認證失敗,轉發到登陸頁
            return "login";
        }
    }

    @RequestMapping(value = "login.do")
    @ResponseBody
    public AppReturn loginDo(@RequestParam String username, @RequestParam String password) {
        return userService.loginDo(username, password);
    }

    @RequestMapping(value = "index.html")
    public String indexView() {
        return "index";
    }

    @RequestMapping(value = "logout.do")
    public String logoutDo() {

        if (SecurityUtils.getSubject().isAuthenticated()) {
            // 退出
            SecurityUtils.getSubject().logout();
        }
        return "redirect:login.html";
    }

    @RequestMapping(value = "unauthorized.html")
    public String unauthorizedView() {

        return "unauthorized";
    }
}

Web 頁面

引入 jquery.js算法

login.html

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陸</title>
</head>
<body>
    <div>
        用戶名:<input id="username" name="username" type="text" /><br/>
        密碼:<input id="password" name="password" type="password"><br/>
        <span id="tip" class="tip"></span><br/>
        <button onclick="login()">點擊登陸</button>
    </div>
</body>

<script type="text/javascript" src="/js/jquery-3.4.1.min.js"></script>
<script type="text/javascript">
    function login() {
        var username = $('#username').val()
        var password = $('#password').val()
        $.ajax({
            url: '/login.do'
            , data: {
                username: username
                , password: password
            }
            , type: 'post'
            , dataType: 'json'
            , success: function(res) {
                if (res.code == 200) {
                    // 登陸成功,跳轉到 index.html
                    window.location.href = '/index.html'
                } else {
                    // 登陸失敗,提示登陸錯誤信息
                    $("#tip").text(res.msg)
                }
            }
            , error: function() {
                $("#tip").text('服務器響應失敗')
            }
        })
    }
</script>
</html>

index.html

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首頁</title>
</head>
<body>
    Hello Shiro
    <a href="/logout.do">退出</a>
</body>
</html>

unauthorized.html

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>無權訪問</title>
</head>
<body>
    權限不足
</body>
</html>

Java 中使用 Shiro 權限註解

除了在 ShiroConfig 配置類中自定義權限過濾規則,還可使用 Shiro 提供的註解實現權限過濾,在 Controller 中的每一個請求方法上能夠添加如下註解實現權限控制:

@RequiresAuthentication: 只有認證經過的用戶才能訪問

@RequiresRoles(value = {「root」}, logical = Logical.OR)

  • value:指定擁有 root 角色才能訪問,角色能夠是多個,以逗號隔開
  • logical:該屬性有兩個值,Logical.OR(只要擁有其中一個角色就能訪問),Logical.AND(須要擁有指定的所有角色才能訪問,不然會拋出權限不足異常)

@RequiresPermissions(value = {「/user/delete」}, logical = Logical.OR)

  • value:指定擁有 /user/delete 權限才能訪問,權限能夠是多個,以逗號隔開
  • logical:有兩個值,Logical.OR(只要擁有其中一個權限就訪問),Logical.AND(須要擁有指定的所有權限才能訪問,不然會拋出權限不足異常)

Thymeleaf 模板中使用 Shiro 權限標籤

修改 thymeleaf 模板的 html 標籤,加入 xmlns:shiro=」http://www.pollix.at/thymeleaf/shiro 命名空間:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

經常使用的 Shiro 標籤有如下:

  • <shiro:hasRole=」root」>:須要擁有root角色
  • <shiro:hasAnyRoles=」root,guest」>:須要擁有root和guest中的任意一個角色
  • <shiro:hasAllRoles =」root,guest」>:須要同時擁有root和guest角色
  • <shiro:hasPerm>:原理同上
  • <shiro:hasAnyPerms> :原理同上
  • <shiro:hasAllPerms> :原理同上

登陸

相關文章
相關標籤/搜索