37 | (加深印象)SpringBoot整合shiro和redis实现授权、认证和会话管理

    科技2022-08-17  109

    前言

    之前我写过一篇关于shiro的,但时间长了,忘了且感觉写得不是很完整,再写一篇加深印象,这里建议你先看完之前那篇文章,因为这里将不重点介绍shiro的基础知识

    1. spring和shiro的整合依赖

    <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.3.2</version> </dependency>

    2. 登录方法

    认证:身份认证/登录,验证用户是不是拥有相应的身份。基于shiro的认证,shiro需要采集到用户登录数据使用subject的login方法进入realm完成认证工作。

    /** * 1.传统登录 * 前端发送登录请求 => 接口部分获取用户名密码 => 程序员在接口部分手动控制 * 2.shiro登录 * 前端发送登录请求 => 接口部分获取用户名密码 => 通过subject.login => realm域的认证方法 * */ //用户登录 @RequestMapping(value="/login") public String login(String username,String password) { //构造登录令牌 try { /** * 密码加密: * shiro提供的md5加密 * Md5Hash: * 参数一:加密的内容 * 111111 --- abcd * 参数二:盐(加密的混淆字符串)(用户登录的用户名) * 111111+混淆字符串 * 参数三:加密次数 * */ password = new Md5Hash(password,username,3).toString(); UsernamePasswordToken upToken = new UsernamePasswordToken(username,password); //1.获取subject Subject subject = SecurityUtils.getSubject(); //获取session String sid = (String) subject.getSession().getId(); //2.调用subject进行登录 subject.login(upToken); return "登录成功"; }catch (Exception e) { return "用户名或密码错误"; } }

    3. 自定义realm

    Realm域:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源

    public class CustomRealm extends AuthorizingRealm { public void setName(String name) { super.setName("customRealm"); } @Autowired private UserService userService; /** * 授权方法 * 操作的时候,判断用户是否具有响应的权限 * 先认证 -- 安全数据 * 再授权 -- 根据安全数据获取用户具有的所有操作权限 * * */ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //1.获取已认证的用户数据 User user = (User) principalCollection.getPrimaryPrincipal();//得到唯一的安全数据 //2.根据用户数据获取用户的权限信息(所有角色,所有权限) SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Set<String> roles = new HashSet<>();//所有角色 Set<String> perms = new HashSet<>();//所有权限 for (Role role : user.getRoles()) { roles.add(role.getName()); for (Permission perm : role.getPermissions()) { perms.add(perm.getCode()); } } info.setStringPermissions(perms); info.setRoles(roles); return info; } /** * 认证方法 * 参数:传递的用户名密码 */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //1.获取登录的用户名密码(token) UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken; String username = upToken.getUsername(); String password = new String( upToken.getPassword()); //2.根据用户名查询数据库 User user = userService.findByName(username); //3.判断用户是否存在或者密码是否一致 if(user != null && user.getPassword().equals(password)) { //4.如果一致返回安全数据 //构造方法:安全数据,密码,realm域名 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),this.getName()); return info; } //5.不一致,返回null(抛出异常) return null; } public static void main(String[] args) { System.out.println(new Md5Hash("123456","wangwu",3).toString()); } }

    3. Shiro的配置

    SecurityManager 是 Shiro 架构的心脏,用于协调内部的多个组件完成全部认证授权的过程。例如通过调用realm完成认证与登录。使用基于springboot的配置方式完成SecurityManager,Realm的装配

    @Configuration public class ShiroConfiguration { //1.创建realm @Bean public CustomRealm getRealm() { return new CustomRealm(); } //2.创建安全管理器 @Bean public SecurityManager getSecurityManager(CustomRealm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm); //将自定义的会话管理器注册到安全管理器中 securityManager.setSessionManager(sessionManager()); //将自定义的redis缓存管理器注册到安全管理器中 securityManager.setCacheManager(cacheManager()); return securityManager; } //3.配置shiro的过滤器工厂 /** * 再web程序中,shiro进行权限控制全部是通过一组过滤器集合进行控制 * */ @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { //1.创建过滤器工厂 ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean(); //2.设置安全管理器 filterFactory.setSecurityManager(securityManager); //3.通用配置(跳转登录页面,为授权跳转的页面) filterFactory.setLoginUrl("/autherror?code=1");//跳转url地址 filterFactory.setUnauthorizedUrl("/autherror?code=2");//未授权的url //4.设置过滤器集合 /** * 设置所有的过滤器:有顺序map * key = 拦截的url地址 * value = 过滤器类型 * */ Map<String,String> filterMap = new LinkedHashMap<>(); //filterMap.put("/user/home","anon");//当前请求地址可以匿名访问 //具有某中权限才能访问 //使用过滤器的形式配置请求地址的依赖权限 //filterMap.put("/user/home","perms[user-home]"); //不具备指定的权限,跳转到setUnauthorizedUrl地址 //使用过滤器的形式配置请求地址的依赖角色 //filterMap.put("/user/home","roles[系统管理员]"); filterMap.put("/user/**","authc");//当前请求地址必须认证之后可以访问 filterFactory.setFilterChainDefinitionMap(filterMap); return filterFactory; } //开启对shior注解的支持 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; }

    4. shiro中的过滤器

    注意:anon, authc, authcBasic, user 是第一组认证过滤器,perms, port, rest, roles, ssl 是第二组授权过滤器,要通过授权过滤器,就先要完成登陆认证操作(即先要完成认证才能前去寻找授权) 才能走第二组授权器(例如访问需要 roles 权限的 url,如果还没有登陆的话,会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url )

    5. 授权

    授权:即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情shiro支持基于过滤器的授权方式也支持注解的授权方式

    5. 1 基于配置的授权

    在shiro中可以使用过滤器的方式配置目标地址的请求权限

    //配置请求连接过滤器配置 //匿名访问(所有人员可以使用) filterMap.put("/user/home", "anon"); //具有指定权限访问 filterMap.put("/user/find", "perms[user-find]"); //认证之后访问(登录之后可以访问) filterMap.put("/user/**", "authc"); //具有指定角色可以访问 filterMap.put("/user/**", "roles[系统管理员]");

    基于配置的方式进行授权,一旦操作用户不具备操作权限,目标地址不会被执行。会跳转到指定的url连接地址。所以需要在连接地址中更加友好的处理未授权的信息提示

    5. 2 基于注解的授权

    (1)RequiresPermissions 配置到方法上,表明执行此方法必须具有指定的权限

    //查询 @RequiresPermissions(value = "user-find") public String find() { return "查询用户成功"; }

    (2)RequiresRoles 配置到方法上,表明执行此方法必须具有指定的角色

    //查询 @RequiresRoles(value = "系统管理员") public String find() { return "查询用户成功"; }

    基于注解的配置方式进行授权,一旦操作用户不具备操作权限,目标方法不会被执行,而且会抛出AuthorizationException 异常。所以需要做好统一异常处理完成未授权处理

    (3)统一异常处理

    /** * 自定义的公共异常处理器 * 1.声明异常处理器 * 2.对异常统一处理 */ @ControllerAdvice public class BaseExceptionHandler { @ExceptionHandler(value = AuthorizationException.class) @ResponseBody public String error(HttpServletRequest request, HttpServletResponse response,AuthorizationException e) { return "未授权"; } }

    6. Shiro中的会话管理的含义与场景分析

    在shiro里所有的用户的会话信息都会由Shiro来进行控制,shiro提供的会话可以用于JavaSE/JavaEE环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。通过Shiro的会话管理器(SessionManager)进行统一的会话管理

    6.1 什么是shiro的会话管理

    SessionManager(会话管理器):管理所有Subject的session包括创建、维护、删除、失效、验证等工作。SessionManager是顶层组件,由SecurityManager管理 shiro提供了三个默认实现:

    DefaultSessionManager:用于JavaSE环境ServletContainerSessionManager:用于Web环境,直接使用servlet容器的会话。(shiro默认使用这个会话管理器)DefaultWebSessionManager:用于web环境,自己维护会话(自己维护着会话,直接废弃了Servlet容器的会话管理)。

    在web程序中,通过shiro的Subject.login()方法登录成功后,用户的认证信息实际上是保存在HttpSession中

    6.2 应用场景分析

    在分布式系统或者微服务架构下,都是通过统一的认证中心进行用户认证。如果使用默认会话管理,用户信息只会保存到一台服务器上。那么其他服务就需要进行会话的同步。 会话管理器可以指定sessionId的生成以及获取方式。通过sessionDao完成模拟session存入,取出等操作

    7. Shiro结合redis的统一会话管理

    7.1 步骤分析

    7.2 构建环境

    使用开源组件Shiro-Redis可以方便的构建shiro与redis的整合工程。

    <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>3.0.0</version> </dependency>

    在springboot配置文件中添加redis配置

    redis: host: 127.0.0.1 port: 6379

    注意:自己事先要开启redis服务器和连接redis客户端

    7.3 自定义shiro会话管理器

    为什么呢?

    因为为了不使用默认的会话管理器ServletContainerSessionManager,这里需要重写DefaultWebSessionManager方法

    /** * 自定义的sessionManager */ public class CustomSessionManager extends DefaultWebSessionManager { /** * 头信息中具有sessionid * 请求头:Authorization: sessionid * * 指定sessionId的获取方式 */ protected Serializable getSessionId(ServletRequest request, ServletResponse response) { //获取请求头Authorization中的数据 String id = WebUtils.toHttp(request).getHeader("Authorization"); if(StringUtils.isEmpty(id)) { //如果没有携带,生成新的sessionId return super.getSessionId(request,response); }else{ //返回sessionId; request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header"); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return id; } } }

    7.4 配置Shiro基于redis的会话管理

    修改shiro的配置

    //2.创建安全管理器 //统一交给SecurityManager管理 @Bean public SecurityManager getSecurityManager(CustomRealm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm); //将自定义的会话管理器注册到安全管理器中 securityManager.setSessionManager(sessionManager()); //将自定义的redis缓存管理器注册到安全管理器中 securityManager.setCacheManager(cacheManager()); return securityManager; } @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; /** * 1.redis的控制器,操作redis * 配置shiro的RedisManager,通过shiro-redis包提供的RedisManager统一对redis操作 */ public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(host); redisManager.setPort(port); return redisManager; } /** * 2.sessionDao * 配置SessionDao,使用shiro-redis实现的基于redis的sessionDao */ public RedisSessionDAO redisSessionDAO() { RedisSessionDAO sessionDAO = new RedisSessionDAO(); sessionDAO.setRedisManager(redisManager()); return sessionDAO; } /** * 3.会话管理器 * 配置会话管理器,指定sessionDao的依赖关系 */ public DefaultWebSessionManager sessionManager() { CustomSessionManager sessionManager = new CustomSessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); return sessionManager; } /** * 4.缓存管理器 * Shiro内部有自己的本地缓存机制,为了更加统一方便管理,全部替换redis实现 */ public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); return redisCacheManager; }

    留言

    也许不久,有的代码已经过期,但只要明白具体实现流程是怎样即可

    Processed: 0.020, SQL: 10