前言
用 SpringSecurity 做项目的权限管理,功能还是挺强大的。
不过最近遇到一个 需求,就是登录的部分,Spring Security 是配置一个专门的登录页面(一般配置为 login 页面,login 页面调用 j_spring_security_check做 form submit,即表单提交),但是客户希望在其他某个页面(比如首页),也需要加登录的功能,而且使用 ajax 的restlet方式提交,这就索性需要开发一个自定义的登录退出功能(原来表单提交的方式不做改变),之前的表单还有自动登录(remember-me),所以实现自定义登录页面也需要考虑这个问题。
如果实现基础数据库表的自动登录,请查看 https://blog.terrynow.com/2021/07/10/spring-boot-spring-security-db-based-rememeber-me-implement/
接下来WebSecurityConfigurerAdapter配置
这里主要看下rememberMe的配置,我是配置的 token 存数据库的那种,按需要复制
@EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private static final String REMEMBER_KEY = "xxx-web"; @Autowired private MySecurityFilter mySecurityFilter; @Autowired private MyAuthenticationProvider myAuthenticationProvider; @Autowired private MyAccountDetailServiceImpl myAccountDetailService; @Autowired private DataSource dataSource; @Autowired private UserDetailsService userDetailsService; @Bean public PersistentTokenRepository persistentTokenRepository() { MyJdbcTokenRepositoryImpl jdbcTokenRepository = new MyJdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); jdbcTokenRepository.setCreateTableOnStartup(false); return jdbcTokenRepository; } @Bean public PersistentTokenBasedRememberMeServices rememberMeServices() { return new PersistentTokenBasedRememberMeServices(REMEMBER_KEY, userDetailsService, persistentTokenRepository()); } @Bean public MyLogoutHandler myLogoutHandler() { return new MyLogoutHandler("/login?code=1"); } @Bean public AuthenticationSuccessHandler myAuthenticationSuccessHandler() { //也可以根据实际情况来决定首页跳转到哪里,写到MyAuthenticationSuccessHandler的逻辑里去,注入service参考MyLogoutHandler return new MyAuthenticationSuccessHandler(); } @Override public void configure(HttpSecurity http) throws Exception { //防止用户名输入了中文,后面获取到了乱码 CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding("UTF-8"); filter.setForceEncoding(true); http.addFilterBefore(filter, CsrfFilter.class); String loginProcessingUrl = "/j_spring_security_check"; http.headers().frameOptions().disable();//可以嵌套到frame中去 http .addFilterBefore(new CaptchaAuthenticationFilter(loginProcessingUrl, "login?code=-2"), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(mySecurityFilter, FilterSecurityInterceptor.class) .authorizeRequests() .antMatchers(loginProcessingUrl, "/main/**", "/", "/logout_index", "/login_index").permitAll() //任何人都可以访问,需要把自定义的登录、登出的链接加到这里来,要不然会调到原生的 login 界面 .anyRequest().authenticated() //其他所有资源都需要认证,登陆后访问 .and().formLogin().loginPage("/login").failureHandler(new MyAuthFailHandler("/login?code=-1")).loginProcessingUrl(loginProcessingUrl).successHandler(myAuthenticationSuccessHandler()).authenticationDetailsSource(new MyWebAuthenticationDetailsSource()) // .and().rememberMe().rememberMeServices(rememberMeServices()) .and().rememberMe().key(REMEMBER_KEY).tokenRepository(persistentTokenRepository()).userDetailsService(myAccountDetailService) .and().exceptionHandling().accessDeniedPage("/login?code=-3") .and().logout().logoutUrl("/logout").logoutSuccessHandler(myLogoutHandler()).permitAll() .and().csrf().disable() ; //UserDetails实现类Account那边要 重写equals()和hashCode() http.sessionManagement().maximumSessions(5).expiredUrl("/login?code=-5").sessionRegistry(sessionRegistry()); } @Override public void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(myAuthenticationProvider); } @Override public void configure(WebSecurity web) { //解决静态资源被拦截的问题 web.ignoring().antMatchers("/assets/**", "/dashboard/**"); web.ignoring().antMatchers("/**/fav.ico", "/favicon.ico", "/robots.txt"); web.ignoring().antMatchers("/**/captcha.jpg"); web.ignoring().antMatchers("/login", "/authweb"); web.ignoring().antMatchers("/test", "/404_error", "/generic_error", "/about"); } @Bean public SessionRegistry sessionRegistry() { SessionRegistry sessionRegistry = new SessionRegistryImpl(); return sessionRegistry; } @Bean public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); } }
最后实现自定义的登录登出
@Controller public class IndexController { private static final Log log = LogFactory.getLog(IndexController.class); @Resource(name = "systemService") private ISystemService systemService; @Autowired PersistentTokenBasedRememberMeServices rememberMeServices; //首页用到的登录验证,这个也没有用 spring 的,spring 都时 form submit //这里的 JsonResult 是我自己写的一个类,会自动输出 JSON 的,你可以用你习惯的方式输出返回结果 //后面我也会写文章实现 JSON 输出 //login_index 需要加入到上面的配置的 permitAll 里去 @RequestMapping(value = "/login_index") public @ResponseBody JsonResult loginIndex(HttpSession session, HttpServletRequest request,HttpServletResponse response, @RequestParam(value = "username", required = true) String username, @RequestParam(value = "password", required = true) String password, @RequestParam(value = "captcha", required = true) String captcha) { //如果没有验证码需求,可以去掉 String sessionCode = (String) session.getAttribute(Constants.SESSION_GENERATED_CAPTCHA_KEY); session.removeAttribute(Constants.SESSION_GENERATED_CAPTCHA_KEY); if (sessionCode == null || !sessionCode.equals(captcha)) { return JsonResult.fail("验证码错误"); } Account account = systemService.userLogin(username, password); if (account == null) { return JsonResult.fail("用户名或密码错误"); } if (!account.isEnabled() || !account.isAccountNonLocked()) { return JsonResult.fail("账户已停用"); } UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( account, null, account.getAuthorities()); token.setDetails(new WebAuthenticationDetails(request)); SecurityContextHolder.getContext().setAuthentication(token); request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); // 默认根据 request 里是否有remember-me为 true/on,判断是否要加自动登录 // 如果不根据remember-me,那么可以设置rememberMeServices.setAlwaysRemember(true) 那就无论参数有没有remember-me,都记住了 rememberMeServices.loginSuccess(request, response, token); return JsonResult.success("OK"); } //退出后跳转到首页,这个 logout 不是 spring security 的 //login_index 需要加入到上面的配置的 permitAll 里去 @RequestMapping(value = "/logout_index") public String logoutIndex(HttpServletRequest request, HttpServletResponse response, HttpSession session) { session.invalidate(); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null) { //要清空自动登录的信息,要不然退出后,还会自动登录 rememberMeServices.logout(request, response, auth); new SecurityContextLogoutHandler().logout(request, response, auth); } return "redirect:/"; } }
文章评论