Shiro框架的基本使用及问题解决
一. 关于我使用Shiro框架的感受
Shiro框架主要用于搭建系统的安全模块,基本上由认证和授权两个逻辑组成,主要的业务实现由一系列过滤器组成,过滤器也是这个框架的精髓,不仅框架内置许多过滤器,根据不同系统的需求自定义的过滤器也十分重要。
我在使用Shiro时也引入了Redis作为缓存,解决了服务器重启等导致session丢失会让在线的用户失去认证。
这里使用了Spring Boot和Shiro-Redis实现登录认证授权。
二. 依赖与部分配置
<!-- shiro框架 begin -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!-- shiro框架 end -->
<!-- 中间件 begin -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 分布式中间件 end -->
spring:
redis:
host: localhost
port: 6379
password: ********
三. 主要的类
1. Shiro配置类
Shiro配置类是整个框架的核心,可以根据需求配置各种跟Shiro相关的组件,各个组件以下代码均有注释解释:
/*
* @author WuZhengHua
* @describe shiro配置类
* @date 14:20 2020/02/10
*/
@Configuration
@Slf4j
public class ShiroConfig {
private static final String CACHE_KEY = "shiro:cache:";
private static final String SESSION_KEY = "shiro:session:";
private static final String SESSION_COOKIE_KEY = "RABBITER_SESSION";
private static final String SESSION_COOKIE_VALUE = "/";
private static final String REMEMBER_COOKIE_KEY = "RABBITER_REMEMBER";
private static final String REMEMBER_COOKIE_VALUE = "/";
private static final int REMEMBER_COOKIE_MAXAGE = 2592000; //三十天:rememberMe的Cookie有效期
@Value("${spring.redis.host}")
private String REDIS_HOST;
@Value("${spring.redis.port}")
private String REDIS_PORT;
@Value("${spring.redis.password}")
private String REDIS_PASSWORD;
/**
* Shiro过滤器配置工厂
*
* @param securityManager 全局安全管理器
* @param shiroService Shiro业务逻辑接口(目前用于配置url权限链)
* @return Shiro过滤器工厂对象
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, ShiroService shiroService) {
log.info("#####Shiro-配置Shiro过滤器工厂ShiroFilterFactoryBean");
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new LinkedHashMap<>(2);
filterMap.put("roles", rolesAuthorizationFilter()); //自定义角色授权过滤器
filterMap.put("auth", authenticationFilter()); //自定义角色认证过滤器
shiroFilter.setFilters(filterMap);
shiroFilter.setFilterChainDefinitionMap(shiroService.loadFilterChainDefinitions());
return shiroFilter;
}
/**
* 创建自定义的角色认证过滤器对象
*
* @return AuthenticationFilter
*/
@Bean
public AuthenticationFilter authenticationFilter() {
return new AuthenticationFilter();
}
/**
* 创建自定义的角色授权过滤器对象
*
* @return CustomRolesAuthorizationFilter
*/
@Bean
public CustomRolesAuthorizationFilter rolesAuthorizationFilter() {
return new CustomRolesAuthorizationFilter();
}
/**
* 配置全局安全管理器
*
* @param realm 角色授权认证逻辑对象
* @param sessionManager session操作相关的管理器
* @param redisCacheManager redis缓存操作相关的管理器
* @return SecurityManager
*/
@Bean("securityManager")
public SecurityManager securityManager(Realm realm, SessionManager sessionManager, RedisCacheManager redisCacheManager, RememberMeManager rememberMeManager) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setSessionManager(sessionManager);
manager.setCacheManager(redisCacheManager);
manager.setRememberMeManager(rememberMeManager);
manager.setRealm(realm);
return manager;
}
/**
* 配置Redis管理器,创建Redis缓存管理器对象RedisCacheManager需要的参数
* Redis集群使用RedisClusterManager,单个Redis使用RedisManager
*
* @return RedisManager
*/
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(REDIS_HOST + ":" + REDIS_PORT);
redisManager.setPassword(REDIS_PASSWORD);
return redisManager;
}
/**
* 配置Redis缓存管理器,需要传入一个Redis管理器,即上面方法创建的RedisManager对象
*
* @param redisManager Redis缓存管理器对象
* @return RedisCacheManager
*/
@Bean
public RedisCacheManager redisCacheManager(RedisManager redisManager) {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager);
redisCacheManager.setExpire(1800);
redisCacheManager.setKeyPrefix(CACHE_KEY);
return redisCacheManager;
}
/**
* Redis-session结合的DAO,session信息都会被缓存到数据库,防止服务器重启等导致session丢失
* 需要传入一个Redis管理器
*
* @param redisManager Redis缓存管理器对象
* @return RedisSessionDAO
*/
@Bean(name = "redisSessionDao")
public RedisSessionDAO redisSessionDAO(RedisManager redisManager) {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setExpire(1800);
redisSessionDAO.setKeyPrefix(SESSION_KEY);
redisSessionDAO.setRedisManager(redisManager);
return redisSessionDAO;
}
/**
* 配置session管理器
*
* @param redisSessionDAO 需要传入SessionDAO对象,因为加入了Redis,所以传入的是上面配置的RedisSessionDAO对象
* @param simpleCookie 自定义的cookie内容对象
* @return DefaultWebSessionManager
*/
@Bean(name = "sessionManager")
public DefaultWebSessionManager sessionManager(RedisSessionDAO redisSessionDAO, SimpleCookie simpleCookie) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO);
sessionManager.setSessionIdCookieEnabled(true);
sessionManager.setSessionIdCookie(simpleCookie);
return sessionManager;
}
/**
* 默认创建的cookie键为JSESSIONID,与sevlet创建的冲突了,所以需要重命名,即定义的参数NAME
*
* @return SimpleCookie
*/
@Bean
public SimpleCookie simpleCookie() {
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName(SESSION_COOKIE_KEY);
simpleCookie.setValue(SESSION_COOKIE_VALUE);
return simpleCookie;
}
/**
* 记住我的Cookie管理器,传入一个Cookie对象可以自定义Cookie的键名
*
* @param rememberMeCookie Cookie对象
* @return CookieRememberMeManager
*/
@Bean
public CookieRememberMeManager rememberMeManager(SimpleCookie rememberMeCookie) {
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
rememberMeManager.setCookie(rememberMeCookie);
//根据需求设置加解密密钥,不添加此配置,每次重启服务器密钥将会是随机生成的,爆出CryptoException异常
rememberMeManager.setCipherKey(Base64.decode("6CmI3F6j2Y+Y1aSn5BOlDA=="));
return rememberMeManager;
}
/**
* 用于保存记住我的Cookie自定义
*
* @return SimpleCookie
*/
@Bean
public SimpleCookie rememberMeCookie() {
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName(REMEMBER_COOKIE_KEY);
simpleCookie.setValue(REMEMBER_COOKIE_VALUE);
simpleCookie.setMaxAge(REMEMBER_COOKIE_MAXAGE);
return simpleCookie;
}
/**
* 创建用户认证授权逻辑对象Realm
*
* @param redisCacheManager Redis缓存管理器
* @return Realm
*/
@Bean
public Realm realm(RedisCacheManager redisCacheManager) {
MyShiroRealm realm = new MyShiroRealm();
realm.setCacheManager(redisCacheManager);
realm.setAuthenticationCachingEnabled(false);
realm.setAuthorizationCachingEnabled(false);
return realm;
}
}
2. 自定义认证过滤器
/*
* @author WuZhengHua
* @describe 自定义登录认证过滤器,该过滤器每次都会进行调用
* @date 12:25 2019/10/5
*/
@Slf4j
public class AuthenticationFilter extends FormAuthenticationFilter {
/**
* 这个过滤器默认跳转到定制的登录页,前后端分离项目中改为返回一个未登录的json信息
*
* @param request 请求
* @param response 响应
* @return 是否放行
* @throws Exception 异常
*/
@Override
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
Subject subject = SecurityUtils.getSubject();
Object kickout = subject.getSession().getAttribute("KICKOUT");
Object user = subject.getPrincipal();
if (user == null || kickout != null) {
log.info("#####MyAuthenticationFilter-用户未认证");
subject.logout();
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
httpServletResponse.getWriter().write(JSONObject.toJSONString(BaseService.setAuthError("未认证"), SerializerFeature.WriteMapNullValue));
} else {
return true;
}
return false;
}
}
3. 自定义授权过滤器
/*
* @author WuZhengHua
* @describe 自定义授权过滤器
* @date 2020/02/08
*/
@Slf4j
public class CustomRolesAuthorizationFilter extends RolesAuthorizationFilter {
@Override
public boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object mappedValue) {
Subject subject = getSubject(req, resp);
String[] rolesArray = (String[]) mappedValue;
//如果没有角色限制,直接放行
if (rolesArray == null || rolesArray.length == 0) {
return true;
}
for (String aRolesArray : rolesArray) {
//若当前用户是rolesArray中的任何一个,则有权限访问
if (subject.hasRole(aRolesArray)) {
return true;
}
}
return false;
}
@Override
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
HttpServletRequest servletRequest = (HttpServletRequest) request;
HttpServletResponse servletResponse = (HttpServletResponse) response;
//处理跨域问题,跨域的请求首先会发一个options类型的请求
if (servletRequest.getMethod().equals(HttpMethod.OPTIONS.name())) {
return true;
}
boolean isAccess = isAccessAllowed(request, response, mappedValue);
if (isAccess) {
return true;
}
servletResponse.setCharacterEncoding("UTF-8");
Subject subject = getSubject(request, response);
PrintWriter printWriter = servletResponse.getWriter();
servletResponse.setContentType("application/json;charset=UTF-8");
servletResponse.setHeader("Access-Control-Allow-Origin", servletRequest.getHeader("Origin"));
servletResponse.setHeader("Access-Control-Allow-Credentials", "true");
servletResponse.setHeader("Vary", "Origin");
String respStr;
if (subject.getPrincipal() == null) {
log.info("#####CustomRolesAuthorizationFilter-用户未认证");
respStr = JSONObject.toJSONString(BaseService.setAuthError("未认证"));
} else {
log.info("#####CustomRolesAuthorizationFilter-用户未授权");
respStr = JSONObject.toJSONString(BaseService.setAuthError("未授权"));
}
printWriter.write(respStr);
printWriter.flush();
servletResponse.setHeader("content-Length", respStr.getBytes().length + "");
return false;
}
}
4. 业务逻辑接口与实现类
/*
* @author WuZhengHua
* @describe Shiro业务逻辑类,所有需要拦截的地址和过滤器都会在这里被配置
* @date 2020/02/10
*/
@Service("shiroService")
@Slf4j
public class ShiroServiceImpl implements ShiroService {
/**
* 初始化url访问权限链
*/
@Override
public Map<String, String> loadFilterChainDefinitions() {
log.info("#####Shiro-开始初始化URL访问权限链");
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/api/user/doLogin.action", "anon");
filterChainDefinitionMap.put("/api/user/doRegister.action", "anon");
filterChainDefinitionMap.put("/api/user/getUser.action", "auth");
filterChainDefinitionMap.put("/api/blogComment/publishComment.action", "auth"); //必须认证才能发表评论
filterChainDefinitionMap.put("/api/blogComment/publishResponse.action", "auth"); //必须认证才能发表评论
filterChainDefinitionMap.put("/api/user/blogger/**", "auth,roles[ROLE_BLOGGER]");
filterChainDefinitionMap.put("/api/blog/blogger/**", "auth,roles[ROLE_BLOGGER]");
filterChainDefinitionMap.put("/api/blogType/blogger/**", "auth,roles[ROLE_BLOGGER]");
filterChainDefinitionMap.put("/api/blogComment/blogger/**", "auth,roles[ROLE_BLOGGER]");
return filterChainDefinitionMap;
}
}
5.跨域过滤器
如果是前后端分离,采用异步请求数据的时候,就要配置跨域过滤器
/*
* @author WuZhengHua
* @describe shiro跨域请求过滤器
* @date 2019/10/5 12:37
*/
@WebFilter(urlPatterns = "/*", filterName = "RestFilter")
public class RestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = null;
if (request instanceof HttpServletRequest) {
req = (HttpServletRequest) request;
}
HttpServletResponse res = null;
if (response instanceof HttpServletResponse) {
res = (HttpServletResponse) response;
}
if (req != null && res != null) {
//设置允许传递的参数
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, Requestfrom");
//设置允许带上cookie
res.setHeader("Access-Control-Allow-Credentials", "true");
String origin = Optional.ofNullable(req.getHeader("Origin")).orElse(req.getHeader("Referer"));
//设置允许的请求来源
res.setHeader("Access-Control-Allow-Origin", origin);
//设置允许的请求方法
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, PUT, DELETE, OPTIONS");
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
5. 认证授权逻辑对象Realm
/*
* @author WuZhengHua
* @describe 自定义Realm程序,实现认证和授权逻辑
* @date 2019/7/14 12:04
*/
@Slf4j
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private UserDao userDao;
/**
* 授权逻辑
*
* @param principalCollection 权限集合
* @return 授权结果信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("#####执行授权逻辑");
Subject subject = SecurityUtils.getSubject();
//获取认证完的对象,在认证逻辑通过SimpleAuthenticationInfo第一个参数传入
User user = (User) subject.getPrincipal();
String role = user.getRole();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole(role);
return simpleAuthorizationInfo;
}
/**
* 认证逻辑
*
* @param authenticationToken 认证信息
* @return 认证结果信息
* @throws AuthenticationException 认证异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("#####执行认证逻辑");
//1.判断用户名密码
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
String email = usernamePasswordToken.getUsername(); //因为用户使用邮箱登录,username中存放着邮箱
//2.根据usernamePasswordToken中用户登录存进的用户名去数据库查询
User user = userDao.getUserByEmail(email);//密码错误;
if (user == null) { //未找到该用户
return null; //shiro底层会抛出UnknownAccountException
}
user.setDevice(UserServiceImpl.device); //device属性用来记录登录的用户是通过PC端还是移动端
//双用户登录(PC端+移动端)
//处理session
DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
DefaultWebSessionManager sessionManager = (DefaultWebSessionManager) securityManager.getSessionManager();
//获取当前已登录的用户session列表
Collection<Session> sessions = sessionManager.getSessionDAO().getActiveSessions();
User temp;
for (Session session : sessions) {
//清除该用户以前登录时保存的session,强制退出
Object attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (attribute == null) {
continue;
}
temp = (User) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
if (user.getEmail().equals(temp.getEmail()) && user.getDevice().equals(temp.getDevice())) {
log.info("#####用户已登录,前登录者下线");
//sessionManager.getSessionDAO().delete(session);//这个试过,但是被记住的用户信息不会被移除,也是容易踩坑的地方
session.setAttribute("KICKOUT", true); //标记该session被挤出,让之前登录的对象自行判断
sessionManager.getSessionDAO().update(session);
}
}
//第一个参数,传入授权逻辑要获取的对象
//第二个参数,判断数据库查到的密码和用户输入的密码是否一致,不一致会抛出IncorrectCredentialsException异常供捕获
//第三个参数,是realm的名称,这里可以不指定
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
}
6. 用户实体类
/*
* @author WuZhengHua
* @describe 用户实体类
* @date 2019/10/22 12:38
*/
@Setter
@Getter
@ToString
public class User extends BaseEntity implements Serializable{
private String role;
private String username;
private String password;
private String email;
private String device;
}
7. 用户业务逻辑接口的实现类
/*
* @author WuZhengHua
* @describe 用户业务层接口实现类
* @date 2019/10/22 12:41
*/
@Slf4j
@Service
public class UserServiceImpl extends BaseService implements UserService {
@Autowired
private UserDao userDao;
public static String device; //方便传递登录的设备类型(PC端还是移动端)
/**
* 登录
*
* @param user 用户输入的账号密码
* @param response 响应
* @return 报文
*/
@Override
public Map<String, Object> doLogin(User user, Boolean rememberMe, HttpServletResponse response, HttpServletRequest request) {
try {
if (user == null) {
return setParamError();
}
//1.设置登录设备
device = user.getDevice(); //设置一个共享的变量,以便于ShiroRealm能获取并存入
//2.Shiro认证操作
Subject subject = SecurityUtils.getSubject();
String newPsw = md5AndBase64Solt(user.getEmail(), user.getPassword());
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getEmail(), newPsw);
usernamePasswordToken.setRememberMe(rememberMe); //是否记住登录信息
subject.login(usernamePasswordToken);
log.info("#####登录成功,rememberMe = {}", rememberMe);
return setSuccess();
} catch (IncorrectCredentialsException e) { //密码错误
log.error("#####登陆失败,密码错误");
return setSuccess("密码错误");
} catch (AuthenticationException e) { //邮箱错误
log.error("#####登陆失败,用户名错误");
return setSuccess("邮箱不存在");
} catch (Exception e) {
e.printStackTrace();
log.error("#####服务器错误");
return setParamError();
}
}
/**
* 获取用户信息,需要通过自定义的认证过滤器
*
* @param request 请求
* @param response 响应
* @return 报文
*/
@Override
public Map<String, Object> getUser(HttpServletRequest request, HttpServletResponse response) {
try {
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
if (user != null) {
log.info("#####获取到用户信息");
user.setPassword(null);
return setSuccess(JSON.toJSONString(user));
} else {
log.info("#####未找到用户信息");
return setSuccess("未找到用户信息");
}
} catch (Exception e) {
e.printStackTrace();
log.error("#####服务器错误");
return setError();
}
}
}
四. 问题解决
由于第一次使用Redis配合Shiro实现session缓存,为了实现双用户登录(PC端+移动端分开),花了不少时间,要不就是记住用户信息时失败,要不就是无法实现挤人的功能,最后通过在session存入一个KICKOUT的键才得以解决(以上代码提出了)。最后,本文是我学习过程的记录,可能有很多地方存在漏洞,之后发现会立即修正,望指正。
0 条评论
登陆后才能评论哦~