最近这几周空闲的时候都在做个人项目,为了尝试更多开发日常用得比较多的轮子。陆陆续续试了SpringSecurity和Shiro。由于我的项目使用的是SpringBoot,本着Spring工具链的原则,第一步先是采用了SpringSecurity,后来由于太重,颗粒度太细不适合项目而放弃。最终决定使用Shiro。
所以第一步是先将Shiro集成到原有的项目中来。
在集成的过程中发现了问题,无论是SpringSecurity还是Shiro,这两个项目在官方网站中关于集成到SpringBoot的资料都非常少,更不要说是官网教程了。需要自己慢慢摸索的地方比较多。整个过程中我更多参考了两个项目中关于集成到Spring中的文档部分来做的。
Shiro
Shiro是一个功能强大且易于使用的Java安全框架,可执行身份验证,授权,加密和会话管理。借助Shiro易于理解的API,您可以快速轻松地保护任何应用程序 – 从最小的移动应用程序到最大的Web和企业应用程序。
参考文档
先填几个Shiro集成Springboot的文档。
- Apache Shiro 官网
- Integrating Apache Shiro into Spring-Boot Applications (一个关于Shiro集成到Springboot的官方文档。但是非常奇怪。这个问题需要搜索才能找到,官网并没有给出链接。而且非常简约,并不是很全面)
- Spring Boot + Shiro 集成
- Shiro-密码的MD5加密
集成到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);
当然,你也可以自行实现。