SpringBoot:Spring Boot整合Shiro安全框架

最近在由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

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

推荐阅读更多精彩内容

  • 2017年,涂嘉禾加入梅溪小学队伍,收到录取通知书,如愿和李校长合影。 一年级快结束了,在这一年里,我们每天早上一...
    和颜悦色lxy阅读 624评论 0 2
  • 我们在这个世界孤独才是真实曾以为有人陪伴你已改变却发现还是没变人固有的坚持我们应纽带而存在若我们缩成一个点那将是一...
    尛心莫伤阅读 117评论 0 0
  • 高绩效教练Day 4-10.21-@因心木灬 当我想做的时候,我的表现会比我不得不做时更好;我想做是为了自己,我不...
    小冷睡了阅读 180评论 0 0