SpringBoot&Shiro實現權限管理

SpringBoot&Shiro實現權限管理

引言

相信你們前來看這篇文章的時候,是有SpringBoot和Shiro基礎的,因此本文只介紹整合的步驟,若是哪裏寫的很差,懇請你們能指出錯誤,謝謝!依賴以及一些配置文件請在源碼裏參考,請參見 https://github.com/Slags/springboot-learn/tree/master/1.springboot-shiro-authentication ,javascript

我的博客:www.fqcoder.cnhtml

1、數據庫模板設計

在本文中,咱們使用RBAC(Role-Based Access Control,基於角色的訪問控制)模型設計用戶,角色和權限間的關係。簡單地說,一個用戶擁有若干角色,每個角色擁有若干權限。這樣,就構形成「用戶-角色-權限」的受權模型。在這種模型中,用戶與角色之間,角色與權限之間,通常者是多對多的關係。以下圖所示:前端

QQ截圖20171214123601.png

而後咱們在來根據這個模型圖,設計數據庫表,記得本身添加一點測試數據哦java

CREATE TABLE `tb_permission`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;

CREATE TABLE `tb_role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '角色名稱',
  `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '描述',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;

CREATE TABLE `tb_role_permission`  (
  `role_id` int(11) NOT NULL COMMENT '角色id',
  `permission_id` int(11) NOT NULL COMMENT '權限id'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;

CREATE TABLE `tb_user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
  `create_time` datetime(0) DEFAULT NULL,
  `status` int(10) DEFAULT NULL COMMENT '狀態',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;

CREATE TABLE `tb_user_role`  (
  `role_id` int(11) NOT NULL COMMENT '角色id',
  `user_id` int(11) NOT NULL COMMENT '用戶id'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;

2、Pojo設計

咱們建立對應的類,筆者這裏用了lombok插件,記得先安裝插件git

@Data
public class User implements Serializable {
    private Integer id;
    private String username;
    private String password;
    private Date createTime;
    private Integer status;
}

@Data
public class Role  implements Serializable {
    private Integer id;
    private String name;
    private String description;
}

@Data
public class Permission implements Serializable {
    private Integer id;
    private String url;
    private String name;
}

3、Dao層設計

由於咱們只是作一個演示,只涉及到用戶登陸,用戶角色、權限查找,並未實現過多方法github

建立UserMapperRolePermissionMapperUserRoleMapper 三個接口web

注意:記得在Mapper接口上面加一個掃描註解@Mapper或者在boot啓動類上加一個@MapperScan(value = "mapper包路徑")註解ajax

public interface UserMapper {
    @Select("select * from tb_user where username=#{username}")
    User selectByName(String username);
}
--------------------------
    
public interface UserRoleMapper {
    /**
     *
     * 查詢用戶角色(可能一個用戶有多個角色)
     * @param username
     * @return
     */
    @Select("select r.id,r.name,r.description from tb_role r " +
            "left join tb_user_role ur on(r.id = ur.role_id)" +
            "left join tb_user u on(u.id=ur.user_id)" +
            "where u.username =#{username}")
    List<Role> findByUserName(String username);
}

------------------------------------------------
public interface RolePermissionMapper {
    /**
     * 經過角色id查詢權限
     * @param roleId
     * @return
     */
    @Select("select p.id,p.url,p.name from tb_permission p " +
            "left join tb_role_permission rp on(p.id=rp.permission_id)" +
            "left join tb_role r on(r.id=rp.role_id)" +
            "where r.id=#{roleId}")
    List<Permission> findByRoleId(Integer roleId);
}

4、Shiro整合實現思路

好了,前面的一些東西,都是能夠算是準備工做,如今纔是真正開始整合Shiro了,咱們先來屢一下思路,實現認證權限功能主要能夠概括爲3點:算法

1.定義一個ShiroConfig配置類,配置 SecurityManager Bean , SecurityManager爲Shiro的安全管理器,管理着全部Subject;spring

2.在ShiroConfig中配置 ShiroFilterFactoryBean ,它是Shiro過濾器工廠類,依賴SecurityManager ;

3.自定義Realm實現類,包含 doGetAuthorizationInfo()doGetAuthenticationInfo()方法 ,

5、定義ShiroConfig配置類

/**
 * @ClassName ShiroConfig
 * @Description TODO
 * @Author fqCoder
 * @Date 2020/2/29 3:08
 * @Version 1.0
 */
@Configuration
public class ShiroConfig {

    /**
     * 這是shiro的大管家,至關於mybatis裏的SqlSessionFactoryBean
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //登陸
        shiroFilterFactoryBean.setLoginUrl("/login");
        //首頁
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //錯誤頁面,認證不經過跳轉
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        //頁面權限控制
        shiroFilterFactoryBean.setFilterChainDefinitionMap(ShiroFilterMapFactory.shiroFilterMap());
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        return shiroFilterFactoryBean;
    }

    /**
     * web應用管理配置
     * @param shiroRealm
     * @param cacheManager
     * @param manager
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager(Realm shiroRealm, CacheManager cacheManager, RememberMeManager manager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setCacheManager(cacheManager);
        securityManager.setRememberMeManager(manager);//記住Cookie
        securityManager.setRealm(shiroRealm);
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }
    /**
     * session過時控制
     * @return
     * @author fuce
     * @Date 2019年11月2日 下午12:49:49
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager defaultWebSessionManager=new DefaultWebSessionManager();
        // 設置session過時時間3600s
        Long timeout=60L*1000*60;//毫秒級別
        defaultWebSessionManager.setGlobalSessionTimeout(timeout);
        return defaultWebSessionManager;
    }
    /**
     * 加密算法
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");//採用MD5 進行加密
        hashedCredentialsMatcher.setHashIterations(1);//加密次數
        return hashedCredentialsMatcher;
    }

    /**
     * 記住個人配置
     * @return
     */
    @Bean
    public RememberMeManager rememberMeManager() {
        Cookie cookie = new SimpleCookie("rememberMe");
        cookie.setHttpOnly(true);//經過js腳本將沒法讀取到cookie信息
        cookie.setMaxAge(60 * 60 * 24);//cookie保存一天
        CookieRememberMeManager manager=new CookieRememberMeManager();
        manager.setCookie(cookie);
        return manager;
    }
    /**
     * 緩存配置
     * @return
     */
    @Bean
    public CacheManager cacheManager() {
        MemoryConstrainedCacheManager cacheManager=new MemoryConstrainedCacheManager();//使用內存緩存
        return cacheManager;
    }

    /**
     * 配置realm,用於認證和受權
     * @param hashedCredentialsMatcher
     * @return
     */
    @Bean
    public AuthorizingRealm shiroRealm(HashedCredentialsMatcher hashedCredentialsMatcher) {
        MyShiroRealm shiroRealm = new MyShiroRealm();
        //校驗密碼用到的算法
//        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        return shiroRealm;
    }

    /**
     * 啓用shiro方言,這樣能在頁面上使用shiro標籤
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

    /**
     * 啓用shiro註解
     *加入註解的使用,不加入這個註解不生效
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    
}

注意:(當時筆者遇到的一個小問題,貼出來給你們漲姿式)

註解無效,登陸時不會執行驗證角色和權限的方法,只會執行登陸驗證方法,遂查詢資料,得知shiro在subject.login(token)方法時不會執行doGetAuthorizationInfo方法,只有在訪問到有權限驗證的接口時會調用查看權限,因而猜測註解無效,發現shiro的權限註解須要開啓纔能有用,添加在配置文件中加入advisorAutoProxyCreatorgetAuthorizationAttributeSourceAdvisor兩個bean開啓shiro註解,解決問題。

六.建立ShiroFilterMapFactory類

注意:

1.這裏要用LinkedHashMap 保證有序

2.filterChain基於短路機制,即最早匹配原則,

3.像anon、authc等都是Shiro爲咱們實現的過濾器,我給出了一張表,在文章尾附錄,自行查看

/**
 * @ClassName ShiroFilterMapFactory
 * @Description TODO
 * @Author fqCoder
 * @Date 2020/2/29 3:09
 * @Version 1.0
 */
public class ShiroFilterMapFactory {

    public static Map<String, String> shiroFilterMap() {
//      設置路徑映射,注意這裏要用LinkedHashMap 保證有序
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //對全部用戶認證
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/logout", "logout");
        //對全部頁面進行認證
        filterChainDefinitionMap.put("/**", "authc");
        return filterChainDefinitionMap;
    }
}

配置完了ShiroConfig後,實現本身的Realm,而後注入到SecurityManager裏

7、實現自定義Realm類

自定義Realm類須要繼承 AuthorizingRealm 類,實現 doGetAuthorizationInfo()和doGetAuthenticationInfo()方法便可 ,

doGetAuthorizationInfo() 方法是進行受權的方法,獲取角色的權限信息

doGetAuthenticationInfo()方法是進行用戶認證的方法,驗證用戶名和密碼

/**
 * @ClassName MyShiroRealm
 * @Description TODO
 * @Author fqCoder
 * @Date 2020/2/29 3:08
 * @Version 1.0
 */
@Service
public class MyShiroRealm  extends AuthorizingRealm {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private UserRoleMapper userRoleMapper;
    @Autowired
    private RolePermissionMapper rolePermissionMapper;

    /**
     * 獲取用戶角色和權限
     * @param principal
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        if(principal == null){
            throw new AuthorizationException("principals should not be null");
        }
        User userInfo= (User) SecurityUtils.getSubject().getPrincipal();
        System.out.println("用戶-->"+userInfo.getUsername()+"獲取權限中");
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        //用戶獲取角色集
        List<Role> roleList=userRoleMapper.findByUserName(userInfo.getUsername());
        Set<String> roleSet=new HashSet<>();
        for (Role r:roleList){
            Integer roleId=r.getId();//獲取角色id
            simpleAuthorizationInfo.addRole(r.getName());//添加角色名字
            List<Permission> permissionList=rolePermissionMapper.findByRoleId(roleId);
            for (Permission p:permissionList){
                //添加權限
                simpleAuthorizationInfo.addStringPermission(p.getName());
            }
        }

        System.out.println("角色爲-> " + simpleAuthorizationInfo.getRoles());
        System.out.println("權限爲-> " + simpleAuthorizationInfo.getStringPermissions());
        return simpleAuthorizationInfo;
    }

    /**
     * 登陸認證
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        //獲取用戶輸入的用戶名密碼
        String username= (String) token.getPrincipal();
        String password=new String((char[])token.getCredentials());

        System.out.println("用戶輸入--->username:"+username+"-->password:"+password);

        //在數據庫中查詢
        User userInfo=userMapper.selectByName(username);
        if (userInfo == null) {
            throw new UnknownAccountException("用戶名或密碼錯誤!");
        }
        if (!password.equals(userInfo.getPassword())) {
            throw new IncorrectCredentialsException("用戶名或密碼錯誤!");
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                userInfo, // 用戶名
                userInfo.getPassword(), // 密碼
                getName() // realm name
        );
        return authenticationInfo;
    }
}

其中UnknownAccountException等異常爲Shiro自帶異常,Shiro具備豐富的運行時AuthenticationException層次結構,能夠準確指出嘗試失敗的緣由。

8、控制層設計

1.建立一個LoginController.class類

用來處理登陸訪問請求

/**
 * @ClassName LoginController
 * @Description TODO
 * @Author fqCoder
 * @Date 2020/2/29 6:06
 * @Version 1.0
 */
@Controller
public class LoginController {

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

    @GetMapping("/")
    public String home(){
        return "redirect:/index";
    }

    @GetMapping("/index")
    public String index(Model model){
        User user= (User) SecurityUtils.getSubject().getPrincipal();
        model.addAttribute("user",user);
        return "index";
    }

    @PostMapping("login")
    @ResponseBody
    public AjaxResult login(User user,Boolean rememberMe){
        System.out.println("user = " + user);
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
        //獲取Subject 對象
        Subject subject= SecurityUtils.getSubject();
        try {
            if (rememberMe){
                token.setRememberMe(true);
            }
            subject.login(token);
            return AjaxResult.success("/index");
        } catch (UnknownAccountException e) {
            return AjaxResult.error(e.getMessage());
        } catch (IncorrectCredentialsException e) {
            return AjaxResult.error(e.getMessage());
        }
    }
    @GetMapping("/403")
    public String forbid(){
        return "403";
    }
}

2.建立一個UserController.class類

用於處理User類的訪問請求,並使用Shiro權限註解控制權限:

/**
 * @ClassName UserController
 * @Description TODO
 * @Author fqCoder
 * @Date 2020/3/3 15:14
 * @Version 1.0
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @RequiresPermissions("user:queryAll")
    @GetMapping("/queryAll")
    public String queryAll(){

        //只演示框架...功能不實現
        return "查詢列表";
    }

    @RequiresPermissions("user:add")
    @GetMapping("/add")
    public String userAdd(){
        return "添加用戶";
    }

    @RequiresPermissions("user:delete")
    @GetMapping("/delete")
    public String userDelete(){
        return "刪除用戶";
    }
}

9、前端頁面設計

1.編寫login.html頁面

這裏我只貼重要代碼,具體的代碼,到這裏找哦!

<form id="loginForm">
    <input type="text" id="username" name="username" class="text"  />
    <input type="password" id="password" name="password"  />
</form>
<div class="signin">
    <input id="loginBut" type="button" value="Login" >
</div>

-------js代碼----
<script type="text/javascript">
    $.fn.serializeObject = function () {
        var o = {};
        var a = this.serializeArray();
        $.each(a, function () {
            if (o[this.name]) {
                if (!o[this.name].push) {
                    o[this.name] = [o[this.name]];
                }
                o[this.name].push(this.value);
            } else {
                o[this.name] = this.value || '';
            }
        });
        return o;
    };

    $(function () {
        $("#loginBut").click(function () {
            var  arr=$('#loginForm').serializeObject();
            $.ajax({
                url: '/login',
                type: 'post',
                data:  arr,
                dataType: "json",
                success: function (data) {
                    if (data.code==200){
                        location.href=data.msg;
                    } else {
                        alert(data.msg);
                    }
                },
                error: function (data) {
                    alert(data.msg);
                }
            })
        });
    });
</script>

當用戶登陸進來的時候調到index.html

2.編寫index.html頁面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首頁</title>
</head>
<body>
<h1>番茄歡迎您!</h1>
登陸用戶:【[[${user.username}]]】
<a th:href="@{/logout}">註銷</a>


<h2>權限測試</h2>
<a th:href="@{/user/queryAll}">獲取用戶所有信息</a>
<a th:href="@{/user/add}">添加用戶</a>
<a th:href="@{/user/delete}">刪除用戶</a>
</body>
</html>

3.編寫403頁面

比較簡單,此處能用就行

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>403</title>
</head>
<body>
<h1>403權限不夠</h1>
<a href="/index">首頁</a>
</body>
</html>

10、測試&問題

啓動項目:訪問http://localhost:8080/,它會自動攔截,頁面重定向到 http://localhost:8080/login ,登陸成功跳轉到http://localhost:8080/index

問題:

登陸測試用戶的時候,訪問沒有權限的連接請求時,後臺拋出org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method異常

當時覺得在ShiroConfig配置類中配置了shiroFilterFactoryBean.setUnauthorizedUrl("/403");

沒有權限的請求會自動從定向到/403,而後倒是拋出了異常,後來在一篇文章中看到了,說這個設置只對filterChain起做用 ,針對這個問題,咱們能夠定義一個全局異常捕獲類:

@ControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {
    @ExceptionHandler(value = AuthorizationException.class)
    public String handleAuthorizationException() {
        return "403";
    }
}

而後再啓動項目,登陸測試帳號,訪問沒有權限的請求,頁面成功定向到/403

源碼連接: https://github.com/Slags/springboot-learn/tree/master/1.springboot-shiro-authentication

至此,筆者剛開始寫,不是寫的很好,歡迎各位網友踊躍指出不足,謝謝!

附錄:

1.Shiro攔截機制表

Filter Name Class Description
anon org.apache.shiro.web.filter.authc.AnonymousFilter 匿名攔截器,即不須要登陸便可訪問;通常用於靜態資源過濾;示例/static/**=anon
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter 基於表單的攔截器;如/**=authc,若是沒有登陸會跳到相應的登陸頁面登陸
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter Basic HTTP身份驗證攔截器
logout org.apache.shiro.web.filter.authc.LogoutFilter 退出攔截器,主要屬性:redirectUrl:退出成功後重定向的地址(/),示例/logout=logout
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter 不建立會話攔截器,調用subject.getSession(false)不會有什麼問題,可是若是subject.getSession(true)將拋出DisabledSessionException異常
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter 權限受權攔截器,驗證用戶是否擁有全部權限;屬性和roles同樣;示例/user/**=perms["user:create"]
port org.apache.shiro.web.filter.authz.PortFilter 端口攔截器,主要屬性port(80):能夠經過的端口;示例/test= port[80],若是用戶訪問該頁面是非80,將自動將請求端口改成80並重定向到該80端口,其餘路徑/參數等都同樣
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter rest風格攔截器,自動根據請求方法構建權限字符串;示例/users=rest[user],會自動拼出user:read,user:create,user:update,user:delete權限字符串進行權限匹配(全部都得匹配,isPermittedAll)
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 角色受權攔截器,驗證用戶是否擁有全部角色;示例/admin/**=roles[admin]
ssl org.apache.shiro.web.filter.authz.SslFilter SSL攔截器,只有請求協議是https才能經過;不然自動跳轉會https端口443;其餘和port攔截器同樣;
user org.apache.shiro.web.filter.authc.UserFilter 用戶攔截器,用戶已經身份驗證/記住我登陸的均可;示例/**=user
相關文章
相關標籤/搜索