SpringBoot整合shiro实现用户的认证授权
- * 项目环境搭建
- * 配置ShiroConfig,用于shiro的基本配置和注入自定义规则
- * 实现自定义的realm,继承AuthorizingRealm
- * 编写测试controller和页面
- 基本环境准备
- 导入依赖坐标
- maven管理、shiro1.4.0 和spring-shiro1.4.0依赖
- 导入数据源,配置thymeleaf,redis,等等
- shiro配置
- 配置shiroConfig
- 编写自定义的realm
- 实现具体的doGetAuthorizationInfo(授权)方法和doGetAuthenticationInfo(认证)
具体实现:
shiroConfig配置
@Bean(name = "securityManager") public DefaultWebSecurityManager securityManager(@Qualifier("myRealm") MyRealm myRealm){ DefaultWebSecurityManager ds = new DefaultWebSecurityManager(); ds.setRealm(myRealm); return ds; } @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultSecurityManager){ ShiroFilterFactoryBean sf = new ShiroFilterFactoryBean(); //设置安全管理器 sf.setSecurityManager(defaultSecurityManager); sf.setLoginUrl("/login"); sf.setUnauthorizedUrl("/non"); sf.setSuccessUrl("/index"); /** * 自定义过滤器 * anon: * authc: * user: 只有实现了remberme的操作才能访问 * perms: 必须得到资源权限才能访问 * role: 必须得到角色权限的时候才能访问 */ Map<String,String> china = new LinkedHashMap<>(); china.put("/index","authc"); china.put("/update","authc"); china.put("/non","authc"); china.put("/toLogin","anon"); china.put("/add","perms[user:add]"); china.put("/**","anon"); sf.setFilterChainDefinitionMap(china); return sf; } @Bean(name = "myRealm") public MyRealm getRealm(){ return new MyRealm(); }
自定义编写realm 继承AuthorizingRealm 实现doGetAuthorizationInfo 和doGetAuthenticationInfo方法
/** * 自定义授权逻辑 * Authorization * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //给资源授权 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //加上授权字符串(当前用户已经授权) simpleAuthorizationInfo.addStringPermission("user:add"); return simpleAuthorizationInfo; } /** * 自定义认证的逻辑 * 判断用户名和密码 * * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //直接强转 UsernamePasswordToken auth = (UsernamePasswordToken) authenticationToken; System.out.println("处理用户登录逻辑"); char[] password = auth.getPassword(); String username = auth.getUsername(); if(!"user".equals(username)){ //返回null 会抛出 UnknownAccountException return null; } //用户传入的密码 数据库中加载出来的密码 return new SimpleAuthenticationInfo(password,"123",""); }
首先配置shiro的配置类使用@Configuration注解类上,这里面我们需要基本的三个配置类
@Bean
ShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultSecurityManager)
@Bean(name = "securityManager")
DefaultWebSecurityManager (@Qualifier("myRealm") MyRealm myRealm)
@Bean(name = "myRealm")
MyRealm()
第一个是可以自定义认证授权规则,配置权限拦截规则指定跳转页面
第二个是将shiro的安全管理器注入,然后返回
第三种是自定义实现自己认证授权逻辑
自定义realm
继承自AuthorizingRealm,实现其中的两个方法
`protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection)`
1. 此方法是授权逻辑的实现,指定用户可以拥有那些权限,将其set到addStringPermission集合中即可
2. 其中授权类是其的一个子类
3. SimpleAuthorizationInfo simpleAuthorizationInfo
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
此方式认证逻辑,用于判断用户是否登录成功,或者异常
用户名判断错误返回 null
密码使用new SimpleAuthenticationInfo(用户输入的密码,数据库的密码,"");
举个例子:
/** * 自定义授权逻辑 * Authorization * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //给资源授权 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //加上授权字符串(当前用户已经授权) simpleAuthorizationInfo.addStringPermission("user:add"); return simpleAuthorizationInfo; } /** * 自定义认证的逻辑 * 判断用户名和密码 * * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken auth = (UsernamePasswordToken) authenticationToken; System.out.println("处理用户登录逻辑"); char[] password = auth.getPassword(); String username = auth.getUsername(); if(!"user".equals(username)){ //返回null 会抛出 UnknownAccountException return null; } return new SimpleAuthenticationInfo(password,"123",""); } /** * 自定密码判断 * * @param pass * @return */ private boolean isPassWord(char[] pass){ String password = "123"; if(pass.length != password.length()){ return false; } char[] chars = password.toCharArray(); for(int i = 0; i < pass.length; i++){ if(chars[i] != pass[i]){ return false; } } return true; }
// 当然授权也可以在controller层中通过@RequiresPermissions("user:update")注解在当前用户操作的地址上授权
<p style="color:red">
注意: 这里面需要在shiroconfig中配置以下内容:
</p>
/**
* 解决@RequiresPermissions("XX:XXX:...")注解无效
*
* @return
*/
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager defaultSecurityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(defaultSecurityManager);
return advisor;
}
并且在controller里面配置的权限检验,用户验证失败,会跳转到/error页面上,需要自己自定义页面
注意:
这里面有个大坑,开始使用如下代码发现并未解决问题
@Bean public SimpleMappingExceptionResolver simpleMappingExceptionResolver(){ SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver(); Properties properties = new Properties(); properties.put("org.apache.shiro.authz.UnauthorizedException","/non"); simpleMappingExceptionResolver.setExceptionMappings(properties); return simpleMappingExceptionResolver; }
最后通过springMVC的异常处理类指定跳转页面,才解决此问题。
@ControllerAdvice public class MyExceptionHandler { @ExceptionHandler(UnauthenticatedException.class) public String nauthenticatedException(Model model){ model.addAttribute("errorMsg","当前无用户!"); return "nonauth"; } @ExceptionHandler(UnauthorizedException.class) public String nauthorizedException(Model model){ model.addAttribute("errorMsg","当前用户没有权限"); return "nonauth"; } }
不错
现在已经解决了两个问题:
1. 在用户未登录的时候,直接访问带有@RequiresPermissions权限的注解时会报错,而不是跳转到指定页面,或者返回相应的信息
2. 未使用在remal配置的权限过滤器的时候,而是在@Controller上直接带有@RequiresPermissions("user:add") 会报500错误,而不是可控操作
在doGetAuthenticationInfo方法中,获取用户登录时候的信息操作,验证用户,将用户存入session,以后通过subject对象强转。
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo( user, user.getPassword(), ByteSource.Util.bytes(salt), //realm name getName());
user 是开始通过用户名查询到的用户信息
salt是加盐处理,硬编码加盐处理
#shiro密码匹配#
这里先来个简单的直接加盐 然后使用MD5加密方式
密码匹配则时先将用户输入的密码加盐再字符串比较
实现自定义的Realm 继承自AuthorizingRealm doGetAuthenticationInfo方法中使用 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo( 用户对象 user, 数据库用户密码 user.getPassword(), 加盐处理 ByteSource.Util.bytes(salt), //realm name getName()); 将用户对象保存至 String string = MD5Util.encryptString(password+salt); UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName,string);
使用shiro的HashedCredentialsMatcher自定义密码加密
先模拟用户和密码数据 使用单元测试:
@Test public void contextLoads() { String algorithmName = "md5"; String username = "admin"; String password = "123"; String salt1 = username; String salt2 = new SecureRandomNumberGenerator().nextBytes().toHex(); int hashIterations = 3; SimpleHash hash = new SimpleHash(algorithmName, password, salt1 + salt2, hashIterations); String encodedPassword = hash.toHex(); System.out.println(encodedPassword); System.out.println(salt2); }
将生成的数据保存到数据库中,验证的时候将密码和盐拿出来,这时候,用户的注册名和密码在注册的时候保存到数据库中
在Shiro的配置类中,注入
/** * 凭证匹配器 * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 * 所以我们需要修改下doGetAuthenticationInfo中的代码; * ) * @return */ @Bean(name = "hashedCredentialsMatcher") public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher = new MyHashedCredent(); //使用指定的散列算法 hashedCredentialsMatcher.setHashAlgorithmName("MD5"); //几次散列? hashedCredentialsMatcher.setHashIterations(3); //设置16进制编码 hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true); return hashedCredentialsMatcher; }
在将其自定义的密码加载类注入到自定义的realm中
@Bean(name = "myRealm") public MyRealm getRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher hashedCredentialsMatcher){ MyRealm myRealm = new MyRealm(); // 设置自定义加密 myRealm.setCredentialsMatcher(hashedCredentialsMatcher); return myRealm; }
下面实现自定义密码:
/** * @author zhangyi * @date 2018/12/12 20:43 */ public class MyHashedCredent extends HashedCredentialsMatcher { public MyHashedCredent(){} /** * 以后放到redis中保存 */ private Cache<String, AtomicInteger> passwordRetryCache; public MyHashedCredent(CacheManager cacheManager) { passwordRetryCache = cacheManager.getCache("passwordRetryCache"); } @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { //限制每个用户的请求登录次数(密码错误的时候) String username = (String) token.getPrincipal(); if(!Objects.isNull(passwordRetryCache)) { // retry count + 1 AtomicInteger retryCount = passwordRetryCache.get(username); if (retryCount == null) { retryCount = new AtomicInteger(0); passwordRetryCache.put(username, retryCount); } if (retryCount.incrementAndGet() > 5) { // if retry count > 5 throw throw new ExcessiveAttemptsException(); } } boolean matches = super.doCredentialsMatch(token, info); if (matches) { // clear retry count // passwordRetryCache.remove(username); } return matches; } }
按照开涛神的方法,计算其密码重试的次数,将其保存到EhCache中,这里我保存到redis,因为好用吧。
开始使用硬编码加盐,网上说可能会有安全问题,现在采用
用户名+密码+随机数 --> 在N次散列 加密,应该好一点点
我这里是继承了HashedCredentialsMatcher这中加密方式,还可以继承SimpleCredentialsMatcher,或者继承PasswordMatcher,或者实现CredentialsMatcher接口来加密,不过本质上差不多