spring安全框架之Shiro

news2024/11/18 5:38:00

Shiro

一、现存问题

1.1 现存问题

认证(登录):认证操作流程都差不多,但是每次都需要手动的基于业务代码去实现,很麻烦!

授权:如果权限控制粒度比较粗,可以自身去实现,但是如果控制粒度比较细,操作麻烦!

分布式会话管理:单体项目时,需要依赖Web容器的Session实现会话,搭建了集群或者是分布式项目,手动去基于Redis或者其他拥有公共存储能力的中间件实现分布式会话管理。

单点登录:在一处服务认证,所有其他服务都信任。(了解)

1.2 Shiro框架介绍

Shiro是基于Java语言编写的,Shiro最核心的功能就是认证和授权。

Shiro官方:http://shiro.apache.org

Shiro的核心架构图

image.png

二、Shiro的基本使用

2.1 SimpleAccountRealm

认证流程:

image.png

授权流程:

image.png

具体操作代码:

@Test
public void authen() {
    //认证的发起者(subject),   SecurityManager,   Realm
    //1. 准备Realm(基于内存存储用户信息)
    SimpleAccountRealm realm = new SimpleAccountRealm();
    realm.addAccount("admin", "admin", "超级管理员", "商家");

    //2. 准备SecurityManager
    DefaultSecurityManager securityManager = new DefaultSecurityManager();

    //3. SecurityManager和Realm建立连接
    securityManager.setRealm(realm);

    //4. subject和SecurityManager建立联系
    SecurityUtils.setSecurityManager(securityManager);

    //5. 声明subject
    Subject subject = SecurityUtils.getSubject();

    //6. 发起认证
    subject.login(new UsernamePasswordToken("admin", "admin"));
    // 如果认证时,用户名错误,抛出:org.apache.shiro.authc.UnknownAccountException异常
    // 如果认证时,密码错误,抛出:org.apache.shiro.authc.IncorrectCredentialsException:

    //7. 判断是否认证成功
    System.out.println(subject.isAuthenticated());

    //8. 退出登录后再判断
    //        subject.logout();
    //        System.out.println("logout方法执行后,认证的状态:" + subject.isAuthenticated());

    //9. 授权是在认证成功之后的操作!!!
    // SimpleAccountRealm只支持角色的授权
    System.out.println("是否拥有超级管理员角色:" + subject.hasRole("超级管理员"));
    subject.checkRole("商家");
    // check方法校验角色时,如果没有指定角色,会抛出异常:org.apache.shiro.authz.UnauthorizedException: Subject does not have role [角色信息]
}
2.2 IniRealm

基于文件存储用户名,密码,角色等信息

准备一个.ini文件,存储用户信息,并且IniRealm支持权限校验

[users]
username=password,role1,role2
admin=admin,超级管理员,运营
[roles]
role1=perm1,perm2
超级管理员=user:add,user:update,user:delete

具体实现业务的代码:

@Test
public void authen(){
    //1. 构建IniRealm
    IniRealm realm = new IniRealm("classpath:shiro.ini");

    //2. 构建SecurityManager绑定Realm
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    securityManager.setRealm(realm);

    //3. 基于SecurityUtils绑定SecurityManager并声明subject
    SecurityUtils.setSecurityManager(securityManager);
    Subject subject = SecurityUtils.getSubject();

    //4. 认证操作
    subject.login(new UsernamePasswordToken("admin","admin"));

    //5. 角色校验
    // 超级管理员
    System.out.println(subject.hasRole("超级管理员"));
    subject.checkRole("运营");

    //6. 权限校验
    System.out.println(subject.isPermitted("user:update"));
    // 如果没有响应的权限,就抛出异常:UnauthorizedException: Subject does not have permission [user:select]
    subject.checkPermission("user:delete");
}
2.3 JdbcRealm

实现权限校验时,库表设计方案

用户认证、授权时推荐的表结构设计,经典五张表!

image.png

具体实现业务代码:

@Test
public void authen(){
    //1. 构建IniRealm
    JdbcRealm realm = new JdbcRealm();

    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql:///shiro");
    dataSource.setUsername("root");
    dataSource.setPassword("root");
    realm.setDataSource(dataSource);

    realm.setPermissionsLookupEnabled(true);

    //2. 构建SecurityManager绑定Realm
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    securityManager.setRealm(realm);

    //3. 基于SecurityUtils绑定SecurityManager并声明subject
    SecurityUtils.setSecurityManager(securityManager);
    Subject subject = SecurityUtils.getSubject();

    //4. 认证操作
    subject.login(new UsernamePasswordToken("admin","admin"));

    //5. 授权操作(角色)
    System.out.println(subject.hasRole("超级管1理员"));

    //6. 授权操作(权限)
    System.out.println(subject.isPermitted("user:add"));

}

SQL构建代码

DROP TABLE IF EXISTS `roles_permissions`;
CREATE TABLE `roles_permissions` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `permission` varchar(128) NOT NULL,
  `role_name` varchar(128) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of roles_permissions
-- ----------------------------
INSERT INTO `roles_permissions` VALUES ('1', 'user:add', '超级管理员');
INSERT INTO `roles_permissions` VALUES ('2', 'user:update', '超级管理员');
INSERT INTO `roles_permissions` VALUES ('3', 'user:select', '运营');

-- ----------------------------
-- Table structure for `users`
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) NOT NULL,
  `password` varchar(32) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES ('1', 'admin', 'admin');

-- ----------------------------
-- Table structure for `user_roles`
-- ----------------------------
DROP TABLE IF EXISTS `user_roles`;
CREATE TABLE `user_roles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(128) NOT NULL,
  `username` varchar(32) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of user_roles
-- ----------------------------
INSERT INTO `user_roles` VALUES ('1', '超级管理员', 'admin');
INSERT INTO `user_roles` VALUES ('2', '运营', 'admin');
2.4 CustomRealm(自定义Realm)

仿照JdbcRealm实现一个自定义的Realm对象

  • 声明POJO类,继承AuthorizingRealm

    public class CustomRealm extends AuthorizingRealm {
        ……………………
    }
    
  • 重写doGetAuthenticationInfo方法(认证)

    /**
     * 认证方法,只需要完成用户名校验即可,密码校验由Shiro内部完成
     * @param token  用户传入的用户名和密码
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1. 基于Token获取用户名
        String username = (String) token.getPrincipal();
    
        //2. 判断用户名(非空)
        if(StringUtils.isEmpty(username)){
            // 返回null,会默认抛出一个异常,org.apache.shiro.authc.UnknownAccountException
            return null;
        }
    
        //3. 如果用户名不为null,基于用户名查询用户信息
        User user = this.findUserByUsername(username);
    
        //4. 判断user对象是否为null
        if(user == null){
            return null;
        }
    
        //5. 声明AuthenticationInfo对象,并填充用户信息
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),"CustomRealm!!");
    
        //6. 返回info
        return info;
    }
    
    // 模拟数据库操作
    private User findUserByUsername(String username) {
        if("admin".equals(username)){
            User user = new User();
            user.setId(1);
            user.setUsername("admin");
            user.setPassword("admin");
            return user;
        }
        return null;
    }
    
  • 重写doGetAuthenticationInfo方法(密码加密加盐)

    {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("MD5");
        matcher.setHashIterations(1024);
        this.setCredentialsMatcher(matcher);
    }
    

    /**

    • 认证方法,只需要完成用户名校验即可,密码校验由Shiro内部完成

    • @param token 用户传入的用户名和密码

    • @return

    • @throws AuthenticationException
      */
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
      //1. 基于Token获取用户名
      String username = (String) token.getPrincipal();

      //2. 判断用户名(非空)
      if(StringUtils.isEmpty(username)){
      // 返回null,会默认抛出一个异常,org.apache.shiro.authc.UnknownAccountException
      return null;
      }

      //3. 如果用户名不为null,基于用户名查询用户信息
      User user = this.findUserByUsername(username);

      //4. 判断user对象是否为null
      if(user == null){
      return null;
      }

      //5. 声明AuthenticationInfo对象,并填充用户信息
      SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),“CustomRealm!!”);
      // 设置盐!
      info.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt()));
      //6. 返回info
      return info;
      }

    // 模拟数据库操作
    private User findUserByUsername(String username) {
    if(“admin”.equals(username)){
    User user = new User();
    user.setId(1);
    user.setUsername(“admin”);
    user.setPassword(“1ebc4dcaf1e21b814ece65f27531f1a9”);
    user.setSalt(“weruiothergjkdfnbgjkdfngjkdf”);
    return user;
    }
    return null;
    }

  • 重写doGetAuthorizationInfo方法(授权)

    // 授权方法,授权是在认证之后的操作
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //1. 获取认证用户的信息
        User user = (User) principals.getPrimaryPrincipal();
    
        //2. 基于用户信息获取当前用户拥有的角色。
        Set<String> roleSet = this.findRolesByUser();
    
        //3. 基于用户拥有的角色查询权限信息
        Set<String> permSet = this.findPermsByRoleSet(roleSet);
    
        //4. 声明AuthorizationInfo对象作为返回值,传入角色信息和权限信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(roleSet);
        info.setStringPermissions(permSet);
    
        //5. 返回
        return info;
    }
    
    private Set<String> findPermsByRoleSet(Set<String> roleSet) {
        Set<String> set = new HashSet<>();
        set.add("user:add");
        set.add("user:update");
        return set;
    }
    
    private Set<String> findRolesByUser() {
        Set<String> set = new HashSet<>();
        set.add("超级管理员");
        set.add("运营");
        return set;
    }
    

三、Shiro的Web流程

image.png

四、Shiro整合Web(SpringMVC,SpringBoot)

4.1 SSM方式
  • 准备SSM的配置(掌握跳过)

  • 准备经典五张表,完成测试

  • 准备Shiro的配置

    • 准备核心过滤器

      <!--    配置Shiro整合web的过滤器-->
      <filter>
          <!--        默认情况下,请求到达这个过滤器,会去Spring容器中名字为filter-name的实例去处理-->
          <filter-name>shiroFilter</filter-name>
          <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
      </filter>
      <filter-mapping>
          <filter-name>shiroFilter</filter-name>
          <url-pattern>/*</url-pattern>
      </filter-mapping>
      
    • 准备shiroFilter实例

      <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
      	.....
      </bean>
      
    • 注入SecurityManager,登录页面路径,过滤器链

      <!--    构建realm-->
      <bean id="realm" class="com.mashibing.realm.ShiroRealm" />
      
      <!--    构建securityManager-->
      <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
          <property name="realm" ref="realm"/>
      </bean>
      
      <!--    构建ShiroFilter实例-->
      <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
          <property name="securityManager" ref="securityManager"/>
          <property name="loginUrl" value="/login.html" />
          <property name="filterChainDefinitionMap">
              <map>
                  <entry key="/login.html" value="anon" />
                  <entry key="/user/**" value="anon" />
                  <entry key="/**" value="authc" />
              </map>
          </property>
      </bean>
      
    • 将ShiroRealm的模拟数据库操作,修改为与数据库交互

    • 编写登录功能,并测试效果

      @PostMapping("/login")
      public String login(String username,String password){
          // 执行Shiro的认证操作
          //1. 直接基于SecurityUtils获取subject主体,不需要手动的将SecurityManager和SecurityUtils手动整合,Spring已经奥丁
          Subject subject = SecurityUtils.getSubject();
      
          //2. 发起认证
          try {
              subject.login(new UsernamePasswordToken(username,password));
              return "SUCCESS";
          } catch (UnknownAccountException exception){
              return "username fail!!!";
          } catch (IncorrectCredentialsException exception){
              return "password fail!!!";
          } catch (AuthenticationException e) {
              return "donot know...!!!";
          }
      }
      
4.2 SpringBoot方式
  • 搭建SpringBoot工程(准备工作)

  • 配置Shiro整合SpringBoot内容

    @Configuration
    public class ShiroConfig {
    
        @Bean
        public DefaultWebSecurityManager securityManager(ShiroRealm realm){
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(realm);
            return securityManager;
        }
    
        @Bean
        public DefaultShiroFilterChainDefinition shiroFilterChainDefinition(){
            DefaultShiroFilterChainDefinition shiroFilterChainDefinition = new DefaultShiroFilterChainDefinition();
    
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
            filterChainDefinitionMap.put("/login.html","anon");
            filterChainDefinitionMap.put("/user/**","anon");
            filterChainDefinitionMap.put("/**","authc");
    
            shiroFilterChainDefinition.addPathDefinitions(filterChainDefinitionMap);
    
            return shiroFilterChainDefinition;
        }
    }
    

五、Shiro的授权方式

5.1 过滤器链
public enum DefaultFilter {
	// ....
    perms(PermissionsAuthorizationFilter.class),
    roles(RolesAuthorizationFilter.class),
	// ....
}
filterChainDefinitionMap.put("/item/select","roles[超级管理员,运营]");
filterChainDefinitionMap.put("/item/delete","perms[item:delete,item:insert]");

image.png

5.2 自定义过滤器
  • 仿照RolesAuthorizationFilter实现自定义过滤器

    /**
     * 在要求的多个角色中,有一个满足要求,就放行
     * @author zjw
     * @description
     */
    public class RolesOrAuthorizationFilter extends AuthorizationFilter {
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
            // 获取主体subject
            Subject subject = getSubject(request, response);
            // 将传入的角色转成数组操作
            String[] rolesArray = (String[]) mappedValue;
            // 健壮性校验
            if (rolesArray == null || rolesArray.length == 0) {
                return true;
            }
            // 开始校验
            for (String role : rolesArray) {
                if(subject.hasRole(role)){
                    return true;
                }
            }
    
            return false;
        }
    }
    
  • 将自定义过滤器配置给Shiro

    @Configuration
    public class ShiroConfig {
    
        @Bean
        public DefaultWebSecurityManager securityManager(ShiroRealm realm){
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(realm);
            return securityManager;
        }
    
        @Bean
        public DefaultShiroFilterChainDefinition shiroFilterChainDefinition(){
            DefaultShiroFilterChainDefinition shiroFilterChainDefinition = new DefaultShiroFilterChainDefinition();
    
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
            filterChainDefinitionMap.put("/login.html","anon");
            filterChainDefinitionMap.put("/user/**","anon");
            filterChainDefinitionMap.put("/item/select","rolesOr[超级管理员,运营]");
            filterChainDefinitionMap.put("/item/delete","perms[item:delete,item:insert]");
            filterChainDefinitionMap.put("/**","authc");
    
            shiroFilterChainDefinition.addPathDefinitions(filterChainDefinitionMap);
    
            return shiroFilterChainDefinition;
        }
    
        @Value("#{ @environment['shiro.loginUrl'] ?: '/login.jsp' }")
        protected String loginUrl;
    
        @Value("#{ @environment['shiro.successUrl'] ?: '/' }")
        protected String successUrl;
    
        @Value("#{ @environment['shiro.unauthorizedUrl'] ?: null }")
        protected String unauthorizedUrl;
    
    @Bean
    protected ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
        //1. 构建ShiroFilterFactoryBean工厂
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
    
        //2. 设置了大量的路径
        filterFactoryBean.setLoginUrl(loginUrl);
        filterFactoryBean.setSuccessUrl(successUrl);
        filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
    
        //3. 设置安全管理器
        filterFactoryBean.setSecurityManager(securityManager);
    
        //4. 设置过滤器链
        filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
    
        //5. 设置自定义过滤器 , 这里一定要手动的new出来这个自定义过滤器,如果使用Spring管理自定义过滤器,会造成无法获取到Subject
        filterFactoryBean.getFilters().put("rolesOr",new RolesOrAuthorizationFilter());
    
        //6. 返回工厂
        return filterFactoryBean;
    }
    

    }

  • 测试功能

    // 修改当前用户的角色授权过滤器
    filterChainDefinitionMap.put("/item/select","rolesOr[超级管理员,运营]");
    
5.3 注解
  • 注解进行授权时,是基于对Controller类进行代理,在前置增强中对请求进行权限校验

  • 因为咱们使用SpringBoot的测试方式,直接在Controller方法上添加注解即可

    @GetMapping("/update")
    @RequiresRoles(value = {"超级管理员","运营"})
    public String update(){
        return "item Update!!!";
    }
    
    @GetMapping("/insert")
    @RequiresRoles(value = {"超级管理员","运营"},logical = Logical.OR)
    public String insert(){
        return "item Update!!!";
    }
    
    //    @RequiresPermissions(value = "",logical = Logical.AND)
    
  • 在SpringBoot中注解默认就生效,是因为自动装配中,已经配置好了对注解的支持

    @Configuration
    @ConditionalOnProperty(name = "shiro.annotations.enabled", matchIfMissing = true)
    public class ShiroAnnotationProcessorAutoConfiguration extends AbstractShiroAnnotationProcessorConfiguration {
    
        @Bean
        @DependsOn("lifecycleBeanPostProcessor")
        @ConditionalOnMissingBean
        @Override
        public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
            return super.defaultAdvisorAutoProxyCreator();
        }
    
        @Bean
        @ConditionalOnMissingBean
        @Override
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            return super.authorizationAttributeSourceAdvisor(securityManager);
        }
    }
    
  • 注解的形式无法将错误页面的信息定位到401.html,因为配置的这种路径,只针对过滤器链有效,注解无效。为了实现友好提示的效果,可以配置异常处理器,@RestControllerAdvice,@ControllerAdvice

5.4 标签(前端,不玩,JSP、Freemarker、Thymeleaf)
5.5 记住我
  • 记住我在开启后,可以针对一些安全级别相对更低的页面采用user过滤器拦截,只要登录过,不需要重新登录就可以访问

  • 准备工作:

    • 准备两个接口

      @GetMapping("/rememberMe")
      public String rememberMe(){
          return "rememberMe!!!";
      }
      
      @GetMapping("/authentication")
      public String authentication(){
          return "authentication!!!";
      }
      
    • 配置不同的过滤器

      filterChainDefinitionMap.put("/item/rememberMe","user");
      filterChainDefinitionMap.put("/item/authentication","authc");
      
  • 在页面追加记住我按钮,并且在登录是,添加rememberMe效果

    <form action="/user/login" method="post">
        用户名:<input  name="username" />  <br />
        密码:<input name="password" />  <br />
        记住我:<input type="checkbox" name="rememberMe" value="on" />  <br />
        <button type="submit">登录</button>
    </form>
    
    <!-- ================================== -->
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    token.setRememberMe(rememberMe != null && "on".equals(rememberMe));
    subject.login(token);
    
  • 测试效果

  • 问题1:认证后,后台报错,原因是记住我,需要以浏览器的cookie和后台的user对象绑定,user对象需要序列化。

    public class User implements Serializable {  ……}
    
  • 问题2:认证后,重新打开浏览器,还可以访问角色授权、权限授权的地址。没有在Realm的授权方法中先判断用户是否认证,导致可以直接方案,因为cookie绑定的是认证成功后,返回的第一个参数,而第一个参数和授权方法中参数能获得到的用户信息是一个内容。直接在授权方法中先做认证判断

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //0. 判断是否认证
        Subject subject = SecurityUtils.getSubject();
        if(subject == null){
            return null;
        }
        if (!subject.isAuthenticated()) {
            return null;
        }
    	………………
    }
    
  • 测试效果:需要认证的接口地址,无法在关闭浏览器后重新访问,必须要重新认证。

  • 测试效果:需要记住我的接口地址,可以在浏览器重新打开后正常访问。

六、Shiro的分布式Session的处理

6.1 Shiro的Session管理

Shiro在认证成功后,可以不依赖Web容器的Session,也可以依赖!

在SpringBoot自动装配之后,Shiro默认将HttpSession作为存储用户认证成功信息的位置。

但是SpringBoot也提供了一个基于JVM内存存储用户认证信息的位置。

修改Shiro默认使用的SessionDAO,修改为默认构建好的MemorySessionDAO

// 构建管理SessionDAO的SessionManager
@Bean
public SessionManager sessionManager(SessionDAO sessionDAO) {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    sessionManager.setSessionDAO(sessionDAO);
    return sessionManager;
}

@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm realm,SessionManager sessionManager){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(realm);
	// 将使用MemorySessionDAO的SessionManager注入到SecurityManager
    securityManager.setSessionManager(sessionManager);
    return securityManager;
}
6.2 Shiro解决分布式Session

在服务搭建集群后,或者是服务是分布式架构的,导致单台服务的认证无法让其他服务也得知到信息:

  • 基于Nginx做ip_hash策略,但是也只是针对单台服务搭建集群有效果
  • 基于Shiro提供的SessionDAO解决,让SessionDAO去与公共的Redis进行交互,存储用户信息

image.png

6.3 实现Shiro的分布式Session处理
  • 项目连接Redis

    • 导入依赖

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
      </dependency>
      
    • 编写配置

      spring:
        redis:
          host: 49.233.115.171
          port: 6379
          password: xxxxx
      
  • 声明SessionDAO的实现类,并重写核心方法

    @Component
    public class RedisSessionDAO extends AbstractSessionDAO {
    
        @Resource
        private RedisTemplate redisTemplate;
    
        // 存储到Redis时,sessionId作为key,Session作为Value
        // sessionId就是一个字符串
        // Session可以和sessionId绑定到一起,绑定之后,可以基于Session拿到sessionId
        // 需要给Key设置一个统一的前缀,这样才可以方便通过keys命令查看到所有关联的信息
    
        private final String SHIOR_SESSION = "session:";
    
        @Override
        protected Serializable doCreate(Session session) {
            System.out.println("Redis---doCreate");
            //1. 基于Session生成一个sessionId(唯一标识)
            Serializable sessionId = generateSessionId(session);
    
            //2. 将Session和sessionId绑定到一起(可以基于Session拿到sessionId)
            assignSessionId(session, sessionId);
    
            //3. 将 前缀:sessionId 作为key,session作为value存储
            redisTemplate.opsForValue().set(SHIOR_SESSION + sessionId,session,30, TimeUnit.MINUTES);
    
            //4. 返回sessionId
            return sessionId;
        }
    
     	@Override
        protected Session doReadSession(Serializable sessionId) {
            //1. 基于sessionId获取Session (与Redis交互)
            if (sessionId == null) {
                return null;
            }
            Session session = (Session) redisTemplate.opsForValue().get(SHIOR_SESSION + sessionId);
            if (session != null) {
                redisTemplate.expire(SHIOR_SESSION + sessionId,30,TimeUnit.MINUTES);
            }
            return session;
        }
    
        @Override
        public void update(Session session) throws UnknownSessionException {
            System.out.println("Redis---update");
            //1. 修改Redis中session
            if(session == null){
                return ;
            }
            redisTemplate.opsForValue().set(SHIOR_SESSION + session.getId(),session,30, TimeUnit.MINUTES);
        }
    
        @Override
        public void delete(Session session) {
            // 删除Redis中的Session
            if(session == null){
                return ;
            }
            redisTemplate.delete(SHIOR_SESSION + session.getId());
        }
    
        @Override
        public Collection<Session> getActiveSessions() {
            Set keys = redisTemplate.keys(SHIOR_SESSION + "*");
    
            Set<Session> sessionSet = new HashSet<>();
            // 尝试修改为管道操作,pipeline(Redis的知识)
            for (Object key : keys) {
                Session session = (Session) redisTemplate.opsForValue().get(key);
                sessionSet.add(session);
            }
            return sessionSet;
        }
    }
    
  • 将RedisSessionDAO交给SessionManager

    @Bean
    public SessionManager sessionManager(RedisSessionDAO sessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(sessionDAO);
        return sessionManager;
    }
    
  • 将SessionManager注入到SecurityManager

    @Bean
    public DefaultWebSecurityManager securityManager(ShiroRealm realm,SessionManager sessionManager){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }
    
6.4 RedisSessionDAO问题

将传统的基于Web容器或者ConcurrentHashMap切换为Redis之后,发现每次请求需要访问多次Redis服务,这个访问的频次会出现很长时间的IO等待,对每次请求的性能减低了,并且对Redis的压力也提高了。

  • 基于装饰者模式重新声明SessionManager中提供的retrieveSession方法,让每次请求先去request域中查询session信息,request域中没有,再去Redis中查询

    public class DefaultRedisWebSessionManager extends DefaultWebSessionManager {
    
        @Override
        protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
            // 通过sessionKey获取sessionId
            Serializable sessionId = getSessionId(sessionKey);
    
            // 将sessionKey转为WebSessionKey
            if(sessionKey instanceof WebSessionKey){
                WebSessionKey webSessionKey = (WebSessionKey) sessionKey;
                // 获取到request域
                ServletRequest request = webSessionKey.getServletRequest();
                // 通过request尝试获取session信息
                Session session = (Session) request.getAttribute(sessionId + "");
                if(session != null){
                    System.out.println("从request域中获取session信息");
                    return session;
                }else{
                    session = retrieveSessionFromDataSource(sessionId);
                    if (session == null) {
                        //session ID was provided, meaning one is expected to be found, but we couldn't find one:
                        String msg = "Could not find session with ID [" + sessionId + "]";
                        throw new UnknownSessionException(msg);
                    }
                    System.out.println("Redis---doReadSession");
                    request.setAttribute(sessionId + "",session);
                    return session;
                }
            }
            return null;
        }
    }
    
  • 配置DefaultRedisWebSessionManager到SecurityManager中

    @Bean
    public SessionManager sessionManager(RedisSessionDAO sessionDAO) {
        DefaultRedisWebSessionManager sessionManager = new DefaultRedisWebSessionManager();
        sessionManager.setSessionDAO(sessionDAO);
        return sessionManager;
    }
    

七、Shiro的授权缓存

如果后台接口存在授权操作,那么每次请求都需要去数据库查询对应的角色信息和权限信息,对数据库来说,这样的查询压力太大了。

在Shiro中,发现每次在执行自定义Realm的授权方法查询数据库之前,会有一个执行Cache的操作。

先从Cache中基于一个固定的key去查询角色以及权限的信息。

只需要提供好响应的CacheManager实例,还要实现一个与Redis交互的Cache对象,将Cache对象设置到CacheManager实例中。

将上述设置好的CacheManager设置到SecurityManager对象中

7.1 实现RedisCache
@Component
public class RedisCache<K, V> implements Cache<K, V> {

    @Autowired
    private RedisTemplate redisTemplate;

    private final String CACHE_PREFIX = "cache:";

    /**
     * 获取授权缓存信息
     * @param k
     * @return
     * @throws CacheException
     */
    @Override
    public V get(K k) throws CacheException {
        V v = (V) redisTemplate.opsForValue().get(CACHE_PREFIX + k);
        if(v != null){
            redisTemplate.expire(CACHE_PREFIX + k,15, TimeUnit.MINUTES);
        }
        return v;
    }

    /**
     * 存放缓存信息
     * @param k
     * @param v
     * @return
     * @throws CacheException
     */
    @Override
    public V put(K k, V v) throws CacheException {
        redisTemplate.opsForValue().set(CACHE_PREFIX + k,v,15,TimeUnit.MINUTES);
        return v;
    }

    /**
     * 清空当前缓存
     * @param k
     * @return
     * @throws CacheException
     */
    @Override
    public V remove(K k) throws CacheException {
        V v = (V) redisTemplate.opsForValue().get(CACHE_PREFIX + k);
        if(v != null){
            redisTemplate.delete(CACHE_PREFIX + k);
        }
        return v;
    }

    /**
     * 清空全部的授权缓存
     * @throws CacheException
     */
    @Override
    public void clear() throws CacheException {
        Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
        redisTemplate.delete(keys);
    }

    /**
     * 查看有多个权限缓存信息
     * @return
     */
    @Override
    public int size() {
        Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
        return keys.size();
    }

    /**
     * 获取全部缓存信息的key
     * @return
     */
    @Override
    public Set<K> keys() {
        Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
        return keys;
    }

    /**
     * 获取全部缓存信息的value
     * @return
     */
    @Override
    public Collection<V> values() {
        Set values = new HashSet();
        Set keys = redisTemplate.keys(CACHE_PREFIX + "*");
        for (Object key : keys) {
            Object value = redisTemplate.opsForValue().get(key);
            values.add(value);
        }
        return values;
    }
}
7.2 实现CacheManager并测试

实现CachaManager

@Component
public class RedisCacheManager implements CacheManager {
    @Autowired
    private RedisCache redisCache;

    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return redisCache;
    }
}

将RedisCacheManager配置到SecurityManager

@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm realm, SessionManager sessionManager, RedisCacheManager redisCacheManager){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(realm);
    securityManager.setSessionManager(sessionManager);
    // 设置CacheManager,提供与Redis交互的Cache对象
    securityManager.setCacheManager(redisCacheManager);
    return securityManager;
}

八、Shiro整合CAS框架实现单点登录

8.1 单点登录

单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

一般这种单点登录的实现方案,分为两种

中心化方式:image.png

去中心化方式:image.png

去中心化方式:不存在单点故障,并且在访问时,可以减少网络IO所占用的时间,并且针对认证服务器没有请求压力。去中心化的方式,采用JWT实现。

中心化方式:存在单点故障,单台服务的访问压力较大,每次请求认证身份都需要访问认证服务器,导致压力相对比较大,效率也比较低。

咱们即将搞定的Shiro+CAS的方式,就是基于中心化实现的。

8.2 CAS介绍&搭建
8.2.1 CAS介绍

CAS是一个开源项目,CAS是应用于企业级别的单点登录的服务,CAS分为CAS Server,CAS Client

CAS Server是需要一个单独部署的Web工程

CAS Client是一个项目中的具体业务服务,并且在需要认证或授权时,找到CAS Server即可

整体CAS的认证和授权流程就是中心化的方式

8.2.2 CAS搭建

在知道CAS是什么内容后,第一步就是将CAS Server单独部署并运行起来

CAS Server的5.x版本更改为使用gradle构建,平时更多的是使用Maven,采用4.x版本、

采用CAS的4.x版本使用……

下载CAS:https://github.com/apereo/cas/archive/refs/tags/v4.1.10.zip

使用IDEA打开CAS Server,并修改一些配置信息,将CAS Server进行打包,扔到Tomcat服务中运行

  • 采用IDEA打开CAS Server,并加载image.png
  • CAS Server默认只支持HTTPS,需要让CAS Server支持HTTP
    • Apereo-10000002.jsonimage.png
    • HTTPSandIMAPS-10000001.jsonimage.png
    • ticketGrantingTicketCookieGenerator.xmlimage.png
    • warnCookieGenerator.xmlimage.png
    • deployerConfigContext.xmlimage.png
  • 将项目进行打包,采用项目中的Maven插件,war的形式打包
    • 打包前,先将CAS Server进行compile,避免启动项目时,出现类路径下的配置文件无法找到
    • 再执行plugins中提供的war:war执行打包
  • 将war包扔到Tomcat的webapps里,并运行即可
  • 访问CAS Server首页,并且完成认证
    • 默认用户名&密码image.png
    • 访问首页测试image.pngimage.png
8.2.3 CAS连接数据库认证

注释掉之前采用配置文件内认证的方式,修改为与数据库交互实现

  • 导入依赖

    <!--    mysql驱动-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.47</version>
    </dependency>
    <!--    druid连接池-->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.10</version>
    </dependency>
    <!--    jdbc的支持-->
    <dependency>
      <groupId>org.jasig.cas</groupId>
      <artifactId>cas-server-support-jdbc</artifactId>
      <version>4.1.10</version>
    </dependency>
    
  • 编写配置

    <!--    数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql:///shiro-web" />
        <property name="username" value="root" />
        <property name="password" value="root" />
    </bean>
    <!--配置primaryAuthenticationHandler,QueryDatabaseAuthenticationHandler-->
    <bean id="primaryAuthenticationHandler" class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
        <property name="dataSource" ref="dataSource" />
        <property name="sql" value="select password from tb_user where username = ?" />
    </bean>
    
  • 需要将webapp项目进行compile,然后再执行war:war

  • 最终经过测试,得知,CAS Server在认证成功后,会给客户端返回一些TGC并写入浏览器的Cookie中,每次客户端携带者正确的TGC来访问时,就会与CAS Server端存储 的TGT进行配对,只要正确,证明认证成功,直接跳转到登录成功页面,否则跳转到登录页面

8.2.4 CAS实现对密码的加密&加盐

在实现CAS与数据库交互时,采用了QueryDatabaseAuthenticationHandler类实现。

同时这个类提供了一个属性passwordEncoder,可以基于passwordEncoder实现对密码进行加密校验。

但是基于咱们的业务,需要对密码进行加密和加盐的操作。

QueryDatabaseAuthenticationHandler无法实现业务需求。

需要参考QueryDatabaseAuthenticationHandler认证处理器去实现可以满足自身业务的认证处理器

需要实现属于自己的认证处理器:

  • 需要编写一个MD5HashQueryDatabaseAuthenticationHandler,去继承AbstractJdbcUsernamePasswordAuthenticationHandler

    /**
     * @author zjw
     * @since 3.0
     */
    public class MD5HashQueryDatabaseAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler {
        // .....
    }
    
  • 声明saltSql,需要注入查询盐的SQL语句,在做密码校验时,需要先将用户输入的密码进行加密和加盐,然后再做比较

    /*
     * Licensed to Apereo under one or more contributor license
     * agreements. See the NOTICE file distributed with this work
     * for additional information regarding copyright ownership.
     * Apereo licenses this file to you under the Apache License,
     * Version 2.0 (the "License"); you may not use this file
     * except in compliance with the License.  You may obtain a
     * copy of the License at the following location:
     *
     *   http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing,
     * software distributed under the License is distributed on an
     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     * KIND, either express or implied.  See the License for the
     * specific language governing permissions and limitations
     * under the License.
     */
    package org.jasig.cas.adaptors.jdbc;
    
    import org.apache.shiro.crypto.hash.Md5Hash;
    import org.jasig.cas.authentication.HandlerResult;
    import org.jasig.cas.authentication.PreventedException;
    import org.jasig.cas.authentication.UsernamePasswordCredential;
    import org.springframework.dao.DataAccessException;
    import org.springframework.dao.IncorrectResultSizeDataAccessException;
    
    import javax.security.auth.login.AccountNotFoundException;
    import javax.security.auth.login.FailedLoginException;
    import javax.validation.constraints.NotNull;
    import java.security.GeneralSecurityException;
    
    /**
     * @author zjw
     * @since 3.0
     */
    public class MD5HashQueryDatabaseAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler {
    
        @NotNull
        private String sql;
    
        @NotNull
        private String saltSql;
    
        private final Integer hashIterations = 1024;
    
        /**
         * {@inheritDoc}
         */
        @Override
        protected final HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential)
                throws GeneralSecurityException, PreventedException {
            // 获取用户输入的用户名
            final String username = credential.getUsername();
            // 获取用户输入的密码
            final String encryptedPassword = this.getPasswordEncoder().encode(credential.getPassword());
            try {
                // 基于用户名查询数据库的密码
                final String dbPassword = getJdbcTemplate().queryForObject(this.sql, String.class, username);
                // 基于用户名查询当前用户的salt
                final String salt = getJdbcTemplate().queryForObject(this.saltSql, String.class, username);
                // 将用户输入的密码进行加密和加盐操作~
                final String password = new Md5Hash(encryptedPassword, salt, hashIterations).toString();
                // 比较密码
                if (!dbPassword.equals(password)) {
                    throw new FailedLoginException("Password does not match value on record.");
                }
            } catch (final IncorrectResultSizeDataAccessException e) {
                if (e.getActualSize() == 0) {
                    throw new AccountNotFoundException(username + " not found with SQL query");
                } else {
                    throw new FailedLoginException("Multiple records found for " + username);
                }
            } catch (final DataAccessException e) {
                throw new PreventedException("SQL exception while executing query for " + username, e);
            }
            return createHandlerResult(credential, this.principalFactory.createPrincipal(username), null);
        }
    
        /**
         * @param sql The sql to set.
         */
        public void setSql(final String sql) {
            this.sql = sql;
        }
    
        /**
         * @param saltSql The sql to set  -  select salt.
         */
        public void setSaltSql(final String saltSql) {
            this.saltSql = saltSql;
        }
    }
    
    

回到webapp项目中,采用MD5HashQueryDatabaseAuthenticationHandler作为认证处理器

<!--配置primaryAuthenticationHandler,QueryDatabaseAuthenticationHandler-->
    <bean id="primaryAuthenticationHandler" class="org.jasig.cas.adaptors.jdbc.MD5HashQueryDatabaseAuthenticationHandler">
        <property name="dataSource" ref="dataSource" />
        <property name="sql" value="select password from tb_user where username = ?" />
        <property name="saltSql" value="select salt from tb_user where username = ?" />
    </bean>

在第一次重新打包并发布时,出现了ClassNotFountException,需要将JDBC项目进行install操作,然后才可以对webapp重新war:war,然后才可以生效,避免出现ClassNotFountException

image.png

8.3 Shiro + pac4j + CAS
8.3.1 认证流程

本质上和ShiroWeb的流程没有变化,只不过内部使用的一些Realm和过滤器交由pac4j提供

image.png

8.3.2 构建项目并设置配置信息
  • 导入依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
    
        <dependency>
            <groupId>io.buji</groupId>
            <artifactId>buji-pac4j</artifactId>
            <version>4.0.0</version>
        </dependency>
    
        <dependency>
            <groupId>org.pac4j</groupId>
            <artifactId>pac4j-cas</artifactId>
            <version>3.0.2</version>
        </dependency>
    </dependencies>
    
  • 配置Realm

    @Component
    public class CasRealm extends Pac4jRealm {
    
    /**
     * 授权操作,需要自己编写,并且也可以基于RedisSessionDAO实现缓存……
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // do something , find DB or Cache
        return null;
    }
    

    }

  • 编写SecurityManager

    @Configuration
    public class ShiroConfig {
    
    
        @Bean
        public SubjectFactory subjectFactory(){
            return new Pac4jSubjectFactory();
        }
    
        @Bean
        public SecurityManager securityManager(CasRealm casRealm,SubjectFactory subjectFactory){
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(casRealm);
            securityManager.setSubjectFactory(subjectFactory);
            return securityManager;
        }
    
    }
    
  • 配置过滤器

    @Configuration
    public class ShiroConfig {
    
    /**
     * 配置核心过滤器
     * @return
     */
    @Bean
    public FilterRegistrationBean filterRegistrationBean(){
        FilterRegistrationBean filterRegistration =new FilterRegistrationBean();
        filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
        filterRegistration.addUrlPatterns("/*");
        return filterRegistration;
    }
    

    }

  • 配置ShiroFiler(ShiroConfig)

    /**
     * shiroFilter核心配置
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        putFilterChain(factoryBean);
        return factoryBean;
    }
    
    private void putFilterChain(ShiroFilterFactoryBean factoryBean) {
        Map<String,String> filterChain = new LinkedHashMap<>();
        // 后面在声明好pac4j提供的过滤器后,需要重新设置!
        filterChain.put("/**","anon");
        factoryBean.setFilterChainDefinitionMap(filterChain);
    }
    
  • 设置pac4j对CAS的设置

    @Configuration
    public class Pac4jConfig {
    
        @Value("${cas.server.url:http://localhost:8080/cas}")
        private String casServerUrl;
    
        @Value("${cas.project.url:http://localhost:81}")
        private String casProjectUrl;
    
        @Value("${cas.clientName:test}")
        private String clientName;
    
        /**
         * 核心Config
         * @param casClient
         * @return
         */
        @Bean
        public Config config(CasClient casClient){
            Config config = new Config(casClient);
            return config;
        }
    
        /**
         * casClient,主要设置回调
         * @param casConfiguration
         * @return
         */
        @Bean
        public CasClient casClient(CasConfiguration casConfiguration){
            CasClient casClient = new CasClient(casConfiguration);
            // 设置CAS访问后的回调地址
            casClient.setCallbackUrl(casProjectUrl + "/callback?client_name=" + clientName);
            casClient.setName(clientName);
            return casClient;
        }
    
        /**
         * CAS服务地址
         * @return
         */
        @Bean
        public CasConfiguration casConfiguration(){
            CasConfiguration casConfiguration = new CasConfiguration();
            // 设置CAS登录页面
            casConfiguration.setLoginUrl(casServerUrl + "/login");
            // 设置CAS协议
            casConfiguration.setProtocol(CasProtocol.CAS20);
            casConfiguration.setPrefixUrl(casServerUrl + "/");
            casConfiguration.setAcceptAnyProxy(true);
            return casConfiguration;
        }
    
    }
    
  • ShiroFilter二次配置(ShiroConfig)

    /**
     * shiroFilter核心配置
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, Config config){
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        putFilterChain(factoryBean);
        // 后面在声明好pac4j提供的过滤器后
        Map<String, Filter> filters = factoryBean.getFilters();
        //1. 准备SecurityFilter
        SecurityFilter securityFilter = new SecurityFilter();
        securityFilter.setConfig(config);
        securityFilter.setClients(clientName);
        filters.put("security",securityFilter);
    
        //2. 设置回调的拦截器
        CallbackFilter callbackFilter = new CallbackFilter();
        callbackFilter.setConfig(config);
        callbackFilter.setDefaultUrl(casProjectUrl);
        filters.put("callback",callbackFilter);
    
        //3. 退出登录
        LogoutFilter logoutFilter = new LogoutFilter();
        logoutFilter.setConfig(config);
        logoutFilter.setCentralLogout(true);
        logoutFilter.setLocalLogout(true);
        logoutFilter.setDefaultUrl(casProjectUrl + "/callback?client_name=" + clientName);
        filters.put("logout",logoutFilter);
    
        return factoryBean;
    }
    
    private void putFilterChain(ShiroFilterFactoryBean factoryBean) {
        Map<String,String> filterChain = new LinkedHashMap<>();
        // 后面在声明好pac4j提供的过滤器后,需要重新设置!
    
        filterChain.put("/test","security");
        filterChain.put("/logout","logout");
        filterChain.put("/callback","callback");
        filterChain.put("/**","security");
        factoryBean.setFilterChainDefinitionMap(filterChain);
    }
    
8.3.3 测试功能

编写了一个Controller,并且要求当前/test地址,必须认证后才可以访问。

  • 访问/test资源后,直接跳转到了CAS登录页面
  • 在CAS登录页面输入用户名和密码认证成功后,跳转到/test地址
  • 再次访问/logout地址,发现退出登录成功后,留在了CAS的退出登录成功页面

希望退出登录后,跳转到登录页面,并且避免出现401问题

需要配置两处位置:

  • CASServer需要支持退出登录后的重定向image.png

  • 修改CasClient对象,页面在退出登录后,会出现401

    public class CasClient extends org.pac4j.cas.client.CasClient {
    
        public CasClient() {
            super();
        }
    
        public CasClient(CasConfiguration configuration) {
            super(configuration);
        }
    
        @Override
        public RedirectAction getRedirectAction(final WebContext context) {
            init();
            AjaxRequestResolver ajaxRequestResolver = getAjaxRequestResolver();
            RedirectActionBuilder redirectActionBuilder = getRedirectActionBuilder();
            // it's an AJAX request -> appropriate action
            if (ajaxRequestResolver.isAjax(context)) {
                logger.info("AJAX request detected -> returning the appropriate action");
                RedirectAction action = redirectActionBuilder.redirect(context);
                cleanRequestedUrl(context);
                return ajaxRequestResolver.buildAjaxResponse(action.getLocation(), context);
            }
            // authentication has already been tried -> unauthorized
            final String attemptedAuth = (String) context.getSessionStore().get(context, getName() + ATTEMPTED_AUTHENTICATION_SUFFIX);
            if (CommonHelper.isNotBlank(attemptedAuth)) {
                cleanAttemptedAuthentication(context);
                cleanRequestedUrl(context);
                // 跑抛出异常,页面401,只修改这个位置!!
                // throw HttpAction.unauthorized(context);
                return redirectActionBuilder.redirect(context);
            }
    
            return redirectActionBuilder.redirect(context);
        }
    
        private void cleanRequestedUrl(final WebContext context) {
            SessionStore<WebContext> sessionStore = context.getSessionStore();
            if (sessionStore.get(context, Pac4jConstants.REQUESTED_URL) != null) {
                sessionStore.set(context, Pac4jConstants.REQUESTED_URL, "");
            }
        }
    
        private void cleanAttemptedAuthentication(final WebContext context) {
            SessionStore<WebContext> sessionStore = context.getSessionStore();
            if (sessionStore.get(context, getName() + ATTEMPTED_AUTHENTICATION_SUFFIX) != null) {
                sessionStore.set(context, getName() + ATTEMPTED_AUTHENTICATION_SUFFIX, "");
            }
        }
    }
    
  • 修改Pac4jConfig,将之前使用的默认CasClient更改为修改的这个!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1550263.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法打卡day19

今日任务&#xff1a; 1&#xff09;235. 二叉搜索树的最近公共祖先 2&#xff09;701.二叉搜索树中的插入操作 3&#xff09;450.删除二叉搜索树中的节点 235. 二叉搜索树的最近公共祖先 题目链接&#xff1a;235. 二叉搜索树的最近公共祖先 - 力扣&#xff08;LeetCode&…

Mysql数据库——高级SQL语句补充

目录 一、子查询——Subquery 1.环境准备 2.In——查询已知的值的数据记录 2.1子查询——Insert 2.2子查询——Update 2.3子查询——Delete 3.Not In——表示否定&#xff0c;不在子查询的结果集里 3.Exists——判断查询结果集是否为空 4.子查询——别名 二、视图—…

政安晨:【Keras机器学习实践要点】(六)—— 使用内置方法进行训练和评估

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: TensorFlow与Keras实战演绎机器学习 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; 本文涵盖使用内置 API 进行训练和验证&#…

pytorch+tensorboard

安装依赖 pip install teorboard pip install torch_tb_profiler了解teorboard 记录并可视化标量[组]、图片[组]。 如何使用 第一步&#xff1a;构建模型&#xff0c;记录中间值&#xff0c;写入summarywriter 每次写入一个标量add_scalar 比如&#xff1a; from torch.u…

深度学习:基于PyTorch的模型解释工具Captum

深度学习&#xff1a;基于PyTorch的模型解释工具Captum 引言简介示例安装解释模型的预测解释文本模型情绪分析问答 解释视觉模型特征分析特征消融鲁棒性 解释多模态模型 引言 当我们训练神经网络模型时&#xff0c;我们通常只关注模型的整体性能&#xff0c;例如准确率或损失函…

上位机图像处理和嵌入式模块部署(qmacvisual区域提取)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 在图像处理中&#xff0c;有两部分比较重要&#xff0c;一个是区域分割&#xff0c;一个是区域提取。区域分割&#xff0c;比较好理解&#xff0c;…

Chrome 插件 storage API 解析

Chrome.storage API 解析 使用 chrome.storage API 存储、检索和跟踪用户数据的更改 一、各模块中的 chrome.storage 内容 1. Service worker 中 runtime 内容 2. Action 中 runtime 内容 3. Content 中 runtime 内容 二、权限&#xff08;Permissions&#xff09; 如果需使…

SPU赋能PSI:探秘隐私集合求交核心技术与高级调度架构实践

1.SPU实现的PSI介绍 1.PSI的定义和种类 隐私集合求交&#xff08;Private Set Intersection, PSI&#xff09;是一种在密码学和安全多方计算&#xff08;MPC&#xff09;领域中的关键技术&#xff0c;允许两个或多个参与者在不泄露各自输入集合中非交集部分的前提下&#xff…

【python分析实战】成本:揭示电商平台月度开支与成本结构占比 - 过于详细 【收藏】

重点关注本文思路&#xff0c;用python分析&#xff0c;方便大家实验复现&#xff0c;代码每次都用全量的&#xff0c;其他工具自行选择。 全文3000字&#xff0c;阅读10min&#xff0c;操作1小时 企业案例实战欢迎关注专栏 每日更新&#xff1a;https://blog.csdn.net/cciehl/…

深入理解element-plus table二次封装:从理论到实践的全面指南

前言 在许多中后台管理系统中&#xff0c;表格占据着半壁江山&#xff0c;如果使用element plus组件库&#xff0c;那么少不了要用到table组件&#xff0c;可是table组件的功能过于基础&#xff0c;因此&#xff0c;我在table组件的实现基础之上进一步封装&#xff0c;从而实现…

2024-03-24 需求分析-智能问答系统-调研

一. 需求列表 基于本地知识库的问答系统对接外围系统 数字人语音识别二. 待调研的公司 2.1 音视贝 AI智能外呼_大模型智能客服系统_大模型知识库系统_杭州音视贝 (yinshibei.com) 2.2 得助智能 智能AI客服机器人-智能电话机器人客服-电话电销机器人-得助智能 (51ima.com) 2…

【LVGL-使用SquareLine Studio设计器 】

LVGL-使用SquareLine Studio设计器 ■ 简介■ 安装■ SquareLine Studio移植到工程 ■ 简介 SquareLine Studio 设计器是一个付费软件。 ■ 安装 SquareLine Studio 设计器的下载地址 我们点击“WINDOWS”下载 SquareLine Studio 设计器&#xff0c;下载完成之后我们就会得到…

ActiveMQ Artemis 系列| High Availability 主备模式(消息复制) 版本2.19.1

一、ActiveMQ Artemis 介绍 Apache ActiveMQ Artemis 是一个高性能的开源消息代理&#xff0c;它完全符合 Java Message Service (JMS) 2.0 规范&#xff0c;并支持多种通信协议&#xff0c;包括 AMQP、MQTT、STOMP 和 OpenWire 等。ActiveMQ Artemis 由 Apache Software Foun…

Android 性能优化实例分享-内存优化 兼顾效率与性能

背景 项目上线一段时间后,回顾重要页面 保证更好用户体验及生产效率&#xff0c;做了内存优化和下载导出优化&#xff0c;具体效果如最后的一节的表格所示。 下面针对拍摄流程的两个页面 预览页 导出页优化实例进行介绍&#xff1a; 一.拍摄前预览页面优化 预览效果问题 存在…

快速上手Spring Cloud 十一:微服务架构下的安全与权限管理

快速上手Spring Cloud 一&#xff1a;Spring Cloud 简介 快速上手Spring Cloud 二&#xff1a;核心组件解析 快速上手Spring Cloud 三&#xff1a;API网关深入探索与实战应用 快速上手Spring Cloud 四&#xff1a;微服务治理与安全 快速上手Spring Cloud 五&#xff1a;Spring …

vuees6新语法

vue的学习网站&#xff1a; https://www.runoob.com/vue2/vue-tutorial.html1.Vue的介绍 学习目标 说出什么是Vue能够说出Vue的好处能够说出Vue的特点 内容讲解 【1】Vue介绍 1.vue属于一个前端框架&#xff0c;底层使用原生js编写的。主要用来进行前端和后台服务器之间的…

搜索插入位置-java

题目描述 : 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。请必须使用时间复杂度为 O(log n) 的算法。 思路分析: 这段代码的解题思想是利用二分查找的方法在…

微服务(基础篇-006-Docker安装-CentOS7)

目录 05-初识Docker-Docker的安装_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1LQ4y127n4?p46&spm_id_frompageDriver&vd_source60a35a11f813c6dff0b76089e5e138cc 0.安装Docker 1.CentOS安装Docker 1.1.卸载&#xff08;可选&#xff09; 1.2.安装dock…

爬虫工作量由小到大的思维转变---<第六十章 Scrapy框架中的信号处理机制(Signals)研究(1)>

前言: Scrapy框架默认的事件驱动设计在某些场景下可能不够灵活或满足定制化需求。因此&#xff0c;深入研究Scrapy框架中的信号处理机制&#xff08;Signals&#xff09;是必要的。 本研究的目的是深入了解Scrapy框架中的信号处理机制&#xff0c;在网络爬虫中的应用和意义。通…

软考高级架构师:ESB 企业服务总线概念和例题

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;大厂高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《Effective Java》独家解析》专栏作者。 热门文章推荐&am…