SpringBoot整合Shiro框架

最近这几周空闲的时候都在做个人项目,为了尝试更多开发日常用得比较多的轮子。所以放弃了传统写PHP时自己定规则而设的session登陆系统。陆陆续续试了SpringSecurity和Shiro。由于我的项目使用的是SpringBoot,本着Spring工具链的原则,第一步先是采用了SpringSecurity,后来由于太重,颗粒度太细不适合项目而放弃。最终决定使用Shiro。

所以第一步是先将Shiro集成到原有的项目中来。

在集成的过程中发现了问题,无论是SpringSecurity还是Shiro,这两个项目在官方网站中关于集成到SpringBoot的资料都非常少,更不要说是官网教程了。需要自己慢慢摸索的地方比较多。整个过程中我更多参考了两个项目中关于集成到Spring中的文档部分来做的。

Shiro

Shiro是一个功能强大且易于使用的Java安全框架,可执行身份验证,授权,加密和会话管理。借助Shiro易于理解的API,您可以快速轻松地保护任何应用程序 – 从最小的移动应用程序到最大的Web和企业应用程序。

参考文档

先填几个Shiro集成Springboot的文档。

集成到SpringBoot

Maven

由于项目中采用了Maven,此处直接修改pom.xml文件添加jar包。

<!-- shiro-spring 安全框架 -->
<dependency>
     <groupId>org.apache.shiro</groupId>
     <artifactId>shiro-spring-boot-web-starter</artifactId>
     <version>1.4.0-RC2</version>
</dependency>
<!-- thymeleaf模板中shiro标签 -->
<!-- 我项目中采用了thymeleaf,如果不使用,无需添加这条 -->
<dependency>
     <groupId>com.github.theborakompanioni</groupId>
     <artifactId>thymeleaf-extras-shiro</artifactId>
     <version>2.0.0</version>
</dependency>

application.properties 配置文件

我参考了部分网上的资料。有部分博主在Springboot的application.properties部分,是有Shiro的配置的。而IDEA在添加为Shiro的jar之后,的确也有Shiro在默认配置中的提示。但是我添加上去后,Shiro并不生效,也没有读取这出的配置。这边仅贴出来作为参考。

# Shiro
shiro.web.enabled=true
# 登陆页面URL
shiro.loginUrl =
# 登陆成功后跳转的URL
shiro.successUrl=/
shiro.sessionManager.sessionIdUrlRewritingEnabled =false
# 登陆失败跳转的URL
shiro.unauthorizedUrl=/index

Shiro Config (JavaConfig)

这部分是SpringBoot中Shiro中最关键的配置,主要的代码均有注释说明。如有不懂可以留言联系。(这其中注意import引入的包是否正常,有部分IDEA容易提示错。 )

其中ShiroRealmConfig是我自定义的AuthorizingRealm实例。Shiro需要通过自己编写Realm的实例来完成认证和授权的逻辑。

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;


/**
 * Shiro 安全框架配置类
 */
@Configuration
public class ShiroConfig {

    @Value("${shiro.loginUrl}")
    private String loginUrl;

    @Value("${shiro.successUrl}")
    private String successUrl;

    @Value("${shiro.unauthorizedUrl}")
    private String unauthorizedUrl;

    @Bean
    protected CacheManager cacheManager() {
        return new MemoryConstrainedCacheManager();
    }

    /**
     * 配置ShiroDialect,用于整合标签
     *
     * @return
     */
    @Bean
    public ShiroDialect getShiroDialect() {
        return new ShiroDialect();
    }


    /**
     * 密码校验规则HashedCredentialsMatcher
     * 这个类是为了对密码进行编码的 ,
     * 防止密码在数据库里明码保存 , 当然在登陆认证的时候 ,
     * 这个类也负责对form里输入的密码进行编码
     * 处理认证匹配处理器:如果自定义需要实现继承HashedCredentialsMatcher
     */
    @Bean("hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //指定加密方式为MD5
        credentialsMatcher.setHashAlgorithmName("MD5");
        //加密次数
        credentialsMatcher.setHashIterations(1024);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    /**
     * 选用加密方式
     *
     * @param matcher 加密
     * @return Realm实例
     */
    @Bean(name = "shirorealmconfig")
    public ShiroRealmConfig getShiroRealmConfig(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
        ShiroRealmConfig shiroRealmConfig = new ShiroRealmConfig();
        shiroRealmConfig.setCredentialsMatcher(matcher);
        return shiroRealmConfig;
    }

    @Bean(name = "securitmanager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("shirorealmconfig") ShiroRealmConfig shiroRealmConfig) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(shiroRealmConfig);
        return defaultWebSecurityManager;
    }

    @Bean
    public ShiroFilterFactoryBean getshiroFilterFactoryBean(@Qualifier("securitmanager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        shiroFilterFactoryBean.setLoginUrl(loginUrl);
        shiroFilterFactoryBean.setSuccessUrl(successUrl);
        shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
        Map<String, String> map = new LinkedHashMap<>();
        map.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    /**
     * 这里统一做鉴权,即判断哪些请求路径需要用户登录,哪些请求路径不需要用户登录。
     * 这里只做鉴权,不做权限控制,因为权限用注解来做。
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        return new DefaultShiroFilterChainDefinition();
    }
}

Shiro Realm 实例

Realm主要是Shiro的认证和授权逻辑,需要自定义。通过继承AuthorizingRealm类,重写doGetAuthenticationInfo和doGetAuthorizationInfo方法实现。

这个类每个项目中基本都是不同的,其中需要注意的点非常多。我建议阅读一下官方文档中的10分钟快速入门教程先了解一下。

无论学习什么工具、项目。我都建议先到官方文档了解一下具体的实现、说明。如果还不懂再去看别人总结的教程。

我这边贴一下自己这边的代码作为参考。(其中强耦合的代码很多,我做了部分的删减并增加了注释。不建议直接复制)

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

public class ShiroRealmConfig extends AuthorizingRealm {

    private static final Logger LOGGER = LoggerFactory.getLogger(ShiroRealmConfig.class);

    @Resource
    private MyUserDao myUserDao;
    @Resource
    private MyRoleDao myRoleDao;

    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername(); //获得登陆表单传来的username
        //而后一般是通过数据库取出相应的唯一的用户。
        //注意,认证逻辑中不包含密码对比。因为Shiro会帮我们做这一步。
        return new SimpleAuthenticationInfo(
                用户名/用户实例,
                加密后的密码(不能是明文),
                ByteSource.Util.bytes(), //盐值,如不使用可为空
                getName());
    }

    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        if (principalCollection == null) {
            LOGGER.error("异常错误");
            throw new AuthenticationException("系统异常,请检查");
        }
        //取出用户名。该处是SimpleAuthenticationInfo第一个参数
        String username = (String) super.getAvailablePrincipal(principalCollection); 
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(**); //该处需要填写用户角色,通常在数据库中取出。
        info.setStringPermissions(); //该处需要填写用户权限,通常在数据库中取出。
        return info;
    }
}

Shiro Exception

在重写AuthorizingRealm类,自定义doGetAuthenticationInfo和doGetAuthorizationInfo方法时,如需对其他返回错误信息。可以抛出Shiro一些已经定义好的Exception。Shiro内部相应也是使用这些Exception。如密码错误,Shiro会抛出IncorrectCredentialsException,你需要在前台进行捕抓。

try {
    currentUser.login( token );
    //if no exception, that's it, we're done!
} catch ( UnknownAccountException uae ) {
    //用户名不存在
} catch ( IncorrectCredentialsException ice ) {
    //密码不匹配
} catch ( LockedAccountException lae ) {
    //用户账户已锁定
}
    你也可以直接抛出以下异常
} catch ( AuthenticationException ae ) {
    //意外情况
}

至此,Shiro就基本配置好了。

登陆逻辑

Shiro对程序的侵入性不强。登陆表单可以按照往常一样写。表单提交的URL也不需要特别的改动。只需要在执行登陆判断的逻辑的Servlet中。传入账号以及密码即可。

public String Login(String username, String password, String url) {
        //你可在传入值之前,对值作出校验。

        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        token.setRememberMe(false);//是否记住我
        try {
            SecurityUtils.getSubject().login(token);
        } catch (UnknownAccountException uae) {
            return JsonUtil.error(uae.getMessage());
        } catch (IncorrectCredentialsException ice) {
            //password didn't match, try again?
            return JsonUtil.error("账号或密码错误");
        } catch (LockedAccountException lae) {
            //account for that username is locked - can't login.  Show them a message?
            return JsonUtil.error("账户被锁定");
        } catch (AuthenticationException ae) {
            //unexpected condition - error?
            return JsonUtil.error("未知错误,请联系管理员");
        }
        return success("登陆成功");
    }

其他

注册

Shiro要求用户提交给SimpleAuthenticationInfo的密码是加密的。(前端传入不需要,Shiro会自行加密,这里值的是数据库取出的数据)

这就要求我们在注册逻辑中使用与Shiro中配置的一样的加密。Shiro提供了相应的方法。需要注意的是,该方法的参数需要和ShiroConfig中的HashedCredentialsMatcher方法一致。不然用户密码始终都会对不上的。

String hashAlgorithmName = "MD5";
int hashIterations = 1024;
Object obj = new SimpleHash(hashAlgorithmName, 密码, 加密盐值(如不需要可以留空), hashIterations);
LOGGER.info("密码:" + obj);

当然,你也可以自行实现。

发表评论

电子邮件地址不会被公开。 必填项已用*标注