PHPER转JAVA记录篇-spring boot 整合shiro

一、SHIRO的概念

shiro是一个安全框架,主要可以帮助我们解决程序开发中认证和授权的问题。基于拦截器做的权限系统,权限控制的粒度有限,为了方便各种各样的常用的权限管理需求的实现,,我们有必要使用比较好的安全框架,早期spring security 作为一个比较完善的安全框架比较火,但是spring security学习成本比较高,于是就出现了shiro安全框架,学习成本降低了很多,而且基本的功能也比较完善。

1.1、SHIRO提供的功能

1、Authentication:身份认证/登陆,验证用户是不是拥有相对应的身份;
2、Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者粒度的验证某个用户对某个资源是否具有权限;
3、Session Manager:会话管理,即用户登陆后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是Web环境的;
4、Cryptographt:加密,保护数据,如密码加密存储到数据库,而不是明文存储;
5、Web Support:Web支持,可以非常容易的继承到Web环境的;
6、Caching:缓存,比如用户登陆后,其用户信息、拥有的角色/权限不必每次去查,这样提高效率;
7、Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
8、Testing:提供测试支持;
9、Run As:允许一个用户假装另一个用户(如果我们允许)的身份进行访问;
10、Remember Me:记住我,这个是非常常见的功能,即一次登陆后,下次再来的话不用登陆了。

1.2、通俗总结

既然大家都是phper转过来的,所以我想大家还是知道rbac的对吧?那么通俗的点说,shiro就是封装好的rbac的项目。还是不懂么?那么大家做CMS后台的时候,总知道管理员管理,角色管理,角色用户关系~~这些东西的吧?对,没错,这玩意就是搞这个东西的。

二、开干

废话不多说,直接干!!!!!

2.1、基础数据源

注意:sql中的 “e10adc3949ba59abbe56e057f20f883e” 是通过123456进行md5得出来的字符串

123456的MD5加密传送门

-- ----------------------------
-- Table structure for shiro_permission
-- ----------------------------
DROP TABLE IF EXISTS `shiro_permission`;
CREATE TABLE `shiro_permission` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `url` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='权限表';

-- ----------------------------
-- Table structure for shiro_role
-- ----------------------------
DROP TABLE IF EXISTS `shiro_role`;
CREATE TABLE `shiro_role` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='角色表';

-- ----------------------------
-- Table structure for shiro_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `shiro_role_permission`;
CREATE TABLE `shiro_role_permission` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `role_id` varchar(255) DEFAULT NULL,
  `permission_id` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='角色与权限多对多表';

-- ----------------------------
-- Table structure for shiro_user
-- ----------------------------
DROP TABLE IF EXISTS `shiro_user`;
CREATE TABLE `shiro_user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_name` (`name`) USING BTREE COMMENT '用户名不可以重复的唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='用户表';

-- ----------------------------
-- Table structure for shiro_user_role
-- ----------------------------
DROP TABLE IF EXISTS `shiro_user_role`;
CREATE TABLE `shiro_user_role` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `role_id` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户与角色多对多表';

insert into `shiro_user` (`id`, `name`, `password`) values('1','admin','e10adc3949ba59abbe56e057f20f883e');
insert into `shiro_user` (`id`, `name`, `password`) values('2','vip','e10adc3949ba59abbe56e057f20f883e');
insert into `shiro_user` (`id`, `name`, `password`) values('3','svip','e10adc3949ba59abbe56e057f20f883e');

insert into `shiro_role` (`id`, `name`) values('1','user');
insert into `shiro_role` (`id`, `name`) values('2','vip');
insert into `shiro_role` (`id`, `name`) values('3','svip');

insert into `shiro_permission` (`id`, `name`, `url`) values('1','user','user');
insert into `shiro_permission` (`id`, `name`, `url`) values('2','vip','vip');
insert into `shiro_permission` (`id`, `name`, `url`) values('3','svip','svip');

insert into `shiro_user_role` (`id`, `user_id`, `role_id`) values('1','1','1');
insert into `shiro_user_role` (`id`, `user_id`, `role_id`) values('3','2','2');
insert into `shiro_user_role` (`id`, `user_id`, `role_id`) values('6','3','3');

insert into `shiro_role_permission` (`id`, `role_id`, `permission_id`) values('1','1','1');
insert into `shiro_role_permission` (`id`, `role_id`, `permission_id`) values('2','2','1');
insert into `shiro_role_permission` (`id`, `role_id`, `permission_id`) values('3','2','2');
insert into `shiro_role_permission` (`id`, `role_id`, `permission_id`) values('4','3','1');
insert into `shiro_role_permission` (`id`, `role_id`, `permission_id`) values('5','3','2');
insert into `shiro_role_permission` (`id`, `role_id`, `permission_id`) values('6','3','3');

三、引入依赖

   <!-- 导入shiro和spring继承的jar包 -->
   <dependency>
       <groupId>org.apache.shiro</groupId>
       <artifactId>shiro-spring</artifactId>
       <version>1.2.3</version>
   </dependency>

四、生成实体类

参考:phper转java记录篇-spring boot 整合easy-code.
生成之后的代码

4.1 要注意的问题:

1、如果你是一气呵成的做完easycode相关的所有操作,你不会感知到,但是当你如果修改了数据库的字段,你会发现有的时候字段变更,重新生成的文件好像有缓存的感觉,这时候你需要考虑项目根目录下**.idea的文件,进去找到对应的json删掉**
easycode生成的json文件
2、生成的文件目录是可以去修改的,比如我的dao和entity是在db目录下的,默认是在项目根目录下的,设置方式如下:
设置easycode模板文件

五、实现类

参考文章:springboot整合shiro+mybatis+mysql
springboot与shiro和mybatis和mysql

5.1 ApiShiroRealm类的实现

注:代码中queryAllByUserIdqueryByUserName,这些方法可能需要自行补充一下,因为easycode生成不出来。

List<ShiroRole> queryAllByUserId(Integer userId);

ShiroUser queryByUserName(String userName);

具体实现,大家自行实现,不在赘述了

================================================

整理的代码如下:

/**
 * 自定义Realm 继承AuthorizingRealm 重写  AuthorizationInfo(授权) 和  AuthenticationInfo(认证)这两个
 * @author leoxie
 */
public class ApiShiroRealm extends AuthorizingRealm {


    @Resource
    private ShiroRolePermissionService shiroRolePermissionService;

    @Resource
    private ShiroUserRoleService shiroUserRoleService;

    @Resource
    private ShiroUserService shiroUserService;


    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("权限配置[doGetAuthorizationInfo]-->"+this.getClass().getName());
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        ShiroUser shiroUser  = (ShiroUser)principals.getPrimaryPrincipal();

        Integer userId = shiroUser.getId();

        //用户对应的角色列表
        List<ShiroRole> userRoleList = shiroUserRoleService.queryAllByUserId(userId);

        for(ShiroRole role:userRoleList){
            authorizationInfo.addRole(role.getName());
            //获取当前角色对应的节点权限
            List<ShiroPermission> userRolePermission = shiroRolePermissionService.queryAllByRoleId(role.getId());

            for(ShiroPermission permission:userRolePermission){
                authorizationInfo.addStringPermission(permission.getUrl());
            }
        }

        return authorizationInfo;
    }

    /**
     * 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确
     *
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("权限配置[doGetAuthenticationInfo]-->"+this.getClass().getName());
        //获取用户的输入的账号.
        String username = (String)token.getPrincipal();
        System.out.println("用户名:"+username);
        //通过username从数据库中查找 User对象,如果找到,没找到.
        //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        ShiroUser shiroUser = shiroUserService.queryByUserName(username);

        if(shiroUser == null){
            return null;
        }

        System.out.println("用户密码:"+shiroUser.getPassword());

        return new SimpleAuthenticationInfo(
                //用户信息
                shiroUser,
                //密码
                shiroUser.getPassword(),
                //salt=ByteSource.Util.bytes(username) ps:不是固定的,自己搞一个能针对这个用户的盐就好了,当然也可以统一一模一样的盐,为了方便,我们先用空字符串,这样算法就完全是md5(xxx),不用考虑盐的作用
                ByteSource.Util.bytes(""),
                //realm name
                getName()
        );
    }
}

5.2 ShiroConfig类的实现

看代码注释就好了,不需要过多的说明

/**
 * @author leoxie
 */
@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        System.out.println("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //拦截器.
        //必须是LinkedHashMap,因为要保证有序
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/submitLogin","anon");

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

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

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

        //未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 凭证匹配器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
     * )
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //散列的次数,相当于 md5("");
        hashedCredentialsMatcher.setHashIterations();
        return hashedCredentialsMatcher;
    }

    @Bean
    public ApiShiroRealm myShiroRealm(){
        ApiShiroRealm apiShiroRealm = new ApiShiroRealm();
        apiShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return apiShiroRealm;
    }


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

    /**
     *  开启shiro aop注解支持.
     *  使用代理方式;所以需要开启代码支持;
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

以上大家应该还是不是很理解2个问题:
1、authc/anon这两个以外还有哪些值

filterChainDefinitionMap.put("/**", "authc");
Filter NameClass
anonorg.apache.shiro.web.filter.authc.AnonymousFilter
authcorg.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasicorg.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
permsorg.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
portorg.apache.shiro.web.filter.authz.PortFilter
restorg.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
rolesorg.apache.shiro.web.filter.authz.RolesAuthorizationFilter
sslorg.apache.shiro.web.filter.authz.SslFilter
userorg.apache.shiro.web.filter.authc.UserFilter

当然要特别的设置某个action的话,就需要通过 RequiresPermissions/RequiresRoles 这两个注解来完成
如果注解无效:springboot 项目 shiro注解不生效

六、验证可用性

6.1 访问静态资源

针对上面config的配置来说,static的字段是可以不需要权限验证就可以访问了,所以我们访问一下
http://127.0.0.1:81/static/abc.txt
的确可以直接访问

遇到404了怎么办,在yml中添加如下代码

spring:
  mvc:
    static-path-pattern: /static/**

6.2 访问不存在的控制器/存在控制器

访问 “http://127.0.0.1:81/axkxk” 被302转发到“http://127.0.0.1:81/login”了

6.3 完善login界面

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h1>用户登录</h1>
<hr>
<form id="from" action="/submitLogin" method="post">
    <table>
        <tr>
            <td>用户名</td>
            <td>
                <input type="text" name="username" placeholder="请输入账户名" value="" th:value="${userName }"/>
            </td>
        </tr>
        <tr>
            <td>密码</td>
            <td>
                <input type="password" name="password" placeholder="请输入密码"/>
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <span style="color: red;">[[${msg }]]</span>
            </td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="登录"/>
                <input type="reset" value="重置"/>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

注意: 这里因为是页面,所以需要注意注解是 @Controller不要写成了 @RestController 否则页面没办法输出页面

@Controller
public class ShiroController extends Base {

    @GetMapping(value="/login")
    public String Login(){
        //文件名(login.html直接放在/resources/templates/下面即可)
        return "login";
    }
}

另外记得引入 spring-boot-starter-thymeleaf 依赖,当然这块内容我们后续有需要的话,再进行研究。现在都是前后端分离了,很少说代码框架里面去使用页面,所以我们只要保证基本的可用性即可。

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

默认的路径就是/resources/templates/下面

如果你发现了Cannot resolve MVC View,或者你发现你的html不能访问到的时候,不妨看看你的ShiroController是否使用了 @Controller注解,另外你的鼠标放在【return “login”;】这句话上面,是否出现了Cannot resolve MVC View。
Cannot resolve MVC View解决方案: IDEA开发springboot项目遇到的问题:Cannot resolve MVC View ‘XXX’

6.4 完善submitLogin方法

简单实现一下,不完善的地方,麻烦自行补充一下。

@Controller
public class ShiroController extends Base {

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

    @GetMapping(value="/index")
    public String Index(){
        return "index";
    }

    @GetMapping(value="/about")
    public String About(){
        return "about";
    }

    @PostMapping("/submitLogin")
    public String login(HttpServletRequest request, HttpServletResponse response){

        String userName = request.getParameter("username");
        String password = request.getParameter("password");

        // 等于null说明用户没有登录,只是拦截所有请求到这里,那就直接让用户去登录页面,就不认证了。
        // 如果这里不处理,那个会返回用户名不存在,逻辑上不合理,用户还没登录怎么就用户名不存在?
        if(null == userName || null == password) {
            return "login";
        }

        // 1.获取Subject
        Subject subject = SecurityUtils.getSubject();
        // 2.封装用户数据
        UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
        // 3.执行登录方法
        request.setAttribute("userName",userName);
        request.setAttribute("password",password);
        try{
            subject.login(token);
            return "redirect:/index";
        } catch (UnknownAccountException e){
            // 这里是捕获自定义Realm的用户名不存在异常
            request.setAttribute("msg","用户名不存在!");
        } catch (IncorrectCredentialsException e){
            request.setAttribute("msg","密码错误!");
        } catch (AuthenticationException e) {
            // 这里是捕获自定义Realm的认证失败异常
            request.setAttribute("msg","认证失败!");
        }

        return "login";
    }

}

小结: 到这里你会发现登录已经可以实现了,另外就是可能出了login页面,你要写一个index的页面和一个about页面做个试验,这里贴一下代码

about.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>关于我们</title>
</head>
<body>
<h1>欢迎来到</h1>
<hr>
关于我们
</body>
</html>

index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页-001</title>
</head>
<body>
<a href="/about">关于页面</a>
</body>
</html>

但是与此同时我发现一个问题。ApiShiroRealm类里面的doGetAuthorizationInfo方法还没有用到呢?另外就是我们还有表没用到,就是角色和permission这一系列的,这时候就需要 RequiresPermissions/RequiresRoles 这两个注解,demo

/**
  * 这里的vip,就是对应权限实体类Permission实体类的字段url,自定义Realm类ApiShiroRealm里是用这个字段
  */
    @GetMapping(value="/about")
    @RequiresPermissions(value = "vip")
    public String About(){
        return "about";
    }

同时要配上拦截器,没有拦截器,异常抛不出来

@ControllerAdvice
public class NoPermissionException {
    @ResponseBody
    @ExceptionHandler(UnauthorizedException.class)
    public ResultJSON handleShiroException(Exception ex) {
    	//自定义的记录日志方法
        SlfUtils.getInst(this).error(0, "访问了无权限目录");
        //自定义的返回对象
        return ResultJSON.error("无权限操作");
    }
    @ResponseBody
    @ExceptionHandler(AuthorizationException.class)
    public String AuthorizationException(Exception ex) {
        return "权限认证失败";
    }
}

注: 一定要注意拦截器方法上面一定有2个注解:@ExceptionHandler & @ResponseBody ,我因为漏了调试了快一小时

微信二维码

有问题可以加个微信,共同学习成功,Leo.xie博客

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值