首页 > 软件开发 > Java > Shiro框架的基本使用及问题解决

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的键才得以解决(以上代码提出了)。最后,本文是我学习过程的记录,可能有很多地方存在漏洞,之后发现会立即修正,望指正。

猜你喜欢

Java常用API

Java常用API

Java工具类整理

Java工具类整理

脚本分享-打印Spring Mvc容器中的所有接口

脚本分享-打印Spring Mvc容器中的所有接口

简单实现MyBatis

简单实现MyBatis

关于 CSharp 中调用非托管代码的方法

关于 CSharp 中调用非托管代码的方法

0 条评论

img 登陆后才能评论哦~