Springboot整合Shiro: 详细的权限管理(编程式和注解式)

该篇博客主要内容:编程式授权方式、注解式授权方式、403页面配置、前端shiro标签使用
接上一篇博客 > Springboot整合Shiro:简洁的身份认证
*******完整代码在文章最下面,转载请说明出处,谢谢 ********

Shiro的授权流程跟认证流程类似:

  1. 创建SecurityManager安全管理器
  2. Subject主体带授权信息执行授权,请求到SecurityManager
  3. SecurityManager安全管理器调用Authorizer授权
  4. Authorizer结合主体一步步传过来的授权信息与Realm中的数据比对,授权

(1)我们已经在ShiroConfig配置类中配置好了SecurityManager安全管理器

 /**
     * 配置Shiro核心 安全管理器 SecurityManager
     * SecurityManager安全管理器:所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;负责与后边介绍的其他组件进行交互。(类似于SpringMVC中的DispatcherServlet控制器)
     */
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //将自定义的realm交给SecurityManager管理
        securityManager.setRealm(new CustomRealm());
        return securityManager;
    }

(2)下面对自定义Realm类CustomRealm中的授权方法doGetAuthorizationInfo进行重写

 @Override
    /**
     * 授权
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        //获取当前登录的用户
        User user = (User) principal.getPrimaryPrincipal();
        //通过SimpleAuthenticationInfo做授权
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //添加角色
        simpleAuthorizationInfo.addRole(user.getRole());
        //添加权限
        simpleAuthorizationInfo.addStringPermissions(user.getPermissions());
        return simpleAuthorizationInfo;
    }

上面主要通过SimpleAuthorizationInfo中的addRoleaddStringPermissions添加当前用户拥有的角色和权限,与主体的授权信息进行比对。
(3)主体调用授权请求
主体进行授权请求有两种方式,一种是编程式,一种是注解式。
①编程式:通过SubjecthasRole()进行角色的校检,通过isPermitted()进行权限的校检

@GetMapping("/dog")
    public String dog(){
        Subject subject = SecurityUtils.getSubject();
        if(subject.hasRole("dog")){
            return "dog√";
        }
       else {
           return  "dog×";
        }
    }

    @GetMapping("/cat")
    public String cat(){
        Subject subject = SecurityUtils.getSubject();
        if(subject.hasRole("cat")){
            return "cat√";
        }
        else {
            return  "cat×";
        }
    }
 @GetMapping("/rap")
    public String rap(){
        Subject subject = SecurityUtils.getSubject();
        if(subject.isPermitted("rap")){
            return "rap";
        }else{
            return "没权限你Rap个锤子啊!";
        }

模拟用户数据如下:

/**
     * 模拟数据库数据
     * @return
     */
    private List<User> getUsers(){
        List<User> users = new ArrayList<>(2);
        List<String> cat = new ArrayList<>(3);
        cat.add("sing");
        cat.add("rap");
        List<String> dog = new ArrayList<>(3);
        dog.add("jump");
        dog.add("basketball");
        users.add(new User("张小黑的猫","123qwe",true,"cat",cat));
        users.add(new User("张小黑的狗","123qwe",true,"dog",dog));
        return users;
    }

测试结果如图:


authorization.gif

②注解式:
首先需要开启Aop注解,在ShiroConfig类中新增如下方法:

 /**
     * 开启aop注解支持
     * 即在controller中使用 @RequiresPermissions("user/userList")
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        //设置安全管理器
        attributeSourceAdvisor.setSecurityManager(securityManager);
        return attributeSourceAdvisor;
    }

    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

对于未授权的用户需要进行友好的提示,一般返回特定的403页面,在ShiroConfig类中新增如下方法实现:

 /**
     * 处理未授权的异常,返回自定义的错误页面(403)
     * @return
     */
    @Bean
    public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
        Properties properties = new Properties();
        /*未授权处理页*/
        properties.setProperty("UnauthorizedException", "403.html");
        resolver.setExceptionMappings(properties);
        return resolver;
    }

解释下上面的代码properties.setProperty("UnauthorizedException","403.html");用来对抓取到UnauthorizedException未授权异常进行处理,重定向到403.html页面。我们需要创建403页面403.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>未授权</title>
</head>
<body>
403,您没有访问权限
</body>
</html>

然后在Controller使用注解@RequiresRoles("xxx")@RequiresPermissions("xxx")进行角色和权限的校检

    @GetMapping("/sing")
    @RequiresRoles("cat")
    public String sing(){
        return "sing";
    }
    @GetMapping("/jump")
    @RequiresPermissions("jump")
    public String jump(){
        return "jump";
    }

测试结果如图:


authorization2.gif

总结:一般对未授权用户需要统一返回指定403页面的,使用注解更加方便;需要做业务逻辑(比如对未授权的请求记录进行预警等),使用编程式更加方便。
(4)前端的shiro标签
通常前端页面展示需要与用户的权限对等,即只给用户看到他们权限内的内容。(比如用户"张小黑的猫",对应角色“cat”,对应权限“sing”和“rap”,那么该用户登录后只展示Cat、sing和rap的按钮)
通常解决方式有两种:
其一:登录后通过读取数据库中角色和权限,获取需要展示的菜单内容,动态的在前端渲染;
其二:所有内容都在前端写好,通过前端的shiro标签控制对应权限内容部分的渲染。
这里给大家演示shiro标签的使用。(前端的shiro标签有Jsp标签、Freemarker标签、Thymeleaf标签等,演示用的为thymeleaf标签)

Shiro标签说明:
guest标签:`<shiro:guest></shiro:guest>`,用户没有身份验证时显示相应信息,即游客访问信息。
user标签:`<shiro:user></shiro:user>`,用户已经身份验证/记住我登录后显示相应的信息。
authenticated标签:`<shiro:authenticated></shiro:authenticated>`,用户已经身份验证通过,即Subject.login登录成功,不是记住我登录的。
notAuthenticated标签:`<shiro:notAuthenticated></shiro:notAuthenticated>`,用户已经身份验证通过,即没有调用Subject.login进行登录,包括记住我自动登录的也属于未进行身份验证。
principal标签:`<shiro: principal/><shiro:principal property="username"/>`,相当`((User)Subject.getPrincipals()).getUsername()`。
lacksPermission标签:`<shiro:lacksPermission name="org:create"></shiro:lacksPermission>`,如果当前Subject没有权限将显示body体内容。
hasRole标签:`<shiro:hasRole name="admin"></shiro:hasRole>`,如果当前Subject有角色将显示body体内容。
hasAnyRoles标签:`<shiro:hasAnyRoles name="admin,user"></shiro:hasAnyRoles>`,如果当前Subject有任意一个角色(或的关系)将显示body体内容。
lacksRole标签:`<shiro:lacksRole name="abc"></shiro:lacksRole>`,如果当前Subject没有角色将显示body体内容。
hasPermission标签:`<shiro:hasPermission name="user:create"></shiro:hasPermission>`,如果当前Subject有权限将显示body体内容

使用:
①pom.xml引入相应的依赖

 <!-- 兼容于thymeleaf的shiro -->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>

注意:这里引用的版本是2.0.0,之前1.0.2有兼容问题
②ShiroConfig中加入配置

 /**
     * 用于thymeleaf模板使用shiro标签
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

③前端页面使用shiro标签

<!--使用标签后 -->
<shiro:hasRole name="dog"><a href="/dog">Dog</a></shiro:hasRole>
<shiro:hasRole name="cat"><a href="/cat">Cat</a></shiro:hasRole>
<hr>
<shiro:hasPermission name="sing"><a href="/sing">Sing</a></shiro:hasPermission>
<shiro:hasPermission name="jump"><a href="/jump">Jump</a></shiro:hasPermission>
<shiro:hasPermission name="rap"><a href="/rap">Rap</a></shiro:hasPermission>
<shiro:hasPermission name="basketball"><a href="/basketball">Basketball</a></shiro:hasPermission>

注意:使用前现在html标签内引入shiro标签,即<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
④测试结果展示:

shiro-tag.gif

----------------------------------------------到这里,shiro授权基本搞定了---------------------------------------------------------
下面附上项目结构和代码:
image.png

AuthorizationController.java:

/**
 * @Description
 * @Author 张小黑的猫
 * @data 2019-05-23 16:26
 */
@RestController
public class AuthorizationController {

    @GetMapping("/dog")
    public String dog(){
        Subject subject = SecurityUtils.getSubject();
        if(subject.hasRole("dog")){
            return "dog√";
        }
       else {
           return  "dog×";
        }
    }

    @GetMapping("/cat")
    public String cat(){
        Subject subject = SecurityUtils.getSubject();
        if(subject.hasRole("cat")){
            return "cat√";
        }
        else {
            return  "cat×";
        }
    }


    @GetMapping("/sing")
    @RequiresRoles("cat")
    public String sing(){
        return "sing";
    }
    @GetMapping("/jump")
    @RequiresPermissions("jump")
    public String jump(){
        return "jump";
    }
    @GetMapping("/rap")
    public String rap(){
        Subject subject = SecurityUtils.getSubject();
        if(subject.isPermitted("rap")){
            return "rap";
        }else{
            return "没权限你Rap个锤子啊!";
        }

    }
    @GetMapping("/basketball")
    public String basketball(){
        Subject subject = SecurityUtils.getSubject();
        if(subject.isPermitted("basketball")){
            return "basketball";
        }else{
            return "你会打个粑粑球!";
        }
    }
}

CustomRealm.java:


/**
 * @Description: shiro的自定义realm
 * Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
 * @Author 张小黑的猫
 * @data 2019-05-22 17:51
 */
public class CustomRealm extends AuthorizingRealm {
    @Override
    /**
     * 认证
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1.获取用户输入的账号
        String username = (String)token.getPrincipal();
        //2.通过username从数据库中查找到user实体
         User user = getUserByUserName(username);
        if(user == null){
            return null;
        }
        //3.通过SimpleAuthenticationInfo做身份处理
        SimpleAuthenticationInfo simpleAuthenticationInfo =
                new SimpleAuthenticationInfo(user,user.getPassword(),getName());
        //4.用户账号状态验证等其他业务操作
        if(!user.getAvailable()){
            throw new AuthenticationException("该账号已经被禁用");
        }
        //5.返回身份处理对象
        return simpleAuthenticationInfo;
    }

    @Override
    /**
     * 授权
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        //获取当前登录的用户
        User user = (User) principal.getPrimaryPrincipal();
        //通过SimpleAuthenticationInfo做授权
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //添加角色
        simpleAuthorizationInfo.addRole(user.getRole());
        //添加权限
        simpleAuthorizationInfo.addStringPermissions(user.getPermissions());
        return simpleAuthorizationInfo;
    }

    /**
     * 模拟通过username从数据库中查找到user实体
     * @param username
     * @return
     */
    private User getUserByUserName(String username){
        List<User> users = getUsers();
        for(User user : users){
            if(user.getUsername().equals(username)){
                return user;
            }
        }
        return null;
    }

    /**
     * 模拟数据库数据
     * @return
     */
    private List<User> getUsers(){
        List<User> users = new ArrayList<>(2);
        List<String> cat = new ArrayList<>(2);
        cat.add("sing");
        cat.add("rap");
        List<String> dog = new ArrayList<>(2);
        dog.add("jump");
        dog.add("basketball");
        users.add(new User("张小黑的猫","123qwe",true,"cat",cat));
        users.add(new User("张小黑的狗","123qwe",true,"dog",dog));
        return users;
    }
}

ShiroConfig.java:

/**
 * @Description springboot中的Shiro配置类
 * @Author 张小黑的猫
 * @data 2019-05-22 17:17
 */
@Configuration
public class ShiroConfig {

    /**
     * 配置Shiro核心 安全管理器 SecurityManager
     * SecurityManager安全管理器:所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;负责与后边介绍的其他组件进行交互。(类似于SpringMVC中的DispatcherServlet控制器)
     */
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //将自定义的realm交给SecurityManager管理
        securityManager.setRealm(new CustomRealm());
        return securityManager;
    }


    /**
     * 配置Shiro的Web过滤器,拦截浏览器请求并交给SecurityManager处理
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean webFilter(SecurityManager securityManager){

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //配置拦截链 使用LinkedHashMap,因为LinkedHashMap是有序的,shiro会根据添加的顺序进行拦截
        // Map<K,V> K指的是拦截的url V值的是该url是否拦截
        Map<String,String> filterChainMap = new LinkedHashMap<String,String>(16);
        //配置退出过滤器logout,由shiro实现
        filterChainMap.put("/logout","logout");
        //authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问,先配置anon再配置authc。
        filterChainMap.put("/login","anon");
        filterChainMap.put("/**", "authc");

        //设置默认登录的URL.
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 开启aop注解支持
     * 即在controller中使用 @RequiresPermissions("user/userList")
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        //设置安全管理器
        attributeSourceAdvisor.setSecurityManager(securityManager);
        return attributeSourceAdvisor;
    }

    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    /**
     * 处理未授权的异常,返回自定义的错误页面(403)
     * @return
     */
    @Bean
    public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
        Properties properties = new Properties();
        /*未授权处理页*/
        properties.setProperty("UnauthorizedException", "403.html");
        resolver.setExceptionMappings(properties);
        return resolver;
    }

    /**
     * 用于thymeleaf模板使用shiro标签
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }
}

User.java:

/**
 * @Description 用户
 * @Author 张小黑的猫
 * @data 2019-05-22 19:18
 */
public class User {
    private String username;
    private String password;
    private Boolean available;
    private String role;
    private List<String> permissions;

    public User(String username, String password, Boolean available, String role, List<String> permissions) {
        this.username = username;
        this.password = password;
        this.available = available;
        this.role = role;
        this.permissions = permissions;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Boolean getAvailable() {
        return available;
    }

    public void setAvailable(Boolean available) {
        this.available = available;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }

    public List<String> getPermissions() {
        return permissions;
    }

    public void setPermissions(List<String> permissions) {
        this.permissions = permissions;
    }
}

success.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>success</title>
</head>
<body>
<span th:text="'欢迎你,'+${username}"></span>
<br>
<!--使用标签前 -->
<!--<a href="/dog">Dog</a>-->
<!--<a href="/cat">Cat</a>-->
<!--<hr>-->
<!--<a href="/sing">Sing</a>-->
<!--<a href="/jump">Jump</a>-->
<!--<a href="/rap">Rap</a>-->
<!--<a href="/basketball">Basketball</a>-->
<!--<hr>-->

<!--使用标签后 -->
<shiro:hasRole name="dog"><a href="/dog">Dog</a></shiro:hasRole>
<shiro:hasRole name="cat"><a href="/cat">Cat</a></shiro:hasRole>
<hr>
<shiro:hasPermission name="sing"><a href="/sing">Sing</a></shiro:hasPermission>
<shiro:hasPermission name="jump"><a href="/jump">Jump</a></shiro:hasPermission>
<shiro:hasPermission name="rap"><a href="/rap">Rap</a></shiro:hasPermission>
<shiro:hasPermission name="basketball"><a href="/basketball">Basketball</a></shiro:hasPermission>

</body>
</html>

共同学习,欢迎指正修改~ 喵喵喵❤
下一篇文章:Springboot整合Shiro: 动态权限配置

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,302评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,563评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,433评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,628评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,467评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,354评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,777评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,419评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,725评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,768评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,543评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,387评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,794评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,032评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,305评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,741评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,946评论 2 336