SpringBoot2教程(五)整合Shiro

源码地址:https://github.com/q200737056/Spring-Course/tree/master/springboot2Shiro

一、项目环境

Java8+Maven3.3.9+SpringBoot2.0.4+Mybatis3+Shiro+H2+Eclipse
注意:使用了H2嵌入式内存模式的数据库

二、Shiro简介

Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。并且相对于其他安全框架,Shiro 要简单的多。


架构

  • Subject:主体,即subject记录了当前操作的主体。可能是一个通过浏览器请求的用户,也可能是一个运行的程序。
  • SecurityManager:安全管理器,它是shiro的核心。管理着认证,授权,session管理,缓存等。
  • Authentication:认证器,对用户身份进行认证。
  • Authorization:授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
  • SessionManager:会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session。
  • SessionDAO:会话dao,是对session会话操作的一套接口。
  • CacheManager:缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
  • Cryptography:密码管理,shiro提供了一套加密/解密的组件,保护数据的安全性,比如用户密码。
  • Realm:域,相当于数据源,SecurityManager需要通过Realm获取用户权限数据。

三、SpringBoot整合Shiro

jar包依赖

<!--省略sringboot等其它依赖-->
<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
   </dependency>

JavaConfig配置Shiro

@Configuration
public class ShiroConfig {
    
    /**
     * shiro过滤器
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //配置登录的url,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/index/loginIndex");
        //未授权界面;配置不会被拦截的链接
       //shiroFilterFactoryBean.setUnauthorizedUrl("/index/noperms");
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
        
        filterChainDefinitionMap.put("/h2-console/**", "anon");
        filterChainDefinitionMap.put("/index/login", "anon");
        filterChainDefinitionMap.put("/index/noperms", "anon");
       //filterChainDefinitionMap.put("/index/toAdd", "perms[user:add]");
        //filterChainDefinitionMap.put("/index/**", "authc");
        
        filterChainDefinitionMap.put("/index/logout", "logout");
        //主要这行代码必须放在所有权限设置的最后,user拦截表示 用户存在或记住我 可以访问
        filterChainDefinitionMap.put("/**", "user");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;

    }
    /**
     * 配置核心安全管理器
     * @param userRealm
     * @return
     */
    @Bean
    public SecurityManager securityManager(UserRealm  userRealm,
          MemoryConstrainedCacheManager cacheManager) {
         DefaultWebSecurityManager defaultSecurityManager = new 
                DefaultWebSecurityManager();
        //设置 领域
        defaultSecurityManager.setRealm(userRealm);
        //设置 缓存
        defaultSecurityManager.setCacheManager(cacheManager());
        //设置 记住我
        defaultSecurityManager.setRememberMeManager(rememberMeManager());
        //设置 session管理器
        defaultSecurityManager.setSessionManager(sessionManager());
        
        return defaultSecurityManager;
    }
    /**
     * 自定义凭证匹配器
     */
    @Bean
    public SimpleCredentialsMatcher customCredentialsMatcher(){
        return new CustomCredentialsMatcher();
    }
    /**
     * 自定义Realm
     * @return
     */
    @Bean
    public UserRealm userRealm() {
        UserRealm realm = new UserRealm();
        realm.setCredentialsMatcher(customCredentialsMatcher());
        return realm;
    }
    /**
     * 使用shiro自带的缓存,当然还可以使用第三方缓存
     * @return
     */
    @Bean
    public MemoryConstrainedCacheManager cacheManager() {
       return new MemoryConstrainedCacheManager();
    }
    /**
     * 会话管理管理器
     * @return
     */
    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new 
           DefaultWebSessionManager();
        //全局会话超时时间(单位毫秒),默认30分钟
        sessionManager.setGlobalSessionTimeout(1800000); 
        sessionManager.setSessionDAO(sessionDAO());
        //删除过期的session
        sessionManager.setDeleteInvalidSessions(true);
        //是否开启会话验证器,默认是开启的
        sessionManager.setSessionValidationSchedulerEnabled(true);
        //去掉URL地标后面的JSESSIONID
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        //定时清理失效会话, 清理用户直接关闭浏览器造成的孤立会话
        sessionManager.setSessionValidationInterval(1800000);
        //sessionID是否保存到cookie中
        sessionManager.setSessionIdCookieEnabled(true);
        //sessionID Cookie
        sessionManager.setSessionIdCookie(sessionIdCookie());
        return sessionManager;
    }
    /**
     * 会话DAO
     * @return
     */
    @Bean
    public SessionDAO sessionDAO() {
        MemorySessionDAO sessionDAO = new MemorySessionDAO();
        return sessionDAO;
    }
    /**
     * 记住我 管理器
     * @return
     */
    @Bean
    public CookieRememberMeManager rememberMeManager(){
        CookieRememberMeManager manager = new CookieRememberMeManager();
        //cookie加密 密钥 ,默认AES算法
        manager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
        manager.setCookie(rememberMeCookie());
        return manager;
    }
    /** 
     * 自动登录  Cookie
     * @return
     */
    @Bean
    public SimpleCookie rememberMeCookie() {
        //构造方法的参数 是cookie的名称
        SimpleCookie cookie = new SimpleCookie("rememberMe");
        // 记住我cookie生效时间1天 ,单位秒
        cookie.setHttpOnly(true);
        cookie.setMaxAge(86400);
        return cookie;
    }
    /** 
     * 会话Cookie 保存sessionID
     * @return
     */
    @Bean
    public SimpleCookie sessionIdCookie() {
        //构造方法的参数 是cookie的名称
        SimpleCookie cookie = new SimpleCookie("sid");
        cookie.setHttpOnly(true);
        //关闭浏览器后 ,此cookie清除
        cookie.setMaxAge(-1);
        return cookie;
    }
    /**
     * *
     * 开启Shiro的注解
     * 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 
     * 配置以下两个bean
     * DefaultAdvisorAutoProxyCreator(可选,可以不用配置)
     * AOP方法级权限检查,扫描上下文,寻找所有的Advistor(通知器),将这些
           Advisor应用到所有符合切入点的Bean中。
     * AuthorizationAttributeSourceAdvisor AOP方法级权限检查
     * * @return
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new 
            DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor 
         authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = 
                new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * Shiro生命周期处理器
     * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
}

最主要的三个一定要配置。ShiroFilterFactoryBean:主要是配置一些URL与拦截器。默认主要的拦截器有anon(可匿名访问),authc(需要身份认证),roles[XX](需要拥有某些角色),perms[xx](需要拥有某些权限),logout(登出),user(需要存在用户或记住我)等。
SecurityManager :需要配置自定义Realm。
UserRealm :自定义Realm。继承AuthorizingRealm,实现了认证及授权。

注意:为了使用注解方式,更加简便设置角色权限访问。需要借助AuthorizationAttributeSourceAdvisor 类。

public class UserRealm extends AuthorizingRealm {
    @Autowired
    private IndexService indexService;
    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
        String username = (String) SecurityUtils.getSubject().getPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        Set<String> stringSet = new HashSet<>();
        //这边直接赋权限了,正式项目中,从数据库查询该用户的角色(role),
        //角色的权 限(perm)
        if("admin".equals(username)){
            stringSet.add("user:query");
               stringSet.add("user:update");
               stringSet.add("user:add");
              stringSet.add("user:delete");
        }else{
            stringSet.add("user:query");
        }
        info.setStringPermissions(stringSet);
        return info;
    }
    /**
     * 身份认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
            throws AuthenticationException {
        
            String userName = (String) token.getPrincipal();
            //查询数据库
            String userPwd = this.indexService.login(userName);
         
            if (userPwd == null) {
                throw new UnknownAccountException();//用户名不存在
            } 
            return new SimpleAuthenticationInfo(userName, userPwd,getName());
    }
}

实例中自定义了凭证匹配器。当然可以直接使用Shiro自带的HashedCredentialsMatcher注入一些必要的参数setHashAlgorithmName("md5") setHashIterations(2)

public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {

        @Override
        public boolean doCredentialsMatch(AuthenticationToken token,
                  AuthenticationInfo info) {
            UsernamePasswordToken utoken=(UsernamePasswordToken) token;
            //获得用户输入的密码
            String inPassword = new String(utoken.getPassword());
            //获得数据库中的密码
            String dbPassword=(String) info.getCredentials();
            //进行密码的比对(可以采用加盐的方式去检验,这边直接明文匹配)
            return this.equals(inPassword, dbPassword);
        }
}

Controller


以下截取了一部分

       /**
     * 首页
     * @return
     */
    @RequestMapping()
    public String index(HttpSession session){
        Subject subject = SecurityUtils.getSubject();
        String name = (String)subject.getPrincipal();
        System.out.println("subject:"+name);
        session.setAttribute("username", name);
        //System.out.println("==="+subject.getSession().getAttribute("username"));
        return "forward:/index/userList";
    }
    /**
     * 登陆页面
     * @return
     */
    @RequestMapping(value="/loginIndex",method=RequestMethod.GET)
    public String loginIndex(){
        return "index";
    }
    /**
     *  登陆
     *  @PostMapping相当于@RequestMapping(method=RequestMethod.POST)
     * @return
     */
    @PostMapping("/login")
    public String login(String name,String password,String rememberMe
                ,ModelMap modelMap){
        boolean booRememberMe = false;
        if("true".equals(rememberMe)){
            booRememberMe=true;
        }
       // System.out.println("===="+booRememberMe);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken=new 
                UsernamePasswordToken(name,password,booRememberMe);
        
        try {
            subject.login(usernamePasswordToken);   //登录
        } catch (UnknownAccountException e) {
            modelMap.put("msg", "用户名不存在!");
            return "index";
        }catch (IncorrectCredentialsException ice) {
            modelMap.put("msg", "密码不正确!");
            return "index";
        }catch (LockedAccountException lae) {
            modelMap.put("msg", "账户已锁定!");
            return "index";
        } catch (ExcessiveAttemptsException eae) {
            modelMap.put("msg", "密码错误次数过多!");
        } catch (AuthenticationException ae) {
            modelMap.put("msg", "用户名或密码不正确!");
            return "index";
        }
        
        return "forward:/index";
    }
    /**
     * 这边自定义logout
     * shiro有默认logout,会自动清除相关信息,返回配置的登陆页面
     * 用户登出
     * @param 
     * @return
     */
    @RequestMapping("/logout")
    public String logout(ModelMap modelMap) {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();

        modelMap.put("msg","安全退出!");
        return "index";
    }
    /**
     * 列出 所有用户
     * @param modelMap
     * @return
     */
    @RequestMapping("/userList")
    public String userList(ModelMap modelMap){
        
        List<User> userList = this.indexService.findUserList();
        modelMap.put("userList", userList);
        return "userList";
    }
      /**
     * 查询用户
     * @param id
     * @return
     * @RequiresPermissions:shiro权限配置
     * 如果没有权限的用户操作,会报错
     * 解决方法1.捕获异常,后续进行相关提示或操作。
     * 解决方法2.配置拦截  比如 /index/queryUser=perms[user:query];
     * ShiroFilterFactoryBean设置没权限时的url setUnauthorizedUrl("/index/noperms")
     * 这里使用了方法1  全局异常捕获处理
     */
    @RequiresPermissions("user:query")
    @PostMapping("/queryUser")
    public String queryUser(User user,ModelMap modelMap){
        List<User> userList = this.indexService.queryUserBy(user);
       
        modelMap.put("userList", userList);
        return "userList";
    }

登录页面

<body>
<form action="/index/login" method="post" >
<div style="width: 400px;margin:30px auto;">
    用户名:<input type="text" name="name" value="admin"><br /><br />
    密码:<input type="password" name="password" value="admin"><br /><br />
    自动登录<input type="checkbox" name="rememberMe" value="true"/>
  <br /><br />
    <input type="submit" value="登录" />
    <br />
    <span th:text="${msg==null?'':msg}"></span>
</div>
</form>
</body>

实例中实现了Shiro记住我,自动登录功能。注意的是需要配置Cookie来保存登录用户名及密码。为了安全考虑需要配置加密算法及密钥,Shiro默认AES算法。实例中还使用了Base64编码过的密钥,隐藏了明文。
还有一个需要注意的是当认证过的用户,没有权限操作时,方法会报没有权限异常UnauthorizedException,解决方法有二种。

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

推荐阅读更多精彩内容