Subject即主体,Subject记录了当前的操作用户信息,外部应用通过Subject向SecurityManager安全管理器进行认证和授权;
SecurityManager即安全管理器,shiro通过SecurityManager来管理内部组件实例,通过来提供安全管理的各种服务;
Cryptography即密码管理,提供了一套加密、解密的组件,比如MD5,散列算法等加密方式;
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.7.1version>
dependency>
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redisartifactId>
<version>2.8.24version>
dependency>
shiro-redis 组件,提供了shiro的登录会话,认证信息、权限信息的第三方redis的缓存支持(集成后便不用手动对登录会话、认证授权信息进行保存,移除、shiro-redis组件会自动处理)
/**
* Redis链接信息配置
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setExpire(1800);
redisManager.setTimeout(0);
return redisManager;
}
/**
* shiro的realm域中认证和授权所支持的缓存管理器
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
redisCacheManager.setKeySerializer(new StringSerializer());
return redisCacheManager;
}
@Bean
public ShiroUserRealm shiroUserRealm() {
// 创建自定义的 userRealm 对象
ShiroUserRealm userRealm = new ShiroUserRealm();
// 设置 userRealm 的 CredentialsMatcher密码校验器
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
// 设置加密算法
matcher.setHashAlgorithmName("md5");
// 设置散列次数
matcher.setHashIterations(6);
userRealm.setCredentialsMatcher(matcher);
userRealm.setCacheManager(cacheManager());
// 开启redis缓存认证
userRealm.setAuthenticationCachingEnabled(true);
// 认证信息缓存key前缀
userRealm.setAuthenticationCacheName("user:authentication");
// 开启redis缓存授权
userRealm.setAuthorizationCachingEnabled(true);
// 权限信息缓存key前缀
userRealm.setAuthorizationCacheName("user:authorization");
return userRealm;
}
/**
* redis session 会话的dao层实现 依赖 shiro-redis
* 提供了一套关于会话的新增,移除等DAO层功能
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
* session 会话管理器 (在SecurityManager中指定会话管理器即可)
*/
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
// 配置会话监听
Collection<SessionListener> listeners = new ArrayList<>();
sessionManager.setSessionListeners(listeners);
return sessionManager;
}
@Bean
public DefaultSecurityManager defaultSecurityManager(){
DefaultSecurityManager defaultSecurityManager = new DefaultWebSecurityManager(userRealm());
defaultSecurityManager.setSessionManager(sessionManager());
return defaultSecurityManager;
}
@Bean
public DefaultSecurityManager defaultSecurityManager(){
DefaultSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
defaultSecurityManager.setRealm(userRealm());
defaultSecurityManager.setSessionManager(sessionManager());
return defaultSecurityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultSecurityManager defaultSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultSecurityManager);
/*
* 设置Shiro 内置过滤器
* anon: 无需认证(登陆)可以访问
* authc: 必须认证才可以访问
* user: 如果使用 rememberMe 的功能可以直接访问
* perms: 该资源必须得到资源权限才可以访问
* role: 该资源必须得到角色权限才可以访问
*/
Map<String, String> filterMap = new LinkedHashMap<>();
// 登录接口放行
filterMap.put("/login", "anon");
filterMap.put("/logout", "anon");
filterMap.put("/send/email/*", "anon");
/*
* 查询当前系统所有的菜单权限,全部加入shiro进行权限认证
* 比如:
* /user/add : perms[user:add]
* /log/view : perms[user:view]
*/
List<SysResource> sysResources = sysResourceService.listResource();
for (SysResource sysResource : sysResources) {
// 菜单url:权限
filterMap.put(sysResource.getRequestUrl(), "perms[" + sysResource.getPermission() + "]");
}
// 其余所有的接口都需要经过认证
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
// 未登录
shiroFilterFactoryBean.setLoginUrl("/unLogin");
// 未授权
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
return shiroFilterFactoryBean;
}
/**
* 用户名、密码权限认证域
*
* 在使用shiro-redis缓存时,
* 1.登录成功:
* - 开始保存缓存认证信息
* - key :AuthenticationToken = UserNamePasswordToken.username 也就是用户名
* - value :new SimpleAuthenticationInfo的第一个参数值(用户详细信息)
* 2.退出登录:
* - AuthenticatingRealm.clearCachedAuthenticationInfo()中清除用户认证缓存信息
* - Object key = getAuthenticationCacheKey(principals); // key 取的是 new SimpleAuthenticationInfo的第一个参数
* - 与我们缓存的认证信息key不一致,此时需要重写getAvailablePrincipal()方法来指定我们删除的key是什么
*
*
*
* @author Sky
* @date 2022/4/15
*/
public class ShiroUserRealm extends AuthorizingRealm {
@Resource
private ISysUserService sysUserService;
@Resource
private ISysRoleService sysRoleService;
@Resource
private ISysResourceService sysResourceService;
/**
* 授权
*
* @param principals 凭证
* @return {@link AuthorizationInfo}
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 用户权限信息
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 获取当前用户的信息
SysUser sysUser = (SysUser) SecurityUtils.getSubject().getPrincipal();
// 初始化授权信息
authorizationInfo(authorizationInfo, sysUser.getId(), Constants.SUPER_ADMIN.equals(sysUser.getSuperAdmin()), sysRoleService, sysResourceService);
return authorizationInfo;
}
/**
* 身份验证
*
* @param token 令牌
* @return {@link AuthenticationInfo}
* @throws AuthenticationException 身份验证异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken userDetail = (UsernamePasswordToken) token;
// 根据用户名获取系统用户
SysUser sysUser = sysUserService.getUserByUsername(userDetail.getUsername());
if (sysUser == null) {
// 用户不存在
throw new UnknownAccountException();
} else if (!sysUser.getEnabledFlag()) {
// 账号被禁用
throw new DisabledAccountException();
}
// 用户的密码,为了数据安全,密码不做缓存
String password = sysUser.getPassword();
sysUser.setPassword(null);
return new SimpleAuthenticationInfo(sysUser,
password,
new CustomByteSource(sysUser.getSalt()),
this.getName());
}
/**
* 多个Realm域时,具体执行那个Realm通过此条件判断
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
/**
* 解决退出登录时,当主体为对象,删除key与缓存key不一致问题
* 指定缓存key
*
* @param principals 主要凭证 new SimpleAuthenticationInfo 第一个参数
* @return 用户名 username
*/
@Override
protected Object getAvailablePrincipal(PrincipalCollection principals) {
return ((SysUser) principals.getPrimaryPrincipal()).getUsername();
}
/**
* 授权信息
* 超级管理员拥有全部角色全部资源权限
*
* @param authorizationInfo 授权信息
* @param userId 用户id
* @param superAdmin 超级管理员
*/
protected void authorizationInfo(SimpleAuthorizationInfo authorizationInfo, Long userId, Boolean superAdmin, ISysRoleService sysRoleService, ISysResourceService sysResourceService) {
// 用户角色信息
List<SysRole> roles = superAdmin ? sysRoleService.listRole() : sysRoleService.listRoleByUserId(userId);
roles.forEach(e -> authorizationInfo.addRole(e.getRoleCode()));
// 用户资源信息
List<SysResource> sysResources = superAdmin ? sysResourceService.listResource() : sysResourceService.listResourceByUserId(userId);
sysResources.forEach(e -> authorizationInfo.addStringPermission(e.getPermission()));
}
}
认证时几种错误状态:
UnknownAccountException
:用户名错误IncorrectCredentialsException
:密码错误DisabledAccountException
:账号被禁用LockedAccountException
:账号被锁定ExcessiveAttemptsException
:登录失败次数过多ExpiredCredentialsException
:凭证过期 public class ShiroUtils {
/**
* 加密方式
*/
public static final String ENCRYPTION_TYPE = "md5";
/**
* 加密次数
*/
public static final Integer ENCRYPTION_NUM = 6;
private ShiroUtils() {
}
/**
* 生成加密后的密码
*
* @param password 密码
* @param salt 盐
* @return md5 散列6次的加密密码
*/
public static String generate(String password, String salt) {
CustomByteSource byteSalt = new CustomByteSource(salt);
return new SimpleHash(ENCRYPTION_TYPE, password, byteSalt, ENCRYPTION_NUM).toString();
}
/**
* 获得当前系统用户
*
* @return {@link SysUser}
*/
public static SysUser getCurrentSysUser() {
return (SysUser) SecurityUtils.getSubject().getPrincipal();
}
/**
* 获取当前主体
*
* @return Subject
*/
public static Subject getSubject() {
return SecurityUtils.getSubject();
}
/**
* 是否登录
*
* @return true登录false未登录
*/
public static boolean isLogin() {
Subject subject = getSubject();
if (subject != null) {
// 登录状态下
return subject.isAuthenticated();
}
return Boolean.FALSE;
}
/**
* 注销
*/
public static void logout() {
getSubject().logout();
}
}
异常信息:java.io.NotSerializableException:org.apache.shiro.util.SimpleByteSource
public class CustomByteSource implements ByteSource, Serializable {
private static final long serialVersionUID = 1L;
private byte[] bytes;
private String cachedHex;
private String cachedBase64;
public CustomByteSource() {
}
public CustomByteSource(byte[] bytes) {
this.bytes = bytes;
}
public CustomByteSource(char[] chars) {
this.bytes = CodecSupport.toBytes(chars);
}
public CustomByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public CustomByteSource(ByteSource source) {
this.bytes = source.getBytes();
}
public CustomByteSource(File file) {
this.bytes = (new CustomByteSource.BytesHelper()).getBytes(file);
}
public CustomByteSource(InputStream stream) {
this.bytes = (new CustomByteSource.BytesHelper()).getBytes(stream);
}
public static boolean isCompatible(Object o) {
return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
}
public void setBytes(byte[] bytes) {
this.bytes = bytes;
}
@Override
public byte[] getBytes() {
return this.bytes;
}
@Override
public String toHex() {
if (this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}
return this.cachedHex;
}
@Override
public String toBase64() {
if (this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
@Override
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
@Override
public String toString() {
return this.toBase64();
}
@Override
public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof ByteSource) {
ByteSource bs = (ByteSource) o;
return Arrays.equals(this.getBytes(), bs.getBytes());
} else {
return false;
}
}
private static final class BytesHelper extends CodecSupport {
private BytesHelper() {
}
public byte[] getBytes(File file) {
return this.toBytes(file);
}
public byte[] getBytes(InputStream stream) {
return this.toBytes(stream);
}
}
}