Shiro使用笔记

0 本文主要涉及

shiro在基于Spring和SpringMVC的前后端分离的JavaWeb项目中认证和授权授权功能的使用

1 shiro简介

shiro是Apache提供的开源的基于Java实现的安全框架
官网:http://shiro.apache.org/index.html
优点:配套功能完善,接口易于使用
主要功能:身份验证,权限验证,会话管理、加密等等

Shiro使用笔记_第1张图片

基本架构:

Subject :实体,代表当前用户,方便交互,实际功能逻辑是由SecurityManager实现
SecurityManager : 安全管理器,负责所有与安全相关的操作,是Shiro的核心,负责与Shiro的其他组件进行交互
Realm : Shiro从Realm获取安全数据(如用户,角色,权限),数据源,需要我们自己实现并提供给框架
Authenticator : 负责身份验证,提供接口,需要我们自己实现并提供给框架
Authorizer :负责权限验证,提供接口,需要我们自己实现并提供给框架
SessionManager 会话管理器,不仅仅可以在Web环境中使用,也可以在普通javaSE中使用
SessionDAO:所有会话的CRUD功能
CacheManager:缓存控制器,来管理用户,角色,权限等的缓存
Cryptography : 密码模块,提供了一些常见的加密组件用于加密和解密

Shiro使用笔记_第2张图片

2 shiro配置集成

0 依赖配置

使用了Maven进行项目依赖管理,JavaWeb基础的依赖这里就不多说了,Shiro相关的如下


    org.apache.shiro
    shiro-core
    1.4.0


    org.apache.shiro
    shiro-web
    1.4.0


    org.apache.shiro
    shiro-spring
    1.4.0


    org.apache.shiro
    shiro-ehcache
    1.4.0


    de.svenkubiak
    jBCrypt
    0.4.1


    net.sf.ehcache
    ehcache
    2.10.2

1 继承实现一个Realm,实现认证和授权的接口

Shiro框架不会去维护用户、角色和权限,需要我们自己去设计/提供,然后通过相应的接口注入给Shiro使用。
具体的,首先设计用户 角色和权限的表(简单的就三个表,一个用户有一种角色,每种角色有多种权限,复杂的可以建中间表实现多对多的映射关系)
然后实现一个基本的认证和鉴权所需的DAO,大概需要根据用户名获取用户,根据用户名获取角色集合,根据用户名获取权限集合(不一定是这样的,可以根据自己Realm的实现所需提供对应的DAO方法)
继承AuthorizingRealm实现一个自己的Realm,在Realm中主要工作就是实现doGetAuthorizationInfo(认证),doGetAuthenticationInfo(鉴权)两个方法,代码如下(代码中使用了BCrypt一种更方便的加盐hash方案,还带有重复登录限制逻辑)

public class MyRealm extends AuthorizingRealm
{
    @Autowired
    private AuthorityService authorityService;//底层调用了上面所说的DAO

    public MyRealm()
    {
        super();
        //BcryptPasswordMatcher
        setCredentialsMatcher(new CredentialsMatcher()
        {
            @Override
            public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info)
            {
                UsernamePasswordToken userToken = (UsernamePasswordToken) token;
                String name = userToken.getUsername();
                String password = new String(userToken.getPassword());
                String hashed = info.getCredentials().toString();

                //重复登陆限制
                Cache passwordRetryCache = getCacheManager().getCache("passwordRetryCache");
                Integer count = (Integer) passwordRetryCache.get(name);
                if(count == null)
                {
                    passwordRetryCache.put(name, 1);
                }
                else
                {
                    passwordRetryCache.put(name, count++);
                    if(count > 5)
                    {
                        throw new ExcessiveAttemptsException();
                    }
                }

                boolean matches = BCrypt.checkpw(password, hashed);
                if(matches)
                {
                    passwordRetryCache.remove(name);
                }
                return matches;
            }
        });
    }

    /**
     * 用于的权限的认证。
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection)
    {
        String username = principalCollection.getPrimaryPrincipal().toString();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        //用户角色(role)放入到Authorization里。
        Set roleName = authorityService.getRoles(username);
        info.setRoles(roleName);

        //用户权限(permission)放入到Authorization里。
        Set permissions = authorityService.getPermissions(username);
        info.setStringPermissions(permissions);

        return info;
    }

    /**
     * 首先执行这个登录验证
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException
    {
        //UsernamePasswordToken对象用来存放提交的登录信息
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        //获取用户账号
        String username = token.getUsername();
        //查出是否有此用户
        UserEntity user = authorityService.getUserByUsername(username);
        if(user != null)
        {
            //若存在,将用户账号和密码存放到 authenticationInfo用于后面的权限判断。第三个参数传入realName。
            return new SimpleAuthenticationInfo(user.getName(), user.getPassword(), user.getRealName());
        }
        else
        {
            // 用户名不存在抛出异常
            throw new UnknownAccountException();
        }
    }
}

2 配置缓存

缓存通过EhCache实现
在缓存的SpringBean中写入shiroEncacheManager


具体缓存申明如下


    
    
    
    

    
    

    
    

    
    

    
    
    

3 在web.xml中配置shiro的过滤器



    shiroFilter
    org.springframework.web.filter.DelegatingFilterProxy
    
        
        targetFilterLifecycle
        true
    


    shiroFilter
    /*

4 定义Shiro的SpringBean

1 配置自定义Realm





    
    
    

    
    

2 配置安全管理器




    
    
    


    
    
    
    





    
    
    





    
    
    



    
    


     
     
     
     
    
    
    
    



    
    
    
    



    
    

3 设置Shiro过滤器

注意:有顺序优先级,匿名的url配置要放在有登录或者用户限制的前面


    
    
    
    
    
    
    
    
    
    
        
        
            
            
            /js/**=anon
            /css/**=anon
            /img/**=anon
            /lib/**=anon

            
            /authority/signup*=anon
            /jsp/login*=anon
            /authority/login*=anon
            /authority/notLogin*=anon
            /authority/noAccess*=anon

            
            
            
            /jsp/dict*=roles[admin]
            
            
            
            
            
            

            /** = user
        
    

4 在springmvcbean中配置拦截器和开启注解功能



    


    

3 shiro常用示例

1 登录

Subject subject = SecurityUtils.getSubject();
//err.println(LogUtils.format(BCrypt.hashpw(user.getPassword(), BCrypt.gensalt())));
UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), user.getPassword(), rememberMe);//设置是否保持登录
try
{
    subject.login(token);
    AuthorityHelper.setCurrentUserEntity(authorityService.getUserByUsername(SecurityUtils.getSubject().getPrincipal().toString()));//自定义工具方法保存当前用户DO
    //if(WebUtils.getSavedRequest(request) != null)
    //{
    //    //配合前端实现登陆前页面跳转
    //    return WebUtils.getAndClearSavedRequest(request).getRequestUrl();
    //}
    //else
    //{
    return "登录成功!";
    //}
}catch(UnknownAccountException e)
{
    e.printStackTrace();
    throw new RuntimeException("用户名不存在!");
}catch(ExcessiveAttemptsException e)
{
    e.printStackTrace();
    throw new RuntimeException("登录失败次数过多!10分钟后重试");


}catch(IncorrectCredentialsException e)
{
    e.printStackTrace();
    throw new RuntimeException("密码错误!");
}catch(AuthenticationException e)
{
    e.printStackTrace();
    throw new RuntimeException("登录出错!");
}

2 Shiro注解使用

常用注解

  • @RequiresAuthentication : 表示当前Subject已经通过Login进行身份验证
  • @RequiresUser : 表示当前Subject已经身份验证或者通过记住我登录
  • @RequiresGuest : 表示当前Subject已经没有身份验证或通过记住我登录,即是游客身份
  • @RequiresRoles(value={"admin", "user"}, Logical.AND) : 表示当前Subject需要admin和user角色
  • @RequiresPermissions(value={"user:a", "user:b"}) : 表示当前Subject需要权限user:a或user:b

最常用的是@RequiresPermissions和@RequiresRoles,加在controler类上(把每个url看成是需要控制权限的资源)
一般Authentication或者User(这两个区别是是否通过通过“
住我”登录)会在过滤器的url中统一限制

关于如何限制权限,简单的直接通过RequiresRoles批量限制,如果是具体某个权限则通过RequiresPermissions来限制

理论上最好的方式是面向资源进行权限限制(即使用RequiresPermissions给不同资源设置权限)然后根据角色分配资源(设置权限),这样便于动态控制(用户表,角色表,资源权限表,角色资源权限表),参考http://globeeip.iteye.com/blog/1236167

3会话管理

当前会话
Subject.getSession();
//获取会话,默认为Subject.getSession(true)即当前没有创建Session则会创建, 
Subject.getSession(false)即当前没有Session返回NULL
session.getId(); //获取当前会话的唯一标示
session.getHost(); //获取当前Subject的主机地址
session.getTimeout() & session.setTimeout(毫秒); 
//获取/设置当前Session的过期时间
session.getStartTimestamp() & session.getListAccessTime(); 
//获取会话的启动时间以及最后访问时间,如果是JavaSE应用需要手动定期调用session.touch
去更新最后访问时间, 如果是Web应用每次进入ShiroFIlter会自动调用session.touch()进行更新最后访问时间

所有会话DAO
在配置中配置过org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO即可在代码中获取使用
enterpriseCacheSessionDAO.delete(enterpriseCacheSessionDAO.readSession(sessionId))

4 区分url访问和ajax请求

返回不同的登录失败或无授权结果(页面或者json数据)
需要真shiro 的springBean中注册过滤器
//身份验证过滤器,区分ajax请求(返回json数据)

并在shiro过滤器中配入


    
    
    
    
    
    

MyShiroAuthenticationFilter具体实现如下
主要重写了
onAccessDenied方法,增加了是否是ajax请求的判断逻辑并返回不同结果

public class MyShiroAuthenticationFilter extends UserFilter
{
    @Data
    @AllArgsConstructor
    class UnauthenticatedExceptionInfoType
    {
        //返回json数据的格式
        int statusCode;
        Date dateTime;
        String message;
    }
    private ObjectMapper objectMapper = new MyJacksonObjectMapper();

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception
    {
        if(isAjax(request))
        {
            ((HttpServletResponse) response).setHeader("Content-Type", "application/json;charset=utf-8");
            response.getWriter().print(objectMapper.writeValueAsString(new UnauthenticatedExceptionInfoType(HttpStatus.UNAUTHORIZED.value(), new Date(), "未登录!")));
        }
        else
        {
            saveRequestAndRedirectToLogin(request, response);
        }
        return false;
    }

    public static boolean isAjax(ServletRequest request)
    {
        String header = ((HttpServletRequest) request).getHeader("X-Requested-With");
        if("XMLHttpRequest".equalsIgnoreCase(header))
        {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }
}

 

 

你可能感兴趣的:(JavaWeb)