美文网首页Java 杂谈程序员技术栈java学习
SpringBoot:Spring Boot整合Shiro安全框

SpringBoot:Spring Boot整合Shiro安全框

作者: AubreyXue | 来源:发表于2018-12-31 22:00 被阅读13次

    最近在由Spring Boot2.x构建的更简洁的后台管理系统,完美整合SpringMvc + Shiro + MybatisPlus + Beetl技术,项目开发完成会开源出来,希望能对大家学习道路上有所帮助。在这一篇中我将把我整合Shiro过程记录下来,希望对大家的学习这块能有所帮助。

    maven依赖包

    <!-- shiro框架 -->
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <version>1.4.0</version>
            </dependency>
    
            <!--shiro依赖和缓存-->
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-core</artifactId>
                <exclusions>
                    <exclusion>
                        <artifactId>slf4j-api</artifactId>
                        <groupId>org.slf4j</groupId>
                    </exclusion>
                </exclusions>
                <version>1.4.0</version>
            </dependency>
    

    Shiro 配置类

    /**
     * Shiro配置中心
     *
     * @Auther: hrabbit
     * @Date: 2018-12-24 12:33 PM
     * @Description:
     */
    @Configuration
    public class ShiroConfig {
    
    
        /**
         * Shiro的过滤器链
         */
        @Bean
        public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
            shiroFilter.setSecurityManager(securityManager);
    
            /**
             * 默认登录路径
             */
            shiroFilter.setLoginUrl("/login");
    
            /**
             * 登录成功后要跳转的链接
             */
            shiroFilter.setSuccessUrl("/");
            /**
             * 没有权限的时候跳转页面
             */
            shiroFilter.setUnauthorizedUrl("/global/error");
            /**
             * 配置shiro拦截器链
             *
             * anon  不需要认证
             * authc 需要认证
             * user  验证通过或RememberMe登录的都可以
             *
             * 当应用开启了rememberMe时,用户下次访问时可以是一个user,但不会是authc,因为authc是需要重新认证的
             *
             * 顺序从上到下,优先级依次降低
             *
             * api开头的接口,走rest api鉴权,不走shiro鉴权
             *
             */
            Map<String, String> hashMap = new LinkedHashMap<>();
            hashMap.put("/static/**", "anon");
            hashMap.put("/login", "anon");
            hashMap.put("/global/sessionError", "anon");
            hashMap.put("/**", "user");
            shiroFilter.setFilterChainDefinitionMap(hashMap);
            return shiroFilter;
        }
    
        /**
         * 凭证匹配器
         * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
         * )
         *
         * @return
         */
        @Bean
        public HashedCredentialsMatcher hashedCredentialsMatcher() {
            HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
            hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
            hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
            return hashedCredentialsMatcher;
        }
    
        /**
         * 自定义shiro认证、授权
         *
         * @return
         */
        @Bean
        public ShiroRealm shiroDbRealm() {
            ShiroRealm shiroDbRealm = new ShiroRealm();
            shiroDbRealm.setCredentialsMatcher(hashedCredentialsMatcher());
            return shiroDbRealm;
        }
    }
    

    注意:里面的 SecurityManager 类导入的应该是 import org.apache.shiro.mgt.SecurityManager;

    shirFilter 方法中主要是设置了一些重要的跳转 url,比如未登陆时setLoginUrl,无权限时的跳转setUnauthorizedUrl

    权限拦截 Filter

    当运行一个Web应用程序时,Shiro将会创建一些有用的默认 Filter 实例,并自动地将它们置为可用,而这些默认的 Filter 实例是被 DefaultFilter 枚举类定义的,当然我们也可以自定义 Filter 实例

    Filter 解释
    anon 无参,开放权限,可以理解为匿名用户或游客
    authc 无参,需要认证
    logout 无参,注销,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url
    authcBasic 无参,表示 httpBasic 认证
    user 无参,表示必须存在用户,当登入操作时不做检查
    ssl 无参,表示安全的URL请求,协议为 https
    perms[user] 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms["user, admin"],当有多个参数时必须每个参数都通过才算通过
    roles[admin] 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles["admin,user"],当有多个参数时必须每个参数都通过才算通过
    rest[user] 根据请求的方法,相当于 perms[user:method],其中 method 为 post,get,delete 等
    port[8081] 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString 其中 schmal 是协议 http 或 https 等等,serverName 是你访问的 Host,8081 是 Port 端口,queryString 是你访问的 URL 里的 ? 后面的参数

    常用的主要就是 anon,authc,user,roles,perms 等

    注意:anon, authc, authcBasic, user 是第一组认证过滤器,perms, port, rest, roles, ssl 是第二组授权过滤器,要通过授权过滤器,就先要完成登陆认证操作(即先要完成认证才能前去寻找授权) 才能走第二组授权器(例如访问需要 roles 权限的 url,如果还没有登陆的话,会直接跳转到 shiroFilterFactoryBean.setLoginUrl(); 设置的 url

    自定义 realm 类

    我们首先要继承 AuthorizingRealm 类来自定义我们自己的 realm 以进行我们自定义的身份,权限认证操作。

    /**
     * 自定义Shiro规则
     * @Auther: hrabbit
     * @Date: 2018-11-21 1:16 PM
     * @Description:
     */
    @Slf4j
    public class MyShiroRealm extends AuthorizingRealm {
    
        @Resource
        private SysModuleOperationService sysModuleOperationService;
    
        @Resource
        private SysUsersService sysUsersService;
    
        @Resource
        private SysRolesService sysRolesService;
    
        /**
         * 资源认证
         * @param principals
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            ShiroUser userInfo  = (ShiroUser) principals.getPrimaryPrincipal();
            //按钮资源
            Set<String> permissionSet = new HashSet<>();
            //用户角色
            Set<String> roleNameSet = new HashSet<>();
            
            //获取用户的角色集合
            List<Integer> roleList = userInfo.getRoleList();
    
            for (Integer roleId:roleList){
                //根据角色id获取到资源信息
                List<ModuleOperation> allMenuByUserId = sysModuleOperationService.getPermissionByRoleId(roleId);
                for (ModuleOperation moduleOperation:allMenuByUserId){
                    if (ToolUtil.isNotEmpty(moduleOperation.getCode()))
                    permissionSet.add(moduleOperation.getCode());
                }
                //查询角色信息
                Roles roles = sysRolesService.selectById(roleId);
                if (roles!=null && ToolUtil.isNotEmpty(roles.getRoleCode())){
                    roleNameSet.add(roles.getRoleCode());
                }
            }
            //添加按钮资源
            authorizationInfo.addStringPermissions(permissionSet);
            //添加角色
            authorizationInfo.addRoles(roleNameSet);
            return authorizationInfo;
        }
    
        /**
         * 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。
         * @param token
         * @return
         * @throws AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
                throws AuthenticationException {
            //获取shiroFactory工厂
            ShiroFactoryService shiroFactory = ShiroFactroy.me();
            //获取到用户的信息
            UsernamePasswordToken userToken = (UsernamePasswordToken)token;
            //获取用户的输入的账号.
            String username = userToken.getUsername();
            //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
            ShiroUser userInfo = sysUsersService.getShiroUserByLoginName(username);
            SysUsers sysUser = sysUsersService.getSysUsersByLoginName(username);
            //创建缓存用户信息
            SimpleAuthenticationInfo info = shiroFactory.info(userInfo,sysUser,super.getName());
            return info;
        }
    
        /**
         * 设置认证加密方式
         */
        @Override
        public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
            HashedCredentialsMatcher md5CredentialsMatcher = new HashedCredentialsMatcher();
            md5CredentialsMatcher.setHashAlgorithmName(ShiroUtils.hashAlgorithmName);
            md5CredentialsMatcher.setHashIterations(ShiroUtils.hashIterations);
            super.setCredentialsMatcher(md5CredentialsMatcher);
        }
    }
    

    重写的两个方法分别是实现身份认证以及权限认证,shiro 中有个作登陆操作的 Subject.login()方法,当我们把封装了用户名,密码的 token 作为参数传入,便会跑进这两个方法里面(不一定两个方法都会进入)
    其中 doGetAuthorizationInfo方法只有在需要权限认证时才会进去,比如前面配置类中配置了 filterChainDefinitionMap.put("/**", "user"); 的管理员角色,这时进入系统时就会进入 doGetAuthorizationInfo 方法来检查权限;而 doGetAuthenticationInfo 方法则是需要身份认证时(比如前面的 Subject.login()方法)才会进入
    再说下 UsernamePasswordToken 类,我们可以从该对象拿到登陆时的用户名和密码(登陆时会使用 new UsernamePasswordToken(username, password);),而 get 用户名或密码有以下几个方法

    //获得用户名 String
    token.getUsername();
    //获得用户名 Object 
    token.getPrincipal();
    //获得密码 char[]
    token.getPassword();
    //获得密码 Object
    token.getCredentials();
    

    LoginController的实现

    
    /**
     * 登录控制器
     * @Auther: hrabbit
     * @Date: 2018-11-19 10:23 AM
     * @Description:
     */
    @Controller
    @Slf4j
    @Api(value = "登录API",description = "登录、登出验证,跳转主界面")
    public class LoginController extends BaseController {
    
        /**
         * 基础路径
         */
        private static String BASEURL = "modual";
    
        @Autowired
        private SysModuleOperationService sysModuleOperationService;
    
        @Autowired
        private SysUsersService sysUsersService;
    
        /**
         * 跳转到主页
         * @return
         */
        @RequestMapping(value = {"/","/index"},method = RequestMethod.GET)
        @ApiOperation(value="跳转到主界面", notes="跳转到主页面,查询用户角色信息和页面信息")
        public String index(ModelMap model){
            //获取用户角色idf
            List<Integer> roleList = ShiroUtils.getUser().getRoleList();
            //如果用户不存在角色,跳转到登录界面
            if (roleList == null || roleList.size() == 0){
                ShiroUtils.getSubject().logout();
                model.addAttribute("msg","该用户没有角色,无法登陆");
                return "login";
            }
            //根据角色id查询按钮资源
            List<MenuNode> menuNodes = sysModuleOperationService.getAllMenuByRoleId(roleList);
            menuNodes = MenuNode.buildTitle(menuNodes);
            //返回用户资料信息
            ShiroUser shiroUser = ShiroUtils.getUser();
            //将Shiro用户信息返回到前端页面
            model.addAttribute("user",shiroUser);
            model.addAttribute("title",menuNodes);
            return BASEURL+"/index.html";
        }
    
        /**
         * 跳转到登录界面
         * @return
         */
        @RequestMapping(value = "login",method = RequestMethod.GET)
        @ApiOperation(value="跳转到登录界面", notes="跳转到登录界面")
        public String login(){
            if (ShiroUtils.isAuthenticated() || ShiroUtils.getUser()!=null){
                return REDIRECT+ "/";
            }else{
                return "login.html";
            }
        }
    
        /**
         * 页面提交登录
         *
         * @param username 登录名称
         * @param password 用户密码
         * @return
         */
        @RequestMapping(value = "/login",method = RequestMethod.POST)
        @ApiOperation(value="表单验证", notes="提交登录信息")
        @ApiImplicitParams({
                @ApiImplicitParam(name = "username",value = "用户名称",required = true,dataType = "String"),
                @ApiImplicitParam(name = "password",value = "用户密码",required = true,dataType = "String")
        })
        public String login(String username,String password){
             Subject subject = ShiroUtils.getSubject();
            //检验用户是否存在
            SysUser sysUser = sysUserService.findByLoginName(username);
            // 在认证提交前准备 token(令牌)
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            // 执行认证登陆
            subject.login(token);
            ShiroUser shiroUser = ShiroUtils.getUser();
            //将ShiroUser对象存储到session中
            HttpUtils.getRequest().getSession().setAttribute("shiroUser",shiroUser);
            //保存Session状态
            ShiroUtils.getSession().setAttribute("sessionFlag",true);
            return REDIRECT+"/";
        }
    
    
        /**
         * 退出登录
         * @return
         */
        @RequestMapping(value = "loginOut",method = RequestMethod.GET)
        @ApiOperation(value="退出登录", notes="返回登录界面")
        public String loginOut(){
            ShiroUtils.getSubject().logout();
            return REDIRECT+"login.html";
        }
    }
    

    这里我们需要注意创建异常拦截器,这样当用户名或者密码不正确的时候,Shiro会自动抛出异常,我们只需要将异常捕获即可

    /**
     * 异常类
     *
     * @Auther: hrabbit
     * @Date: 2018-11-15 3:40 PM
     * @Description:
     */
    @ControllerAdvice("com.hrabbit.admin")
    @Order(-1)
    @Slf4j
    public class GlobalExceptionHandler {
    
        /**
         * 其他异常抛出信息
         *
         * @param response
         * @param ex
         * @return
         */
        @ExceptionHandler(Exception.class)
        public BaseResponse otherExceptionHandler(HttpServletResponse response, Exception ex) {
            response.setStatus(500);
            log.error(ex.getMessage(), ex);
            return new BaseResponse(500, ex.getMessage());
        }
    
    
        /**
         * 账号被冻结异常
         */
        @ExceptionHandler(DisabledAccountException.class)
        @ResponseStatus(HttpStatus.UNAUTHORIZED)
        public String accountLocked(DisabledAccountException e, Model model) {
            model.addAttribute("message", "账号被冻结");
            return "/login.html";
        }
    
        /**
         * 账号密码错误异常
         */
        @ExceptionHandler(CredentialsException.class)
        @ResponseStatus(HttpStatus.UNAUTHORIZED)
        public String credentials(CredentialsException e, Model model) {
            model.addAttribute("message", "账号密码错误");
            return "/login.html";
        }
    }
    

    测试

    密码错误的时候,会自动捕获到异常信息


    image.png

    密码正确,进入到主页面


    image.png

    代码正在编写中,等这块我编写完成,会放到码云上面的
    码云地址: https://gitee.com/hrabbit/hrabbit-admin
    个人博客:http://www.hrabbit.xin

    相关文章

      网友评论

        本文标题:SpringBoot:Spring Boot整合Shiro安全框

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