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包依赖



            org.apache.shiro
            shiro-spring
            1.4.0
   

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 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 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 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 userList = this.indexService.queryUserBy(user);
       
        modelMap.put("userList", userList);
        return "userList";
    }

登录页面


用户名:

密码:

自动登录


实例中实现了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;
    }
}

你可能感兴趣的:(SpringBoot2教程(五)整合Shiro)