美文网首页springbootSpring Cloud JAVA
SpringBoot+shiro整合学习之登录认证和权限控制

SpringBoot+shiro整合学习之登录认证和权限控制

作者: z77z | 来源:发表于2017-02-13 22:53 被阅读41723次

学习任务目标

  1. 用户必须要登陆之后才能访问定义链接,否则跳转到登录页面。

  2. 对链接进行权限控制,只有当当前登录用户有这个链接访问权限才可以访问,否则跳转到指定页面。

  3. 输入错误密码用户名或则用户被设置为静止登录,返回相应json串信息。

我是用的是之前搭建的一个springboot+mybatisplus+jsp的一个基础框架。在这之上进行shiro的整合。需要的同学可以去我的码云下载。

个人博客:http://z77z.oschina.io/

此项目下载地址:https://git.oschina.net/z77z/springboot_mybatisplus

导入shiro依赖包到pom.xml

<!-- shiro权限控制框架 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
</dependency>

采用RBAC模式建立数据库

RBAC 是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。

/*表结构插入*/
DROP TABLE IF EXISTS `u_permission`;

CREATE TABLE `u_permission` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `url` varchar(256) DEFAULT NULL COMMENT 'url地址',
  `name` varchar(64) DEFAULT NULL COMMENT 'url描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;

/*Table structure for table `u_role` */

DROP TABLE IF EXISTS `u_role`;

CREATE TABLE `u_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL COMMENT '角色名称',
  `type` varchar(10) DEFAULT NULL COMMENT '角色类型',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

/*Table structure for table `u_role_permission` */

DROP TABLE IF EXISTS `u_role_permission`;

CREATE TABLE `u_role_permission` (
  `rid` bigint(20) DEFAULT NULL COMMENT '角色ID',
  `pid` bigint(20) DEFAULT NULL COMMENT '权限ID'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Table structure for table `u_user` */

DROP TABLE IF EXISTS `u_user`;

CREATE TABLE `u_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `nickname` varchar(20) DEFAULT NULL COMMENT '用户昵称',
  `email` varchar(128) DEFAULT NULL COMMENT '邮箱|登录帐号',
  `pswd` varchar(32) DEFAULT NULL COMMENT '密码',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
  `status` bigint(1) DEFAULT '1' COMMENT '1:有效,0:禁止登录',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8;

/*Table structure for table `u_user_role` */

DROP TABLE IF EXISTS `u_user_role`;

CREATE TABLE `u_user_role` (
  `uid` bigint(20) DEFAULT NULL COMMENT '用户ID',
  `rid` bigint(20) DEFAULT NULL COMMENT '角色ID'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Dao层代码的编写

Dao层的entity,service,mapper等我是采用mybatisplus的代码自动生成工具生成的,具备了单表的增删改查功能和分页功能,比较方便,这里我就不贴代码了。

配置shiro

ShiroConfig.java

/**
 * @author 作者 z77z
 * @date 创建时间:2017年2月10日 下午1:16:38
 * 
 */
@Configuration
public class ShiroConfig {
    /**
     * ShiroFilterFactoryBean 处理拦截资源文件问题。
     * 注意:单独一个ShiroFilterFactoryBean配置是或报错的,以为在
     * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
     *
     * Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过
     * 3、部分过滤器可指定参数,如perms,roles
     *
     */
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        // 未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        // 拦截器.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/ajaxLogin", "anon");

        // 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterChainDefinitionMap.put("/logout", "logout");

        filterChainDefinitionMap.put("/add", "perms[权限添加]");

        // <!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
        // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
        filterChainDefinitionMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        System.out.println("Shiro拦截器工厂类注入成功");
        return shiroFilterFactoryBean;
    }

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

    /**
     * 身份认证realm; (这个需要自己写,账号密码校验;权限等)
     * 
     * @return
     */
    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        return myShiroRealm;
    }
}

登录认证实现

在认证、授权内部实现机制中都有提到,最终处理都将交给Real进行处理。因为在Shiro中,最终是通过Realm来获取应用程序中的用户、角色及权限信息的。通常情况下,在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。可以说,Realm是专用于安全框架的DAO.

Shiro的认证过程最终会交由Realm执行,这时会调用Realm的getAuthenticationInfo(token)方法。
该方法主要执行以下操作:

1、检查提交的进行认证的令牌信息

2、根据令牌信息从数据源(通常为数据库)中获取用户信息

3、对用户信息进行匹配验证。

4、验证通过将返回一个封装了用户信息的AuthenticationInfo实例。

5、验证失败则抛出AuthenticationException异常信息。

而在我们的应用程序中要做的就是自定义一个Realm类,继承AuthorizingRealm抽象类,重载doGetAuthenticationInfo
(),重写获取用户信息的方法。

doGetAuthenticationInfo的重写

/**
* 认证信息.(身份验证) : Authentication 是用来验证用户身份
 * 
 * @param token
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
        AuthenticationToken authcToken) throws AuthenticationException {
    System.out.println("身份认证方法:MyShiroRealm.doGetAuthenticationInfo()");

    ShiroToken token = (ShiroToken) authcToken;
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("nickname", token.getUsername());
    map.put("pswd", token.getPswd());
    SysUser user = null;
    // 从数据库获取对应用户名密码的用户
    List<SysUser> userList = sysUserService.selectByMap(map);
    if(userList.size()!=0){
        user = userList.get(0);
    }
    if (null == user) {
        throw new AccountException("帐号或密码不正确!");
    }else if(user.getStatus()==0){
        /**
         * 如果用户的status为禁用。那么就抛出<code>DisabledAccountException</code>
         */
        throw new DisabledAccountException("帐号已经禁止登录!");
    }else{
        //更新登录时间 last login time
        user.setLastLoginTime(new Date());
        sysUserService.updateById(user);
    }
    return new SimpleAuthenticationInfo(user, user.getPswd(), getName());
}

通俗的说,这个的重写就是我们第一个学习目标的实现。

链接权限的实现

shiro的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();

当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行,所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回null即可。

在这个方法中主要是使用类:SimpleAuthorizationInfo

进行角色的添加和权限的添加。

authorizationInfo.addRole(role.getRole());

authorizationInfo.addStringPermission(p.getPermission());

当然也可以添加set集合:roles是从数据库查询的当前用户的角色,stringPermissions是从数据库查询的当前用户对应的权限

authorizationInfo.setRoles(roles);

authorizationInfo.setStringPermissions(stringPermissions);

就是说如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "perms[权限添加]");
就说明访问/add这个链接必须要有“权限添加”这个权限才可以访问,

如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "roles[100002],perms[权限添加]");
就说明访问/add这个链接必须要有“权限添加”这个权限和具有“100002”这个角色才可以访问。

/**
* 授权
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
        PrincipalCollection principals) {
    System.out.println("权限认证方法:MyShiroRealm.doGetAuthenticationInfo()");
    SysUser token = (SysUser)SecurityUtils.getSubject().getPrincipal();
    String userId = token.getId();
    SimpleAuthorizationInfo info =  new SimpleAuthorizationInfo();
    //根据用户ID查询角色(role),放入到Authorization里。
    /*Map<String, Object> map = new HashMap<String, Object>();
    map.put("user_id", userId);
    List<SysRole> roleList = sysRoleService.selectByMap(map);
    Set<String> roleSet = new HashSet<String>();
    for(SysRole role : roleList){
        roleSet.add(role.getType());
    }*/
    //实际开发,当前登录用户的角色和权限信息是从数据库来获取的,我这里写死是为了方便测试
    Set<String> roleSet = new HashSet<String>();
    roleSet.add("100002");
    info.setRoles(roleSet);
    //根据用户ID查询权限(permission),放入到Authorization里。
    /*List<SysPermission> permissionList = sysPermissionService.selectByMap(map);
    Set<String> permissionSet = new HashSet<String>();
    for(SysPermission Permission : permissionList){
        permissionSet.add(Permission.getName());
    }*/
    Set<String> permissionSet = new HashSet<String>();
    permissionSet.add("权限添加");
    info.setStringPermissions(permissionSet);
       return info;
}

这个类的实现是完成了我们学习目标的第二个任务。

编写web层的代码

登录页面:

controller

//跳转到登录表单页面
@RequestMapping(value="login")
public String login() {
    return "login";
}

/**
 * ajax登录请求
 * @param username
 * @param password
 * @return
 */
@RequestMapping(value="ajaxLogin",method=RequestMethod.POST)
@ResponseBody
public Map<String,Object> submitLogin(String username, String password,Model model) {
    Map<String, Object> resultMap = new LinkedHashMap<String, Object>();
    try {
        
        ShiroToken token = new ShiroToken(username, password);
        SecurityUtils.getSubject().login(token);
        resultMap.put("status", 200);
        resultMap.put("message", "登录成功");

    } catch (Exception e) {
        resultMap.put("status", 500);
        resultMap.put("message", e.getMessage());
    }
    return resultMap;
}

jsp

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://"
            + request.getServerName() + ":" + request.getServerPort()
            + path;
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript"
    src="<%=basePath%>/static/js/jquery-1.11.3.js"></script>
<title>登录</title>
</head>
<body>
    错误信息:
    <h4 id="erro"></h4>
    <form>
        <p>
            账号:<input type="text" name="username" id="username" value="admin" />
        </p>
        <p>
            密码:<input type="text" name="password" id="password" value="123" />
        </p>
        <p>
            <input type="button" id="ajaxLogin" value="登录" />
        </p>
    </form>
</body>
<script>
    var username = $("#username").val();
    var password = $("#password").val();
    $("#ajaxLogin").click(function() {
        $.post("/ajaxLogin", {
            "username" : username,
            "password" : password
        }, function(result) {
            if (result.status == 200) {
                location.href = "/index";
            } else {
                $("#erro").html(result.message);
            }
        });
    });
</script>
</html>

主页页面

controller

//跳转到主页
@RequestMapping(value="index")
public String index() {
    return "index";
}

/**
* 退出
 * @return
 */
@RequestMapping(value="logout",method =RequestMethod.GET)
@ResponseBody
public Map<String,Object> logout(){
    Map<String, Object> resultMap = new LinkedHashMap<String, Object>();
    try {
        //退出
        SecurityUtils.getSubject().logout();
    } catch (Exception e) {
        System.err.println(e.getMessage());
    }
    return resultMap;
}

jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://"
            + request.getServerName() + ":" + request.getServerPort()
            + path;
%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript"
    src="<%=basePath%>/static/js/jquery-1.11.3.js"></script>
<title>Insert title here</title>
</head>
<body>
    helloJsp
    <input type="button" id="logout" value="退出登录" />
</body>
<script type="text/javascript">
    $("#logout").click(function(){
        location.href="/logout";
    });
</script>
</html>

添加操作页面

controller

@RequestMapping(value="add")
public String add() {
    return "add";
}

jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://"
            + request.getServerName() + ":" + request.getServerPort()
            + path;
%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript"
    src="<%=basePath%>/static/js/jquery-1.11.3.js"></script>
<title>Insert title here</title>
</head>
<body>
具有添加权限
</body>
</html>

测试

任务一

编写好后就可以启动程序,访问index页面,由于没有登录就会跳转到login页面。

登录之后就会跳转到index页面,点击退出登录后,有直接在浏览器中输入index页面访问,又会跳转到login页面

上面这些操作时候触发MyShiroRealm.doGetAuthenticationInfo()这个方法,也就是登录认证的方法。


任务二

登录之后访问add页面成功访问,在shiro配置文件中改变add的访问权限为

filterChainDefinitionMap.put("/add","perms[权限删除]");

再重新启动程序,登录后访问,会重定向到/403页面,由于没有编写403页面,报404错误。

上面这些操作,会触发权限认证方法:MyShiroRealm.doGetAuthorizationInfo(),每访问一次就会触发一次。


任务三

输入错误的用户名或则密码,返回“帐号或密码不正确!”的错误信息,在数据库中把一个用户的状态改为被禁用,再登陆,提示“帐号已经禁止登录!”的错误信息

上面的操作,是在MyShiroRealm.doGetAuthenticationInfo()登录认证的方法中实现的,通过查询数据库判断当前登录用户是否被禁用,具体可以去看源码。

总结

当然shiro很强大,这仅仅是完成了登录认证和权限管理这两个功能,接下来我会继续学习和分享,说说接下来的学习路线吧:

  1. shiro+redis集成,避免每次访问有权限的链接都会去执行MyShiroRealm.doGetAuthenticationInfo()方法来查询当前用户的权限,因为实际情况中权限是不会经常变得,这样就可以使用redis进行权限的缓存。

  2. 实现shiro链接权限的动态加载,之前要添加一个链接的权限,要在shiro的配置文件中添加filterChainDefinitionMap.put("/add", "roles[100002],perms[权限添加]"),这样很不方便管理,一种方法是将链接的权限使用数据库进行加载,另一种是通过init配置文件的方式读取。

  3. Shiro 自定义权限校验Filter定义,及功能实现。

  4. Shiro Ajax请求权限不满足,拦截后解决方案。这里有一个前提,我们知道Ajax不能做页面redirect和forward跳转,所以Ajax请求假如没登录,那么这个请求给用户的感觉就是没有任何反应,而用户又不知道用户已经退出了。

  5. Shiro JSP标签使用。

  6. Shiro 登录后跳转到最后一个访问的页面

  7. 在线显示,在线用户管理(踢出登录)。

  8. 登录注册密码加密传输。

  9. 集成验证码。

  10. 记住我的功能。关闭浏览器后还是登录状态。

  11. 还有没有想到的后面再说,欢迎大家提出一些建议。

相关文章

网友评论

  • Mr_Elliot:您好 请问在码云上面的项目 开发环境的IDE是什么?
    z77z:sts
  • Mr_Elliot:打开sql的那一瞬间我看到了什么??!!! 福利嘛这是, 楼主真的是妹子吗?233333:no_mouth:
  • 69b0d489b981:如果,页面时HTMl 怎么办呢?
    z77z:可以使用thymeleaf模版
  • git浅寻:这样写只能拦截登录,权限检查doGetAuthorizationInfo没有执行
    git浅寻:楼主有知道shiro ajax 权限解决方法吗?百度的两个解决方法通过拦截器对我的不起作用啊(spring boot 1.5.10)
    z77z:链接权限没有配置 doGetAuthorizationInfo不会执行的,文章中有说到,仔细阅读
  • 203dabfc5193:请问,
    1.当前你的项目中没有触发授权方法doGetAuthorizationInfo,请问该如何配置呢?
    2.不同用户登录进去好像还是那个权限啊
    z77z:@AllenLeung 可以啊 有相应的shiro权限标签可以使用 在前端模版中
    203dabfc5193:@z77z 主要是说不同用户进去访问的都是同一套菜单,能处理下菜单权限么?以角色划分
    z77z:@AllenLeung 我的用户权限和角色权限实在数据库动态配置的,你可以注意下
  • 7a82dbc536a0:可以分享下个人博客的markdown吗?
  • 1a1f17196705:你好,我按照这样启动完,每次输入8080/login就会出现
    javax.servlet.ServletException: Circular view path [login]: would dispatch back to the current handler URL [/login] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)
    真的不知道为什么会这样,pom里面我是直接复制你最新代码的pom,看到请帮忙告诉我一下,谢谢!
    1a1f17196705:@z77z 谢谢,我都查了下,然后自己重新写了一个最简单的例子发现一直有,最后我发现是pom里面 <dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <!--<scope>provided</scope>-->
    </dependency>
    要注释掉provided
    z77z:@卡尔_1e7f Controller中方法返回字符串(视图名),也可能会解析成Servlet。当你的请求路径与视图名相同时,就会发生死循环。检查下是否有视图名和sevlet有重复。http://www.jianshu.com/p/fbef6d3cac9c
  • 9885d5ec80e1:刚才拉了代码,导数据时发现,作者好闷骚啊!
  • 3fd3252944f2:《SpringBoot+shiro整合学习之登录认证和权限控制- 简书》写的不错不错,收藏了。

    推荐下,分库分表中间件 Sharding-JDBC 源码解析 17 篇:http://tinyurl.com/yddgt5bo


    8afe04d66ee7:恩恩

    还不错那
  • 21814a64328f:你好,那个ShiroToken是自己写的实体还是什么,他会提示我cannot be cast to org.apache.shiro.authc.AuthenticationToken,根本没有进行认证
    z77z:@你微笑时很美_50e4 那是之前封装以一遍UsernamePasswordToken,后来觉得没必要就改了,你可以去码云看我的最新代码
  • 艾特不出先生:怎么html标签链接里添加权限标签?
    z77z:用shro提供的jstl自定义标签:smile:
  • EarthChen:dalao,能加个QQ交流一下spring boot shiro的问题嘛 有地方卡住了实在解决不了了😅
  • 176b8580dd7c:您好,您留下的博客地址我点击后自动跳转到:https://git.oschina.net/logo.gif 就一个图片
    176b8580dd7c:已经解决了,不能直接点击链接,把链接复制出来,再浏览器地址栏上敲一下就好!
  • 壮Man:博主总结的技术篇很赞,不知道文末抛出的问题作者博主是否找到了解决方案呢?期待!!另外,我也是半桶水啦,对于shiro ajax请求返回用户无感知的问题,我之前是扩展了shiro的登录校验器,重写里头的onAccessDenied方法,如果请求头是ajax的话,直接返回json数据,前端可以捕获这个失败码再进一步操作。
    wch853:是说登录失败想阻止shiro跳转回登录页面吗?
    z77z:已经全部实现,博客中有相关文章。按时间顺序看
  • 47b0c540c01d:erro java 非法字符
  • 47b0c540c01d:
    运行不了 , 项目下了,就报错 POM 文件
  • NeeYoo:filterChainDefinitionMap.put("/static/**", "anon");
    这个已经配置了,但是静态资源还是被拦截了?是否遇到过呢。
    把filterChainDefinitionMap.put("/**", "authc");改成filterChainDefinitionMap.put("/**", "anon");不拦截任何的,资源是可以加载的,求楼主分析分析,感谢
    z77z:@Basaker 那目录应该没有问题,你再看看你权限的配置顺序,shiro是会顺序执行权限的验证,只要找到匹配的,就不会继续对比权限了。filterChainDefinitionMap.put("/static/**", "anon");这句按理应该配置在最前面。
    NeeYoo:@z77z 静态资源就在static下 static/css和static/js
    z77z:你看你的静态资源文件是放在哪的咯
  • 先生_吕:哥们,可否交流一下,最近公司也在用spring boot 整合shiro
    z77z:@小丨方 可以可以
  • 38faf480efd2:Hi,你好。我使用这份代码的时候怎么没有调用到认证和授权方法?控制台也没出错。
    filterChainDefinitonMap.put("/**", "authc");
    用了这个之后前端无法请求到后端了
    38faf480efd2:我先写死,进行测试的。但是现在没有进入权限验证方法,你有遇到过这种问题吗?
    @RequiresRoles("hh")
    加了这个注解
    z77z:@z77z 是后面改的 可以看看后面的博文
    z77z:我这个项目的权限是在数据库读取的哈
  • z77z:这个是shiro框架使用的开篇
    z77z:@Frank_7522 加我QQ嘛 1093615728
    e2c8baddfa8f:有关角色对应url配置的问题想找您了解一下 可以留个联系方式吗

本文标题:SpringBoot+shiro整合学习之登录认证和权限控制

本文链接:https://www.haomeiwen.com/subject/hkntwttx.html