【问题标题】:Spring Security with session get online users returns empty具有会话获取在线用户的 Spring Security 返回空
【发布时间】:2021-02-18 22:12:47
【问题描述】:

相同的问题有多个版本,但似乎没有一个可以解决这个问题。我想从spring security获得在线用户。我知道我们需要 Autowire SessionRegistry 并使用它。但是,它仍然不起作用。这是代码。 不确定是由于自定义用户名、密码身份验证还是由于自定义密码编码器或其他原因。一切似乎都是正确的。即使获取当前登录用户的数据也可以正常工作,但未登录用户列表。

SessionSecurityConfig.java

@EnableJpaRepositories(basePackageClasses = UsersRepository.class)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SessionSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordencoder;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private UsernamePasswordAuthProvider usernamepasswdauth;

    @Bean
    SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(usernamepasswdauth).userDetailsService(userDetailsService)
                .passwordEncoder(passwordencoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
        http.csrf().disable();
        http.authorizeRequests() //
                .antMatchers("/ua/*").permitAll() //
                .antMatchers("/auth/*").authenticated() //
                .and().requestCache() //
                .requestCache(new NullRequestCache());
        http.httpBasic().disable();
        http.formLogin().disable();
        http.logout().disable();
        http
          .sessionManagement()
          .maximumSessions(1).sessionRegistry(sessionRegistry());
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }

}

PasswordUpgrader.java

@Component
@Primary
public class PasswordUpgrader implements PasswordEncoder { // used to upgrade NTML password hashes to Bcrypt

    private final static BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();

    private final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();

    @Autowired
    JdbcTemplate jdbc;

    public String encode(CharSequence rawPassword) {
        byte[] bytes = NtlmPasswordAuthentication.nTOWFv1(rawPassword.toString());
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = HEX_ARRAY[v >>> 4];
            hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
        }
        return new String(hexChars).toLowerCase();
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword == null || encodedPassword.length() == 0) {
            return false;
        }
        if (encodedPassword.equals(encode(rawPassword))) {
            String sql = "update user_data set password=? where password=?";
            jdbc.update(sql, new Object[] { bcrypt.encode(rawPassword), encode(rawPassword) });
            return true;
        } else {
            return bcrypt.matches(rawPassword, encodedPassword);
        }
    }
}

用户名密码AuthProvider.java

@Component
public class UsernamePasswordAuthProvider implements AuthenticationProvider {
    Log logger = LogFactory.getLog(getClass());
    
    @Autowired
    private PasswordEncoder passwordencoder;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    Userdata userdata;

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
        String username = String.valueOf(auth.getPrincipal());
        String password = String.valueOf(auth.getCredentials());
        UserDetails user = userDetailsService.loadUserByUsername(username);
        String encodedpassword = user.getPassword().toString();
        logger.info("inside username passwd authentication");
        if (encodedpassword != null && password != null && passwordencoder.matches(password, encodedpassword)) {
            logger.info("inside username passwd authentication");
            return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
        }
        throw new BadCredentialsException("Username/Password Incorrect");
    }

    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

}

UnauthController.java

@RestController
@RequestMapping("/ua")
public class UnauthController {

    @Autowired
    private UsernamePasswordAuthProvider usernamepasswdauth;

    @PostMapping("/login")
    public Map<String, Object> login(HttpServletRequest req, @RequestBody Map<String, Object> map) {
        Authentication auth = usernamepasswdauth.authenticate(new UsernamePasswordAuthenticationToken(
                map.get("username").toString(), map.get("password").toString()));
        SecurityContextHolder.getContext().setAuthentication(auth);
        map.put("sessionid", session.getId());
        return map;
    }
}

AuthController.java

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    Userdata user;

    @Autowired
    SessionRegistry sessionregistry;

    Log logger = LogFactory.getLog(getClass());

    @GetMapping("/onlineusers")
    public List<String> authhello(Authentication authentication) {
        logger.debug(user.getEmail()); // prints current logged in user's email.
        logger.debug(sessionRegistry.getAllPrincipals());//returns empty
        return sessionRegistry.getAllPrincipals().stream()
                .filter(u -> !sessionRegistry.getAllSessions(u, false).isEmpty()).map(Object::toString)
                .collect(Collectors.toList());
    }
}

尝试过的方法:

  1. Baeldung
  2. Stackoverflow
  3. StackOverflow

【问题讨论】:

  • 您找到解决方案了吗?
  • @It'sK 通过在 Redis 中创建和存储一个集合并在用户登录时向其中添加用户名来应用解决方法。在 15 分钟内使用了一次调度程序来检查会话到期并从集合中删除用户名。仍在等待适当的修复。
  • 我遇到过,当您使用自定义身份验证并自己重定向经过身份验证的用户时,它不会调用 spring session 过滤器,因此它不会自行处理,我们必须对其进行配置( sessionManagement 下的 sessionAuthenticationStrategy 和 expiredSessionStrategy)。我已经设置了它,它现在就像魅力一样。可能我稍后会写答案以帮助其他人。
  • @It'sK 这将是一个很大的帮助。另外,如果您可以在 GitHub 上发布该项目并添加一个链接,那将是很棒的教程。
  • 已经计划好了。这些配置我也很挣扎,所以我很乐意提供帮助。

标签: java spring spring-boot spring-security


【解决方案1】:

如果你仔细阅读文档here,它写得很好(虽然很隐蔽)。问题的原因在于身份验证后处理数据的方式。在 spring security 提供的默认身份验证中,成功身份验证后,控件通过过滤器管理会话传递。但是,如果您使用自定义身份验证并在成功身份验证后重定向用户,则过滤器不会出现,这就是为什么没有在会话注册表中添加会话并返回空列表的原因。

解决方法是在spring security的会话管理配置中设置带有会话注册的认证策略。这将导致预期的行为。 您会发现代码更有帮助。


方法一:

会话的 Spring 安全配置

http
    .sessionManagement()
    .sessionAuthenticationStrategy(concurrentSession())
    .maximumSessions(-1)
                .expiredSessionStrategy(sessionInformationExpiredStrategy())

定义bean
@Bean
public CompositeSessionAuthenticationStrategy concurrentSession() {

    ConcurrentSessionControlAuthenticationStrategy concurrentAuthenticationStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
    List<SessionAuthenticationStrategy> delegateStrategies = new ArrayList<SessionAuthenticationStrategy>();
    delegateStrategies.add(concurrentAuthenticationStrategy);
    delegateStrategies.add(new SessionFixationProtectionStrategy());
    delegateStrategies.add(new RegisterSessionAuthenticationStrategy(sessionRegistry()));

    return new CompositeSessionAuthenticationStrategy(delegateStrategies);
}


@Bean
SessionInformationExpiredStrategy sessionInformationExpiredStrategy() {
    return new CustomSessionInformationExpiredStrategy("/login");
}


@Bean
public SessionRegistry sessionRegistry() {
    return new SessionRegistryImpl();
}

这是 CustomSessionInformationExpiredStrategy.java

public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {

    private String expiredUrl = "";

    public CustomSessionInformationExpiredStrategy(String expiredUrl) {
        this.expiredUrl = expiredUrl;
    }

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {

        HttpServletRequest request = sessionInformationExpiredEvent.getRequest();
        HttpServletResponse response = sessionInformationExpiredEvent.getResponse();
        request.getSession();// creates a new session
        response.sendRedirect(request.getContextPath() + expiredUrl);
    }

}

方法:2

在 spring 安全配置中,使用 方法 1 中的 concurrentSession()。

http.sessionManagement().sessionAuthenticationStrategy(concurrentSession());
http.addFilterBefore(concurrentSessionFilter(), ConcurrentSessionFilter.class);

这里是 CustomConcurrentSessionFilter.java

public class CustomConcurrentSessionFilter extends ConcurrentSessionFilter {

    public CustomConcurrentSessionFilter(SessionRegistry sessionRegistry) {
        super(sessionRegistry);
    }

    public CustomConcurrentSessionFilter(SessionRegistry sessionRegistry, SessionInformationExpiredStrategy sessionInformationExpiredStrategy) {
        super(sessionRegistry, sessionInformationExpiredStrategy);
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        super.doFilter(req, res, chain);
    }

}

还在为某事摸不着头脑吗?在Github repo 找到工作示例。随意提出问题或贡献。

【讨论】:

  • 我正在针对 LDAP (AD) 进行身份验证。方法1对我有用。谢谢。我没有让方法 2 起作用。 concurrentSessionFilter() 把我扔了。由于类需要构造函数参数,因此不能是当前状态的 @Bean。我没有查看 GIT 存储库,我可能会发现它应该是怎样的。
猜你喜欢
  • 2015-12-06
  • 2018-01-17
  • 1970-01-01
  • 2018-04-26
  • 2019-09-13
  • 1970-01-01
  • 1970-01-01
  • 2013-03-08
  • 1970-01-01
相关资源
最近更新 更多