springboot整合shiro详解


前言

记得在我培训的时候为了防止用户在不登入的状态下访问到其他页面,需要是在jsp中写一个防盗链,始终感觉那样挺麻烦的而且功能还单一,然后就想到使用shiro。

知识概念

一、什么是shiro?

简单来说shiro是一个开源安全的框架,它可以让我们更加简单方便的处理身份认证、授权、会话管理以及加密等功能。

二、shiro能做什么?



Authentication:验证用户身份,有点类似防盗链
Authorization:给用户赋予权限,不同的用户可以拥有不同的权限,同时根据权限的不同可以限制其访问的范围
Session Management:管理用户会话,即使没有web容器或者EJB容器也可以管理
Cryptography:数据安全加密
• Web Support:拥有一套健全的WebApi支持Web应用程序开发
• Caching:shiro支持数据缓存,保证数据访问安全、高效、快速
• Concurrency:shiro支持并发多线程应用程序
• Testing:shiro支持测试应用程序
• Run As:支持用户伪装成另一个用户身份,通常是管理员测试使用
• Remember Me:在Session中记住用户身份,Session不失效,就不会要求你重新登入,当Session失效或被强制退出时需要重新登入
• SSO:shiro支持应用程序实现单点登入,即一个账户不能同时在多个地方登入

三、shiro架构



Shiro架构有3个主要概念:SubjectSecurityManagerRealm
Subject:相当于用户的意思,但是这里的用户泛指任何事物,不一定是人可以是第三方进程也可以是后台账户等泛指当前正在与软件进行交互的东西,每个Subject都需要与SecurityManager进行绑定,所有Subject统一交由SecurityManager进行管理,访问Subject其实就是访问SecurityManager中特定的subject。
SecurityManager:SecurityManager是shiro的核心,对Subject的安全操作进行保护,统一管理所有Subject。
Realms:Realm相当于一个特定的安全DAO,充当数据与应用程序交互的桥梁。Realms定义了认证、授权方式,如果默认的Realm不能满足你的需求,可以自定义Realm来满足需求。配置shiro时,至少需要一个Realm用来进行身份验证或授权,SecurityManager可以配置多个Realm,但至少一个是必须的。

四、示例


框架:Springboot;shiro官方Demo
Poom.xml
<dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.0</version>
</dependency> 
通过shiro架构图我们可以清楚的知道流程执行顺序,那么实现shiro的功能就需要逆推原理。
1、自定义的Realm实现
如下图shiro定义了很多类或抽象类供我们来实现自定义的Realm,我选用的是继承AuthorizingRealm,因此需要重写它的两个方法doGetAuthorizationInfo和doGetAuthenticationInfo方法。


UserRealm:
public class UserRealm extends AuthorizingRealm{

    @Autowired
    UserService userService;
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(
            PrincipalCollection principals) {
        String userName = (String)principals.fromRealm(getName()).iterator().next();
        User user = userService.getUserInfoByUname(userName);
        if(null == user)return null;
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        for(Role role:user.getRoleList()){
            info.addRole(role.getRole());
            for(Permission p:role.getPermissionList()){
                info.addStringPermission(p.getPermission());
            }
        }
        return info;
    }


    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken token) throws AuthenticationException {
        String userName = (String) token.getPrincipal();
        if(null != userName && !userName.equals("")){
            User user = userService.getUserByUname(userName);
            if(null != user ){
                return new SimpleAuthenticationInfo(userName,user.getUserPwd(),getName());
            }
            //如果用户都不存在那么一定验证失败
            throw new AuthenticationException();
        }
        throw new AuthenticationException();
    }

}
1、doGetAuthorizationInfo方法的作用看代码也知道是授权,这个就不多说了。

2、doGetAuthenticationInfo方法的作用是身份认证但是并没有验证密码是否正确,真正验证身份的正确性是在AuthenticatingRealm的assertCredentialsMatch中体现,具体请看shiro官方给出的源码:

继承关系:如下图


AuthenticatingRealm抽象类中的方法:
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    AuthenticationInfo info = getCachedAuthenticationInfo(token);
    if (info == null) {
        //otherwise not cached, perform the lookup:
        //此处会调用自定义中重写的doGetAuthenticationInfo方法
        info = doGetAuthenticationInfo(token);
        log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
        if (token != null && info != null) {
            cacheAuthenticationInfoIfPossible(token, info);
        }
    } else {
        log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
    }

    if (info != null) {
        assertCredentialsMatch(token, info);
    } else {
        log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
    }

    return info;
}
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
    CredentialsMatcher cm = getCredentialsMatcher();
    if (cm != null) {
        //此处是验证账号密码的正确性
        if (!cm.doCredentialsMatch(token, info)) {
            //not successful - throw an exception to indicate this:
            String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
            throw new IncorrectCredentialsException(msg);
        }
    } else {
        throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " +
                "credentials during authentication.  If you do not wish for credentials to be examined, you " +
                "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
    }
}

    从上述源码中可以看出cm.doCredentialsMatch(token, info)才是验证的关键,token中有登入时的账号和密码,info是自定义UserRealm中的doGetAuthenticationInfo返回的对象也有账号和密码,当cm.doCredentialsMatch(token, info)为真的时候证明身份认证成功,当为假的时候登入不成功但是会抛出throw new IncorrectCredentialsException(msg)异常,此时只需要在Subject.login(token)上try,catch捕获AuthenticationExceptionIncorrectCredentialsException异常即可

2、SecurityManager配置
@Configuration
public class ShiroConfiguration {

    //将自己的验证方式加入容器
    @Bean
    public UserRealm myShiroRealm() {
        UserRealm myShiroRealm = new UserRealm();
        return myShiroRealm;
    }

    //SecurityManager配置
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }
    //Filter工厂,设置对应的过滤条件和跳转条件
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String,String> map = new ConcurrentHashMap<String, String>();
        //登出
        map.put("/logout","logout");
        //对所有用户认证
        map.put("/**","authc");
        map.put("/user/validate","anon");

        //静态资源匿名获取,否则页面将无法获取样式等
        map.put("/static/**", "anon");
        map.put("/css/**", "anon");
        map.put("/swf/**", "anon");
        map.put("/fonts/**", "anon");
        map.put("/img/**", "anon");
        map.put("/js/**", "anon");
        map.put("/layer/**", "anon");
        map.put("/layer/theme/default/**","anon");
        map.put("/layer/mobile/**","anon");
        map.put("/i18n/**","anon");
        //url拦截,匿名提交会自动跳转到登录界面
        shiroFilterFactoryBean.setLoginUrl("/");
        //登录成功首页
        //shiroFilterFactoryBean.setSuccessUrl("/index");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }
    //提供Aop支持
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    //当用户没有权限的时候跳进指定的页面
    @Bean
    public SimpleMappingExceptionResolver unauthoriedCatch(){
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
        Properties exception = new Properties();
        //value 是页面的名称不是controller中的路径
        exception.setProperty("org.apache.shiro.authz.UnauthorizedException", "unauthorized");
        exceptionResolver.setExceptionMappings(exception);
        return exceptionResolver;
    }

}
3、获取当前正在与软件交互的Subject
Subject subject = SecurityUtils.getSubject();
获取后就可以进行很多操作,例如
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
登入:subject.login(token);
登出:subject.logout();
获取Session:subject.getSessioin();
4、给请求增加权限或角色限制
@RequiresRoles(value={"Admin","SuperAdmin"},logical=Logical.OR)
@RequiresPermissions("database")
用户登录成功时会根据你在自定义的UserRealm中doGetAuthorizationInfo方法给用户赋予相应角色和权限,
如果用户的权限或角色满足注解上权限或角色限制就可以成功访问否则就跳转到指定的页面

总结

      整个流程:当用户登入时需要先获取当前正在与软件交互的Subject对象,然后调用login方法进行登录,登录会先验证身份如果验证成功就给用户赋予权限和角色,此时用户已经登入成功了,但是有些url是需要一定的权限或角色才能访问,所以在请求这些url的时候会根据该用户的权限或角色去跟url上的限制进行匹配如果成功就放行否则就会被阻挡