springboot-shiro

springboot-shiro

简介

shiro是apache下的一个轻量级开源项目,相对于springSecurity简单的多。

三大功能模块:

  • Subject:主体,一般指用户;
  • SecurityManager:安全管理器,管理所有Subject,可以配合内部安全组件;
  • Realms:用于进行权限信息验证,一般需要自己实现。

细分功能:

  • Authentication:身份认证/登录;
  • Authorization:授权;
  • Session Manager:会话管理,即登录后的session;
  • Cryptography:加密,密码加密登;
  • Web Support:web支持;
  • Caching:缓存,用户信息、角色、权限等缓存redis等缓存中;
  • Concurrency:多线程并发验证;
  • Testing:测试支持;
  • Run As:允许一个用户假装另一个用户(如果允许);
  • Remember Me:记住密码;

开始

添加依赖

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>${spring.shiro.version}</version>
</dependency>

密码比较器

这个Bean定义了密码的加密方式、加密盐值和加密次数;当然也可以自己写个类继承HashedCredentialsMatcher类,从而进一步自定义密码匹配(例如:添加密码错误次数限制);

public static String md5 = "md5";
public static int md5Time = 1;
@Bean
public CredentialsMatcher credentialsMatcher() {
    // 如果要用redis,可以将RedisCacheManager作为构造参数
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
    //加密方式
    credentialsMatcher.setHashAlgorithmName(md5);
    //加密迭代次数
    credentialsMatcher.setHashIterations(md5Time);
    //true加密用的hex编码,false用的base64编码
    credentialsMatcher.setStoredCredentialsHexEncoded(true);
    return credentialsMatcher;
}

认证和授权

这里其实就是配置一个认证域,

  • 认证:可以获取用户输入的用户名和密码、token类型,可以设置密码匹配规则,最后需要返回一个AuthenticationInfo交给shiro处理(这里是可以进行很大程度上的自定义认证操作,例如:免密登录,丰富用户信息到session等);
  • 授权:主要是给用户设置角色和权限,需要注意的是,授权方法是在第一次访问授权的地址时才会执行;
public static String md5Salt = "1234";
@Bean
public AuthorizingRealm pwdAuthorizingRealm(CredentialsMatcher credentialsMatcher) {
    return new AuthorizingRealm(credentialsMatcher) {
        // 认证
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            // 用户输入的用户名和密码
            String userName = token.getPrincipal().toString();
            // String userPwd = new String((char[]) token.getCredentials());
            // todo: 根据用户名从数据库获取密码,这里固定为:5678
            String password = "25d55ad283aa400af464c76d713c07ad";
            if (userName == null) {
                return null;
            }
            // 自定义密码加密盐值,可以不要,默认没有加密。也可以在这里自定义密码匹配。
            ByteSource credentialsSalt = ByteSource.Util.bytes(md5Salt);
            return new SimpleAuthenticationInfo(userName, password, credentialsSalt, getName());
        }

        // 授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            // todo: 可以根据登录名称查询角色、权限信息、同时还可以将角色信息缓存起来
            String username = (String) principals.getPrimaryPrincipal();
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            // 设置角色
            Set<String> roles = new HashSet<>();
            roles.add(username);
            roles.add("role_xxx");
            info.setRoles(roles);
            // 直接设置权限
            Set<String> stringSet = new HashSet<>();
            stringSet.add(username);
            info.setStringPermissions(stringSet);
            return info;
        }
    };
}

redis

开始

使用redis管理缓存和会话(可以不要);

  • 参考springboot在项目中引入redis;

  • 添加依赖

<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>3.2.3</version>
</dependency>

Bean

配置好这些Bean并不代表就成功整合redis了,还要在后面的会话管理器、权限管理器中注入这些Bean;

@Bean
public RedisManager redisManager(RedisProperties redisProperties) {
    RedisManager redisManager = new RedisManager();
    redisManager.setHost(redisProperties.getHost() + ":" + redisProperties.getPort());
    redisManager.setPassword(redisProperties.getPassword());
    redisManager.setTimeout(1800);
    return redisManager;
}

@Bean
public RedisSessionDAO redisSessionDAO(RedisManager redisManager) {
    RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
    redisSessionDAO.setRedisManager(redisManager);
    return redisSessionDAO;
}

@Bean
public RedisCacheManager redisCacheManager(RedisManager redisManager) {
    RedisCacheManager cacheManager = new RedisCacheManager();
    cacheManager.setRedisManager(redisManager);
    return cacheManager;
}

会话管理器

可以设置一些会话管理器的参数,例如是否使用redis;

@Bean
public DefaultWebSessionManager defaultWebSessionManager() {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    // 这两行用于整合redis
    // sessionManager.setSessionDAO(sessionDAO);
    // sessionManager.setCacheManager(cacheManager);
    return sessionManager;
}

权限管理器

可以设置一些会话管理器的参数,例如是否使用redis缓存,配置一个或者多个Realm域

@Bean
public DefaultWebSecurityManager securityManager(AuthorizingRealm pwdAuthorizingRealm, DefaultWebSessionManager sessionManager) {
    DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
    // 这行用于整合redis
    // defaultSecurityManager.setCacheManager(cacheManager);
    defaultSecurityManager.setSessionManager(sessionManager);
    defaultSecurityManager.setRealm(pwdAuthorizingRealm);
    // 记住密码功能,需要结合登录参数RememberMe使用
    defaultSecurityManager.setRememberMeManager(new CookieRememberMeManager());
    return defaultSecurityManager;
}

自定义filterMap

这其实就是个Map,可以在这个map中自定义shiro权限过滤器,其中Map的key为要自定义的权限名(如:roles、perms、authc等),Map的value为自定义的过滤方法,下面的示例代码实现了将角色过滤的判断逻辑有原来默认的and改为or(如果不需要自定义,可以不要这个Map);

public Map<String, Filter> filterMap(){
    Map<String, Filter> filterMap = new HashMap<>();
    // 自定义角色过滤器,将and改成or,同时role无权限返回json
    filterMap.put("roles", new RolesAuthorizationFilter() {
        @Override
        public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            String[] roles = (String[]) mappedValue;
            if (roles == null || roles.length == 0) {
                return true;
            }
            Subject subject = getSubject(request, response);
            for (String role : roles) {
                if (subject.hasRole(role)) return true;
            }
            return false;
        }

        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
            Subject subject = getSubject(request, response);
            HttpServletResponse res = (HttpServletResponse) response;
            res.setContentType("application/json;charset=utf-8");
            if (subject.getPrincipal() == null) {
                saveRequestAndRedirectToLogin(request, response);
                /*res.setStatus(HttpStatus.UNAUTHORIZED.value());
                    res.getWriter().write("请先登录");*/
            } else {
                res.setStatus(HttpStatus.FORBIDDEN.value());
                res.getWriter().write("您没有访问权限");
            }
            return false;
        }
    });
    // 自定义登录拦截,未登录的访问直接返回json,而不是跳转
    /*filterMap.put("authc", new UserFilter(){
            @Override
            protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
                HttpServletResponse res = (HttpServletResponse) response;
                res.setStatus(HttpStatus.UNAUTHORIZED.value());
                res.setContentType("application/json;charset=utf-8");
                res.getWriter().write("请先登录");
            }
        });*/
    return filterMap;
}

资源权限

这个其实就是LinkedHashMap,需要注意的是这是个有顺序的Map,先插入的权限数据优先判断,另外这个Map的键是访问url规则,值为具体权限,取值有:

  • authc:所有url都必须认证通过才可以访问;
  • anon:所有url都都可以匿名访问
  • user:如果使用rememberMe的功能可以直接访问
  • perms: 该资源必须得到资源权限可以访问
  • roles: 该资源必须得到角色权限才能访问
public Map<String, String> findFilterChainDefinitionMap(){
    // todo: 后面这个可以直接从数据库里面获取
    // 注意这个map
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    //按顺序依次判断
    filterChainDefinitionMap.put("/favicon.ico", "anon");
    filterChainDefinitionMap.put("/login/login", "anon");
    filterChainDefinitionMap.put("/login/login/pwd", "anon");
    filterChainDefinitionMap.put("/login/logout", "logout");
    filterChainDefinitionMap.put("/admin", "roles[admin]");
    filterChainDefinitionMap.put("/user", "roles[user]");
    // 注意权限设置顺序
    filterChainDefinitionMap.put("/a/1", "roles[admin]");
    filterChainDefinitionMap.put("/a/**", "roles[user, admin]");
    // 这种默认是and,即同时拥有admin和user才能访问
    filterChainDefinitionMap.put("/info", "roles[admin,user]");
    filterChainDefinitionMap.put("/perms", "perms[admin]");
    
    filterChainDefinitionMap.put("/**", "authc");
    return filterChainDefinitionMap;
}

ShiroFilterFactoryBean

Shiro的核心配置类

初始化权限

@Autowired
private ShiroService shiroService;
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    // 上面自定义的filterMap
    shiroFilterFactoryBean.setFilters(shiroService.filterMap());
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    shiroFilterFactoryBean.setLoginUrl("/login/login");
    shiroFilterFactoryBean.setUnauthorizedUrl("/deny");
    // 上面编写资源权限Map
    Map<String, String> filterChainDefinitionMap = shiroService.findFilterChainDefinitionMap();
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return shiroFilterFactoryBean;
}

动态更新权限

@Autowired
private ShiroFilterFactoryBean shiroFilterFactoryBean;
@Autowired
private ShiroService shiroService;

public synchronized void updatePermission() throws Exception {
    AbstractShiroFilter shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean.getObject();
    DefaultFilterChainManager manager = (DefaultFilterChainManager) ((PathMatchingFilterChainResolver)
                shiroFilter.getFilterChainResolver()).getFilterChainManager();
    manager.getFilterChains().clear();
    shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();
    // 上面编写资源权限Map
    Map<String, String> filterChainDefinitionMap = shiroService.findFilterChainDefinitionMap();
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    Map<String, String> chains = shiroFilterFactoryBean.getFilterChainDefinitionMap();
    for (Map.Entry<String, String> entry : chains.entrySet()) {
        manager.createChain(entry.getKey(), entry.getValue());
    }
}

登录

@PostMapping("/login")
public String login(String username, String password, Model model, HttpServletRequest req) {
    // 在认证提交前准备 token(令牌)
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    // 执行认证登陆
    // 可以获取登录前的访问request对象
    String url = WebUtils.getSavedRequest(req).getRequestURI();
    System.out.println(url);
    // 从SecurityUtils里边创建一个 subject
    Subject subject = SecurityUtils.getSubject();
    try {
        subject.login(token);
    } catch (AuthenticationException e) {
        // 包括未知账户、密码错误、用户名或密码错误次数过多、账户已锁定、用户名或密码不正确等异常
        model.addAttribute("msg", e.getMessage());
    }
    if (subject.isAuthenticated()) {
        Session session = subject.getSession();
        // session.setTimeout(30 * 1000);
        session.setAttribute("user", "这里有登录信息");
        model.addAttribute("msg", "登录成功");
        return "index";
    } else {
        token.clear();
    }
    return "login";
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值