SpringBoot集成Spring Security(6)——登陆管理

文章目录

1、自定义认证成功、失败处理html

  1.1 CustomAuthenticationSuccessHandlergit

  1.2 CustomAuthenticationFailureHandler
  1.3 修改 WebSecurityConfig
  1.4 运行程序
2、Session 超时
3、限制最大登陆数
4、踢出用户
5、退出登陆
6、Session 共享
  6.1 配置 Redis
  6.2 配置 Session 共享
  6.3 运行程序
在本篇中,主要关注登陆的管理,所以代码使用最原始版本的便可,即《SpringBoot集成Spring Security(1)——入门程序》源码便可。github

源码地址:https://github.com/jitwxs/blog_sample

1、自定义认证成功、失败处理

有些时候咱们想要在认证成功后作一些业务处理,例如添加积分;有些时候咱们想要在认证失败后也作一些业务处理,例如记录日志。redis

在以前的文章中,关于认证成功、失败后的处理都是以下配置的:spring

http.authorizeRequests()
    // 若是有容许匿名的url,填在下面
//    .antMatchers().permitAll()
    .anyRequest().authenticated().and()
    // 设置登录页
    .formLogin().loginPage("/login")
    .failureUrl("/login/error")
    .defaultSuccessUrl("/")
    .permitAll()
    ...;

即 failureUrl() 指定认证失败后Url,defaultSuccessUrl() 指定认证成功后Url。咱们能够经过设置 successHandler()和 failureHandler() 来实现自定义认证成功、失败处理。docker

PS:当咱们设置了这两个后,须要去除 failureUrl() 和 defaultSuccessUrl() 的设置,不然没法生效。这两套配置同时只能存在一套。

 

 

1.1 CustomAuthenticationSuccessHandler

 自定义 CustomAuthenticationSuccessHandler 类来实现 AuthenticationSuccessHandler 接口,用来处理认证成功后逻辑:json

 

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        logger.info("登陆成功,{}", authentication);
        
        response.sendRedirect("/");
    }
}

onAuthenticationSuccess() 方法的第三个参数 Authentication 为认证后该用户的认证信息,这里打印日志后,重定向到了首页。跨域

1.2 CustomAuthenticationFailureHandler

自定义 CustomAuthenticationFailureHandler 类来实现 AuthenticationFailureHandler 接口,用来处理认证失败后逻辑:浏览器

@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Autowired
    private ObjectMapper objectMapper;

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        logger.info("登录失败");

        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
    }
}

onAuthenticationFailure()方法的第三个参数 exception 为认证失败所产生的异常,这里也是简单的返回到前台。服务器

1.3 修改 WebSecurityConfig

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
    @Autowired
    private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
    
    ...
   
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 若是有容许匿名的url,填在下面
//                .antMatchers().permitAll()
                .anyRequest().authenticated().and()
                // 设置登录页
                .formLogin().loginPage("/login")
                .successHandler(customAuthenticationSuccessHandler).permitAll()
                .failureHandler(customAuthenticationFailureHandler)
//                .failureUrl("/login/error")
//                .defaultSuccessUrl("/")
                .permitAll()
                ...;

        // 关闭CSRF跨域
        http.csrf().disable();
    }

    ...
}

 

  1. 首先将 customAuthenticationSuccessHandler 和 customAuthenticationFailureHandler注入进来
  2. 配置 successHandler() 和 failureHandler()
  3. 注释 failureUrl() 和 defaultSuccessUrl()

1.4 运行程序

运行程序,当咱们成功登录后,发现日志信息被打印出来,页面被重定向到了首页:

当咱们认证失败后,发现日志中“登录失败”被打印出来,页面展现了认证失败的异常消息:

 

2、Session 超时

当用户登陆后,咱们能够设置 session 的超时时间,当达到超时时间后,自动将用户退出登陆。

Session 超时的配置是 SpringBoot 原生支持的,咱们只须要在 application.properties 配置文件中配置:

# session 过时时间,单位:秒
server.servlet.session.timeout=60
Tip:
从用户最后一次操做开始计算过时时间。
过时时间最小值为 60 秒,若是你设置的值小于 60 秒,也会被更改成 60 秒。

咱们能够在 Spring Security 中配置处理逻辑,在 session 过时退出时调用。修改 WebSecurityConfig 的 configure()方法,添加:

.sessionManagement()
    // 如下二选一
    //.invalidSessionStrategy()
    //.invalidSessionUrl();

Spring Security 提供了两种处理配置,一个是 invalidSessionStrategy(),另一个是 invalidSessionUrl()

这两个的区别就是一个是前者是在一个类中进行处理,后者是直接跳转到一个 Url。简单起见,我就直接用 invalidSessionUrl()了,跳转到 /login/invalid,咱们须要把该 Url 设置为免受权访问, 配置以下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            // 若是有容许匿名的url,填在下面
            .antMatchers("/login/invalid").permitAll()
            .anyRequest().authenticated().and()
            ...
            .sessionManagement()
                .invalidSessionUrl("/login/invalid");

    // 关闭CSRF跨域
    http.csrf().disable();
}

 

在 controller 中写一个接口进行处理:

@RequestMapping("/login/invalid")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ResponseBody
public String invalid() {
    return "Session 已过时,请从新登陆";
}

运行程序,登录成功后等待一分钟(或者重启服务器),刷新页面:

session 过时

3、限制最大登陆数

接下来实现限制最大登录数,原理就是限制单个用户可以存在的最大 session 数。

在上一节的基础上,修改 configure() 为:

.sessionManagement()
    .invalidSessionUrl("/login/invalid")
    .maximumSessions(1)
    // 当达到最大值时,是否保留已经登陆的用户
    .maxSessionsPreventsLogin(false)
    // 当达到最大值时,旧用户被踢出后的操做
    .expiredSessionStrategy(new CustomExpiredSessionStrategy())

增长了下面三行代码,其中:

  • maximumSessions(int):指定最大登陆数
  • maxSessionsPreventsLogin(boolean):是否保留已经登陆的用户;为true,新用户没法登陆;为 false,旧用户被踢出
  • expiredSessionStrategy(SessionInformationExpiredStrategy):旧用户被踢出后处理方法
maxSessionsPreventsLogin()可能不太好理解,这里咱们先设为 false,效果和 QQ 登陆是同样的,登录后以前登陆的帐户被踢出。

 

编写 CustomExpiredSessionStrategy 类,来处理旧用户登录失败的逻辑:

public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {
    private ObjectMapper objectMapper = new ObjectMapper();
//    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        Map<String, Object> map = new HashMap<>(16);
        map.put("code", 0);
        map.put("msg", "已经另外一台机器登陆,您被迫下线。" + event.getSessionInformation().getLastRequest());
        // Map -> Json
        String json = objectMapper.writeValueAsString(map);

        event.getResponse().setContentType("application/json;charset=UTF-8");
        event.getResponse().getWriter().write(json);

        // 若是是跳转html页面,url表明跳转的地址
        // redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), "url");
    }
}

onExpiredSessionDetected() 方法中,处理相关逻辑,我这里只是简单的返回一句话。

执行程序,打开两个浏览器,登陆同一个帐户。由于我设置了 maximumSessions(1),也就是单个用户只能存在一个 session,所以当你刷新先登陆的那个浏览器时,被提示踢出了。
maxSessionsPreventsLogin 为 false

下面咱们来测试下 maxSessionsPreventsLogin(true) 时的状况,咱们发现第一个浏览器登陆后,第二个浏览器没法登陆:

maxSessionsPreventsLogin 为 true

4、踢出用户

下面来看下如何主动踢出一个用户。

首先须要在容器中注入名为 SessionRegistry 的 Bean,这里我就简单的写在 WebSecurityConfig 中:

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

 

修改 WebSecurityConfig 的 configure() 方法,在最后添加一行 .sessionRegistry()

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 若是有容许匿名的url,填在下面
                .antMatchers("/login/invalid").permitAll()
                .anyRequest().authenticated().and()
                // 设置登录页
                .formLogin().loginPage("/login")
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler)
                .permitAll().and()
                .logout().and()
                .sessionManagement()
                    .invalidSessionUrl("/login/invalid")
                    .maximumSessions(1)
                    // 当达到最大值时,是否保留已经登陆的用户
                    .maxSessionsPreventsLogin(false)
                    // 当达到最大值时,旧用户被踢出后的操做
                    .expiredSessionStrategy(new CustomExpiredSessionStrategy())
                    .sessionRegistry(sessionRegistry());

        // 关闭CSRF跨域
        http.csrf().disable();
    }
}

 

编写一个接口用于测试踢出用户:

@Controller
public class LoginController {
    @Autowired
    private SessionRegistry sessionRegistry;

    ...

    @GetMapping("/kick")
    @ResponseBody
    public String removeUserSessionByUsername(@RequestParam String username) {
        int count = 0;

        // 获取session中全部的用户信息
        List<Object> users = sessionRegistry.getAllPrincipals();
        for (Object principal : users) {
            if (principal instanceof User) {
                String principalName = ((User)principal).getUsername();
                if (principalName.equals(username)) {
                    // 参数二:是否包含过时的Session
                    List<SessionInformation> sessionsInfo = sessionRegistry.getAllSessions(principal, false);
                    if (null != sessionsInfo && sessionsInfo.size() > 0) {
                        for (SessionInformation sessionInformation : sessionsInfo) {
                            sessionInformation.expireNow();
                            count++;
                        }
                    }
                }
            }
        }
        return "操做成功,清理session共" + count + "个";
    }
}
  1. sessionRegistry.getAllPrincipals(); 获取全部 principal 信息
  2. 经过 principal.getUsername 是否等于输入值,获取到指定用户的 principal
  3. sessionRegistry.getAllSessions(principal, false)获取该 principal 上的全部 session
  4. 经过 sessionInformation.expireNow() 使得 session 过时

运行程序,分别使用 admin 和 jitwxs 帐户登陆,admin 访问 /kick?username=jitwxs 来踢出用户 jitwxs,jitwxs 刷新页面,发现被踢出。

5、退出登陆

补充一下退出登陆的内容,在以前,咱们直接在 WebSecurityConfig 的 configure() 方法中,配置了:

http.logout();

 

这就是 Spring Security 的默认退出配置,Spring Security 在退出时候作了这样几件事:

  1. 使当前的 session 失效
  2. 清除与当前用户有关的 remember-me 记录
  3. 清空当前的 SecurityContext
  4. 重定向到登陆页

Spring Security 默认的退出 Url 是 /logout,咱们能够修改默认的退出 Url,例如修改成 /signout,那么在退出登陆的按钮,地址也要改成/signout

http.logout()
    .logoutUrl("/signout");

咱们也能够配置当退出时清除浏览器的 Cookie,例如清除 名为 JSESSIONID 的 cookie:

http.logout()
    .logoutUrl("/signout")
    .deleteCookies("JSESSIONID");

 

咱们也能够配置退出后处理的逻辑,方便作一些别的操做:

http.logout()
    .logoutUrl("/signout")
    .deleteCookies("JSESSIONID")
    .logoutSuccessHandler(logoutSuccessHandler);

建立类 DefaultLogoutSuccessHandler

@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
    Logger log = LoggerFactory.getLogger(getClass());
    
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String username = ((User) authentication.getPrincipal()).getUsername();
        log.info("退出成功,用户名:{}", username);
        
        // 重定向到登陆页
        response.sendRedirect("/login");
    }
}

最后把它注入到 WebSecurityConfig 便可:

@Autowired
private CustomLogoutSuccessHandler logoutSuccessHandler;
退出登陆的比较简单,我就直接贴代码,不截图了。

6、Session 共享

在最后补充下关于 Session 共享的知识点,通常状况下,一个程序为了保证稳定至少要部署两个,构成集群。那么就牵扯到了 Session 共享的问题,否则用户在 8080 登陆成功后,后续访问了 8060 服务器,结果又提示没有登陆。

这里就简单实现下 Session 共享,采用 Redis 来存储。

6.1 配置 Redis

为了方便起见,我直接使用 Docker 快速部署,若是你须要传统方式安装,能够参考文章《Redis初探(1)——Redis的安装》

docker pull redis
docker run --name myredis -p 6379:6379 -d redis
docker exec -it myredis redis-cli

这样就启动了 redis,而且进入到 redis 命令行中。

6.2 配置 Session 共享

首先须要导入依赖,由于咱们采用 Redis 方式实现,所以导入:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

 

在 application.xml 中新增配置指定 redis 地址以及 session 的存储方式:

spring.redis.host=192.168.139.129
spring.redis.port=6379

spring.session.store-type=redis

而后为主类添加 @EnableRedisHttpSession 注解。

@EnableRedisHttpSession
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

若是在主类添加的@EnableRedisHttpSession 后,程序运行抛出异常,则取消上述注解,将@EnableRedisHttpSession 注解移交到RedisSessionConfig 类

@Configuration  
@EnableRedisHttpSession  
public class RedisSessionConfig {  
}  

 

 

6.3 运行程序

这样就完成了基于 Redis 的 Session 共享,下面来测试下。首先修改 IDEA 配置来容许项目在多端口运行,勾选 Allow running in parallel

Allow running in parallel

运行程序,而后修改配置文件,将 server.port 更改成 8060,再次运行。这样项目就会分别在默认的 8080 端口和 8060 端口运行。

先访问 localhost:8080,登陆成功后,再访问 localhost:8060,发现无需登陆。

Session 共享运行结果

而后咱们进入 Redis 查看下 key:

最后再测试下以前配置的 session 设置是否还有效,使用其余浏览器登录,登录成功后发现原浏览器用户的确被踢出。

--------------------- 做者:Jitwxs 来源:CSDN 原文:https://blog.csdn.net/yuanlaijike/article/details/84638745

相关文章
相关标签/搜索