springboot整合shiro
shiro的简介
这是一款安全权限框架,进行角色、权限管理。
主要功能:
- Authentication:登录;
- Authorization:授权,返回用户所有的角色和权限;
- Session Manager:会话管理;
- Cryptography:加密。
主要的类:
- Subject:指当前用户,类似于Web里面的Session?
- SecurityManager:安全事务管理器,管理所有的subject,是shiro的核心,需要在内设置realm和rememberMeManager。
- principals:身份,即主体的标识属性,一般为唯一的用户名或id,一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。
- realm:指域,需要在内设置登录验证和授权验证。授权代码一般需要自己编写。
- 登录验证:从
token
中获取用户名,在数据库中查询是否有该用户,并将信息返回到authenticationInfo
之中。 - 授权认证:从
principals
获取用户信息,在数据库中查询该用户的角色集合和权限集合,并返回到authorizationInfo
- 登录验证:从
- HashedCredentialsMatcher:哈希密码比较器,在realm中作为参数使用,需要设置参数:
setHashAlgorithmName
和setHashIterations
,分别为加密算法和散列次数。
建表:
主要有五个表:user,role,permission,user-role,role-permission。
主要字段
- user:id,用户名,密码,盐值
- role:id,角色名,描述,状态
- permission:id,权限名,描述,url,父节点,类型,状态
配置:
主要为ShiroConfiguration,UserRealm,CacheConfiguration (缓存)
ShiroConfiguration:
package com.example.shiro.config;
import org.apache.log4j.Logger;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@Configuration
public class ShiroConfiguration {
private Logger logger=Logger.getLogger(ShiroConfiguration.class);
@Bean
public ShiroFilterFactoryBean shiroFilter (SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//登录页面
shiroFilterFactoryBean.setLoginUrl("/login");
//登录成功后的页面
shiroFilterFactoryBean.setSuccessUrl("index");
//权限不足返回的页面
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//自定义过滤器
Map<String, Filter> filterMap = new LinkedHashMap<>();
shiroFilterFactoryBean.setFilters(filterMap);
//权限控制map
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/user:add","perms[user:add]");
//配置不会被拦截的链接
filterChainDefinitionMap.put("/static/**","anon");
filterChainDefinitionMap.put("logout","logout");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 核心的安全事务管理器
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager ();
//设置realm
securityManager.setRealm(myShiroRealm());
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
/**
* 身份认证Realm,此处的注入不可以缺少。否则会在UserRealm中注入对象会报空指针.
* @return
*/
@Bean
public UserRealm myShiroRealm( ){
UserRealm myShiroRealm = new UserRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
/**
* 哈希密码比较器。在myShiroRealm中作用参数使用
* 登陆时会比较用户输入的密码,跟数据库密码配合盐值salt解密后是否一致。
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用md5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5( md5(""));
return hashedCredentialsMatcher;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* shiro缓存管理器;
* 需要注入对应的其它的实体类中: 安全管理器:securityManager
* 可见securityManager是整个shiro的核心;
* @return
*/
@Bean
public EhCacheManager ehCacheManager(){
logger.info("------------->ShiroConfiguration.getEhCacheManager()执行");
EhCacheManager cacheManager=new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return cacheManager;
}
/**
* 记住我管理器
* @return
*/
@Bean
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager=new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
//rememberMe cookie加密的密钥 默认AES算法
// cookieRememberMeManager.setCipherKey();
return cookieRememberMeManager;
}
/**
* cookie对象
* @return
*/
@Bean
public Cookie rememberMeCookie() {
SimpleCookie simpleCookie=new SimpleCookie("rememberMe");
//记住我cookie生效时间,单位秒
simpleCookie.setMaxAge(3600);
return simpleCookie;
}
/**
* Shiro生命周期处理器
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
/**
* 自动创建代理
* @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
}
UserRealm:
package com.example.shiro.config;
import com.example.shiro.entity.User;
import com.example.shiro.entity.UserRole;
import com.example.shiro.service.*;
import com.example.shiro.utils.State;
import org.apache.log4j.Logger;
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 javax.annotation.Resource;
import java.util.HashSet;
import java.util.Set;
public class UserRealm extends AuthorizingRealm {
@Resource
private UserService userService;
@Resource
private RoleService roleService;
@Resource
private UserRoleService userRoleService;
@Resource
private RolePermissionService rolePermissionService;
@Resource
private PermissionService permissionService;
private Logger logger=Logger.getLogger(UserRealm.class);
/**
* 提供用户信息,返回权限信息
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
logger.info("---------------------------->授权认证:");
System.out.println("---------------------------->开始授权验证:");
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
String userName=(String) principals.getPrimaryPrincipal();
String userId=userService.findUserIdByUserName(userName);
Set<UserRole> roleIdSet=userRoleService.findRoleIdByUserId(userId);
Set<String> roleSet=new HashSet<>();
Set<String> pemissionIdSet=new HashSet<>();
Set<String> pemissionSet=new HashSet<>();
for(UserRole roleInfo : roleIdSet) {
String roleId=roleInfo.getRoleId();
roleSet.add( roleService.findRoleNameByRoleId(roleId));
//将拥有角色的所有权限放进Set里面,也就是求Set集合的并集
//由于我这边的数据表设计得不太好,所以提取set集合比较麻烦
pemissionIdSet.addAll( rolePermissionService.findPerIdByRoleId(roleId));
}
for(String permissionId : pemissionIdSet) {
String permission= permissionService.findPermissionById(permissionId).getPerName() ;
pemissionSet.add( permission );
}
// 将角色名称组成的Set提供给授权info
authorizationInfo.setRoles( roleSet );
// 将权限名称组成的Set提供给info
authorizationInfo.setStringPermissions(pemissionSet);
System.out.println("角色名称集合:");
for (String name:roleSet
) {
System.out.println(name);
}
System.out.println("权限名称集合:");
for (String name:pemissionSet
) {
System.out.println(name);
}
System.out.println("---------------------------->结束授权验证:");
return authorizationInfo;
}
/**
* 提供帐户信息,返回认证信息
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
logger.info("---------------------------->登陆验证:");
System.out.println("---------------------------->开始登陆验证:");
String userName=(String)authenticationToken.getPrincipal();
User user=userService.findUserByName(userName);
if(user==null) {
//用户不存在就抛出异常
throw new UnknownAccountException();
}
if( State.LOCKED.equals(user.getUserStatu())) {
//用户被锁定就抛异常
throw new LockedAccountException();
}
//密码可以通过SimpleHash加密,然后保存进数据库。
//此处是获取数据库内的账号/用户名、密码、盐值,保存到登陆信息info中
SimpleAuthenticationInfo authenticationInfo=new SimpleAuthenticationInfo(user.getUserName(),
user.getUserPassword(),
ByteSource.Util.bytes(user.getUserSalt()) ,
getName()); //realm name
System.out.println("用户名:"+user.getUserName());
System.out.println("---------------------------->结束登陆验证:");
return authenticationInfo;
}
}
--------------------------分割线
以下部分为后续添加内容,标题为 springboot如何跳过shiro的登录要求进行测试
从上文得知,shiro的登录和授权逻辑主要是写在realm里,那测试的话只需要在测试代码中写一个简单的登录逻辑的realm,跳过数据库查询便可以完成登录状态。
具体代码如下:
ShiroRealm:
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken token2 = (UsernamePasswordToken)token;
SimpleAuthenticationInfo info =
new SimpleAuthenticationInfo(token2.getUsername(), token2.getPassword(), this.getName());
return info;
}
}
Login方法:
private void login(String username, String password) {
final UsernamePasswordToken token = new UsernamePasswordToken(username, password);
final Subject subject = SecurityUtils.getSubject();
subject.login(token);
}
装配过程:
@BeforeEach
public void mock() {
DefaultSecurityManager securityManager = new DefaultSecurityManager();
securityManager.setRealm(new ShiroRealm());
SecurityUtils.setSecurityManager(securityManager);
login("admin", "password");
}
然后将此realm装配到测试的DefaultSecurityManager中,再将DefaultSecurityManager装配到SecurityUtils中即可调用login方法(自己写的)模拟登录状态。
再说一点,写完realm后,理论来讲可以将它装配到spring容器中,再在后面使用@Autowired获取,但是我这里不知道为什么不行,就直接new来使用了。