[Java]SpringSecurity5实现自定义的登录和退出,包括 remember-me下次自动登录

2021-02-07 49点热度 0人点赞 0条评论
前言

SpringSecurity 做项目的权限管理,功能还是挺强大的。

不过最近遇到一个 需求,就是登录的部分,Spring Security 是配置一个专门的登录页面(一般配置为 login 页面,login 页面调用 j_spring_security_check做 form submit,即表单提交),但是客户希望在其他某个页面(比如首页),也需要加登录的功能,而且使用 ajax 的restlet方式提交,这就索性需要开发一个自定义的登录退出功能(原来表单提交的方式不做改变),之前的表单还有自动登录(remember-me),所以实现自定义登录页面也需要考虑这个问题。

接下来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,判断是否要加自动登录
        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:/";
    }

}

 

Terry

记录开发运维过程中遇到的坑以及解决方案,干货分享

文章评论

*

code