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
有些时候咱们想要在认证成功后作一些业务处理,例如添加积分;有些时候咱们想要在认证失败后也作一些业务处理,例如记录日志。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() 的设置,不然没法生效。这两套配置同时只能存在一套。
自定义 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
为认证后该用户的认证信息,这里打印日志后,重定向到了首页。跨域
自定义 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
为认证失败所产生的异常,这里也是简单的返回到前台。服务器
@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(); } ... }
运行程序,当咱们成功登录后,发现日志信息被打印出来,页面被重定向到了首页:
当咱们认证失败后,发现日志中“登录失败”被打印出来,页面展现了认证失败的异常消息:
当用户登陆后,咱们能够设置 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 数。
在上一节的基础上,修改 configure()
为:
.sessionManagement() .invalidSessionUrl("/login/invalid") .maximumSessions(1) // 当达到最大值时,是否保留已经登陆的用户 .maxSessionsPreventsLogin(false) // 当达到最大值时,旧用户被踢出后的操做 .expiredSessionStrategy(new CustomExpiredSessionStrategy())
增长了下面三行代码,其中:
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(true)
时的状况,咱们发现第一个浏览器登陆后,第二个浏览器没法登陆:
下面来看下如何主动踢出一个用户。
首先须要在容器中注入名为 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 + "个"; } }
运行程序,分别使用 admin 和 jitwxs 帐户登陆,admin 访问 /kick?username=jitwxs
来踢出用户 jitwxs,jitwxs 刷新页面,发现被踢出。
补充一下退出登陆的内容,在以前,咱们直接在 WebSecurityConfig 的 configure() 方法中,配置了:
http.logout();
这就是 Spring Security 的默认退出配置,Spring Security 在退出时候作了这样几件事:
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;
退出登陆的比较简单,我就直接贴代码,不截图了。
在最后补充下关于 Session 共享的知识点,通常状况下,一个程序为了保证稳定至少要部署两个,构成集群。那么就牵扯到了 Session 共享的问题,否则用户在 8080 登陆成功后,后续访问了 8060 服务器,结果又提示没有登陆。
这里就简单实现下 Session 共享,采用 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 命令行中。
首先须要导入依赖,由于咱们采用 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 { }
这样就完成了基于 Redis 的 Session 共享,下面来测试下。首先修改 IDEA 配置来容许项目在多端口运行,勾选 Allow running in parallel
:
运行程序,而后修改配置文件,将 server.port
更改成 8060,再次运行。这样项目就会分别在默认的 8080 端口和 8060 端口运行。
先访问 localhost:8080
,登陆成功后,再访问 localhost:8060
,发现无需登陆。
而后咱们进入 Redis 查看下 key:
最后再测试下以前配置的 session 设置是否还有效,使用其余浏览器登录,登录成功后发现原浏览器用户的确被踢出。
--------------------- 做者:Jitwxs 来源:CSDN 原文:https://blog.csdn.net/yuanlaijike/article/details/84638745