最新版Spring Security & Spring Session 实现单点登录

Spring Security & Spring Session 实现单点登录

    • 前置知识简述
      • 分布式
      • SSO简述
      • Cookie简述
      • Session简述
    • 实现方式
      • JWT实现
      • Session实现
    • 案例(只涉及部分代码,只展示与上篇文章的不同部分)

前置知识简述

分布式

分布式:曾经都集中部署的系统,现在往往会进行拆分,并部署在不同的服务器上,以增大系统容量、增强系统可用性。

SSO简述

单点登录(SSO)就是用户在首次登录之后,再访问其他功能模块就不再需要重新登录。

Cookie简述

Cookie:一些存储在客户端电脑上的信息。首次客户端访问服务器的时候,服务器都会生成Cookie,之后的交互都会携带该Cookie进行交互。因为存储在客户端的原因,导致Cookie就相对不安全。

Session简述

Session:一些存储在服务端的信息。客户端在与服务器之间的交互都会建立Session。由于存储在服务器上,故服务器宕机、服务重启都会导致信息的丢失,当然也不可避免地影响服务器的性能。

实现方式

JWT实现

JWT实现机制就是通过登陆时生成Token令牌,之后前后端通过该Token进行交互,每次cookie里带上Token即可。实现见Spring Security整合JWT实现单点登录

Session实现

由于Session保存在服务器端,所以在现在分布式的系统中,如果没做好Session共享问题,很容易造成数据的丢失。

Session共享实现方式

  1. Session复制
    即每台服务器都保存一份相同的Session,很显然这种方式开销过大。
  2. 通过Nginx做ip_hash
    通过hash算法,把来自某IP的请求分配到对应的服务器上,但是依旧存在服务器宕机,会出现Session丢失问题
  3. Cookie记录Session
    既然涉及Cookie那显然是避免不了Cookie的不安全以及对性能的影响
  4. 通过 Redis 实现 Session的统一缓存
    把每次⽤⼾的请求的时候⽣成的sessionID给放到Redis的服务器上。然后在基于Redis的特性进⾏设置⼀个失效时间的机制,这样就能保证⽤⼾在我们设置的Redis中的session失效时间内,都不需要进⾏再次登录。当业务场景对session管理有⽐较⾼的要求,⽐如利⽤session服务基层单点登录(sso),⽤⼾服务器等功能,就⽐较适合该⽅法。本⽂将使⽤该⽅法实现单点登录功能。

案例(只涉及部分代码,只展示与上篇文章的不同部分)

  1. 创建Springboot项目
  2. 修改pom.xml
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.sessiongroupId>
            <artifactId>spring-session-data-redisartifactId>
        dependency>
  1. 修改application.yml
  2. 修改Spring Security配置类,更新了Springboot版本之后,之前文章中的配置类实现方式已经过期了
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private CustomerExpiredSessionStrategy sessionStrategy;

    @Autowired
    private CustomerLogoutHandler logoutHandler;
    @Bean
    /**
     * 新版本配置
     */
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        //自定义登录页面地址
        http.formLogin().loginPage("/login.html")
                .loginProcessingUrl("/login")               //登录的请求地址
                .successForwardUrl("/session")                 //成功登录之后跳转的地址
                .and().csrf().disable()
                .authorizeRequests()
                .antMatchers("/login", "/register", "/toLogin").permitAll()                 // 放行哪些请求
                .anyRequest().authenticated()                 // 除了上述请求都进行拦截校验
                .and().userDetailsService(userDetailsService)// 设置后 从数据库查询数据
                .addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate))        // 自定义在登录之后存权限
                .addFilter(new TokenAuthenticationFilter(authenticationManager(), redisTemplate))// 自定义验证过滤器 session是否存在
                .logout().addLogoutHandler(logoutHandler)// 自定义退出
                .and().httpBasic();
        // 这里限制最多同时在线1个用户,否则就强制下线
        http.sessionManagement().maximumSessions(1)
                .expiredSessionStrategy(sessionStrategy);
        return http.build();
    }



    @Bean
    public static PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        // 哪些web请求可以直接放行 不需要拦截
        return (web) -> web.ignoring().antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/v2/**", "/api/**", "/register.html", "/login.html");
    }
}

  1. 开启Spring Session帮我们管理Session会话,添加之后就已经能成功实现单点登录
@EnableCaching //开启缓存
@Configuration  //配置类
@EnableRedisHttpSession //开启Spring Session 帮我们管理
public class RedisConfig extends CachingConfigurerSupport {
}
  1. 编写Session强制下线的策略
@Component
public class CustomerExpiredSessionStrategy implements SessionInformationExpiredStrategy {
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        ResponseUtil.out(event.getResponse(), R.ok().message("您的账号已经在其他地方登录").code(ResultCode.UNAUTHORIZED));
    }
}
  1. 编写TokenLoginFilter实现自定义的登录逻辑
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
                                            Authentication auth){
        HttpSession session = req.getSession();
        SecurityUser user = (SecurityUser) auth.getPrincipal();
        RedisBean redisBean = new RedisBean(user.getUsername(),user.getPermissionValueList());
        redisTemplate.opsForValue().set(session.getId(), redisBean);   // 以Session : admin 、 权限 存入redis
        ResponseUtil.out(res, R.ok().message("登录成功"));
    }
  1. 编写TokenAuthenticationFilter实现自定义校验逻辑
  private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        HttpSession session = request.getSession();
        String sessionId = session.getId();
        // 从redis中获取权限
        Object object = redisTemplate.opsForValue().get(sessionId);
        if (Objects.nonNull(object)) {
            RedisBean redisBean = (RedisBean) object;
            List<String> permissionValueList = redisBean.getPermissionValueList();
            Collection<GrantedAuthority> authorities = new ArrayList<>();             // 把权限封装成指定形式的集合
            for (String permissionValue : permissionValueList) {
                if (Strings.isNullOrEmpty(permissionValue)) {
                    continue;
                }
                // SimpleGrantedAuthority 权限内容为String 将String类型的权限存入SimpleGrantedAuthority类
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
                authorities.add(authority);
            }
            // 把有用信息塞入UsernamePasswordAuthenticationToken后返回
            // UsernamePasswordAuthenticationToken令牌存了sessionid 和 权限
            return new UsernamePasswordAuthenticationToken(redisBean.getUsername(), null, authorities);
        }
        return null;
    }
  1. 最后效果
    最新版Spring Security & Spring Session 实现单点登录_第1张图片
    没有附上的TokenLoginFilter和TokenAuthenticationFilter代码可以看之前的文章,结合Spring Security整合JWT实现单点登录进行理解

你可能感兴趣的:(java,#,SpringBoot,spring,spring,redis,java)