SpringSecurity5.7+最新案例 -- 用户名密码+验证码+记住我······

墨色 8月前 ⋅ 266 阅读

简介

根据最近一段时间的设计以及摸索,对SpringSecurity进行总结,目前security采用的是5.7+版本,和以前的版本最大的差别就是,以前创建SecurityConfig需要继承WebSecurityConfigurerAdapter,而到了5.7以后,并不推荐这种做法,查了网上一些教程,其实并不好,绝大多数用的都是老版本,所以出此文案。

一些原理什么的,就不过多说明了,一般搜索资料的,其实根本不想你说什么原理 T·T。

写法区别

最大的区别就是不需要继承WebSecurityConfigurerAdapter(官方也开始弃用此方法),所有配置不需要用and()方法链接,采用lambda处理,个人觉得lambda写法更加的美观,可阅读性更高

这是以前的写法
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
         http.authorizeHttpRequests()
                .mvcMatchers("/login.html").permitAll()
                .mvcMatchers("/index").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/doLogin")
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .successForwardUrl("/index") 		 //forward 跳转           注意:不会跳转到之前请求路径
                //.defaultSuccessUrl("/index")   //redirect 重定向    注意:如果之前请求路径,会有优先跳转之前请求路径
                .failureUrl("/login.html")
                .and()
                .csrf().disable();//关闭 CSRF
    }
}
这是现在的写法
@Configuration
public class WebSecurityConfigurer{
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests(auth ->
                        auth.mvcMatchers(loadExcludePath()).permitAll()
                                .anyRequest().authenticated()
                )
                .cors(conf ->
                        conf.configurationSource(corsConfigurationSource())
                )
                .rememberMe(conf -> {
                    conf.useSecureCookie(true)
                            .rememberMeServices(rememberMeServices());
                })
                .formLogin(conf ->
                        conf.loginPage(loginPage)
                                .defaultSuccessUrl(defaultSuccessUrl, true)
                                .failureUrl(loginPage)
                )
                .logout(conf ->
                        conf.invalidateHttpSession(true)
                                .clearAuthentication(true)
                                .logoutSuccessUrl(logoutSuccessUrl)
                )
                .csrf(AbstractHttpConfigurer::disable)
                .build();
    }
}

案例包含

自定义认证数据源、密码加密、remember-me、session会话管理、csrf漏洞保护、跨域处理、异常处理 等核心模块,授权以后再说

目录结构

image-20230803194417226.png

核心代码

主配置SecurityConfig
@Configuration
public class SecurityConfig<S extends Session> {

    // 基于数据库验证,自定义实现UserDetailService
    @Resource
    MyUserDetailsService myUserDetailsService;

    // 注入自定义认证失败处理器
    @Bean
    MyAuthFailureHandler myAuthFailureHandler() {
        return new MyAuthFailureHandler();
    }

    // 注入数据源
    @Resource
    DataSource dataSource;

    // 注入自定义认证成功处理器
    @Bean
    MyAuthSuccessHandler myAuthSuccessHandler() {
        return new MyAuthSuccessHandler();
    }

    // 注入自定义注销登录处理器
    @Bean
    MyLogoutSuccessHandler myLogoutSuccessHandler() {
        return new MyLogoutSuccessHandler();
    }

    // 注入自定义未认证访问处理器
    @Bean
    MyAuthEntryPointHandler myAuthEntryPointHandler() {
        return new MyAuthEntryPointHandler();
    }

    // 注入自定义session会话管理处理器
    @Bean
    MySessionExpiredHandler mySessionExpiredHandler() {
        return new MySessionExpiredHandler();
    }

    // 注入自定义未授权访问处理器
    @Bean
    MyAccessDeniedHandler myAccessDeniedHandler() {
        return new MyAccessDeniedHandler();
    }

    // 注入redis-session管理
    @Resource
    private FindByIndexNameSessionRepository<S> sessionRepository;

    // 注入redis-session管理
    @Bean
    public SpringSessionBackedSessionRegistry<S> sessionRegistry() {
        return new SpringSessionBackedSessionRegistry<>(sessionRepository);
    }

    // 登录url
    private final String loginUrl = "/login";

    /**
     * 配置放行请求
     */
    private String[] loadExcludePath() {
        return new String[]{
                "/pm", loginUrl, "/error"
        };
    }

    /**
     * 配置密码加密规则
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 跨域配置
     */
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));
        corsConfiguration.setAllowedMethods(Collections.singletonList("*"));
        corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
        corsConfiguration.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }

    /**
     * 记住我 令牌 持久化存储
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        // 这个sql可手动执行到数据库中,当setCreateTableOnStartup 为 false的时候
        String initSql = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key,token varchar(64) not null, last_used timestamp not null)";
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        //只需要没有表时设置为 true,也就是说,第一次启动的时候设置为true,后续都要设置为false
        jdbcTokenRepository.setCreateTableOnStartup(false);
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

    /**
     * 记住我 service 注入
     */
    @Bean
    public RememberMeServices rememberMeServices() {
        return new MyRememberMeServices(UUID.randomUUID().toString(), myUserDetailsService, persistentTokenRepository());
    }

    /**
     * 构建认证管理器
     */
    @Bean
    AuthenticationManager authenticationManager(HttpSecurity httpSecurity) throws Exception {
        // 开启自定义userDetail,开启密码加密
        return httpSecurity.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(myUserDetailsService)
                .passwordEncoder(passwordEncoder())
                .and()
                .build();
    }

    /**
     * 自定义认证过滤器
     */
    @Bean
    public MyAuthenticationFilter myAuthenticationFilter(HttpSecurity httpSecurity) throws Exception {
        MyAuthenticationFilter myAuthenticationFilter = new MyAuthenticationFilter();
        // 设置认证管理器
        myAuthenticationFilter.setAuthenticationManager(authenticationManager(httpSecurity));
        // 设置登录成功后返回
        myAuthenticationFilter.setAuthenticationSuccessHandler(myAuthSuccessHandler());
        // 设置登录失败后返回
        myAuthenticationFilter.setAuthenticationFailureHandler(myAuthFailureHandler());
        // 设置记住我功能  认证登录的时候,往数据库写值使用
//        myAuthenticationFilter.setRememberMeServices(rememberMeServices());
        return myAuthenticationFilter;
    }

    /**
     * 安全认证过滤器链
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests(auth ->
                // 配置需要放行的请求
                        auth.mvcMatchers(loadExcludePath()).permitAll()
                                // 除了以上放行请求,其它都需要进行认证
                                .anyRequest().authenticated()
                )

                // 跨域处理
                .cors(conf ->
                        // 配置跨域
                        conf.configurationSource(corsConfigurationSource())
                )

                // csrf 关闭
                .csrf(AbstractHttpConfigurer::disable)
                // csrf 开启请求,并且将login请求放行, 登录成功后,在cookies中会有一个XCSRF-TOKEN的值(value)
                // 后续的所有接口,在 header中加入 X-XSRF-TOKEN:value即可
//                .csrf(conf -> conf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).ignoringAntMatchers(loginUrl))

                // 开启请求登录
                .formLogin(
                        // 这里是自定义json body请求登录
                        // 将默认登录页面关闭,只能采用发请求的方式登录
                        AbstractHttpConfigurer::disable

                        // 这里适用于form表单登录
//                        conf ->
//                                conf.successHandler(myAuthSuccessHandler())
//                                        .failureHandler(myAuthFailureHandler())
                )

                // 开启记住我功能  --  自动登录的时候使用
//                .rememberMe(conf ->
//                        conf
//                                .useSecureCookie(true)
//                                .rememberMeServices(rememberMeServices())
//                                .tokenRepository(persistentTokenRepository())
//                )

                // 请求 未认证,未授权 时提示
                .exceptionHandling(conf ->
                        // 未认证
                        conf.authenticationEntryPoint(myAuthEntryPointHandler())
                                // 未授权
                                .accessDeniedHandler(myAccessDeniedHandler())
                )

                // 注销登录返回提示
                .logout(conf ->
                        conf.logoutSuccessHandler(myLogoutSuccessHandler())
                                .invalidateHttpSession(true)
                                .clearAuthentication(true)
                )

                // session 会话管理
                .sessionManagement(conf ->
                        // 同一个用户 只允许 创建 多少个 会话
                        conf.maximumSessions(2)
                                // 同一个用户登录之后,禁止再次登录
                                .maxSessionsPreventsLogin(true)
                                // 会话过期处理
                                .expiredSessionStrategy(mySessionExpiredHandler())
                                // 会话信息注册,交由redis管理
                                // 需要引入 org.springframework.boot:spring-boot-starter-data-redis
                                // 和 org.springframework.session:spring-session-data-redis
//                                .sessionRegistry(sessionRegistry())
                )

                // 自定义过滤器替换 默认的 UsernamePasswordAuthenticationFilter
                // 如果用form表单登录,这里的过滤器就需要注释掉
                .addFilterAt(myAuthenticationFilter(http), UsernamePasswordAuthenticationFilter.class)

                // 构建 HttpSecurity
                .build();
    }

}
MyAuthenticationFilter
public class MyAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        Map<String, String> loginInfo;
        try {
            loginInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        String username = loginInfo.get(getUsernameParameter());// 用来接收用户名
        String password = loginInfo.get(getPasswordParameter());// 用来接收密码
        String code = loginInfo.get("code");// 用来接收验证码

        // 获取记住我 值
        String rememberValue = loginInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER);
        if (!ObjectUtils.isEmpty(rememberValue)) {
            request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER, rememberValue);
        }

        if (StringUtils.isEmpty(code))
            throw new BadCredentialsException("验证码不能为空 !");
        if (!"123".equalsIgnoreCase(code))
            throw new BadCredentialsException("验证码错误 !");

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

MyRememberMeServices
/**
 * 自定义记住我 services 实现类
 */
public class MyRememberMeServices extends PersistentTokenBasedRememberMeServices {

    public MyRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService, tokenRepository);
    }

    /**
     * 自定义前后端分离获取 remember-me 方式
     */
    @Override
    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        Object paramValue = request.getAttribute(parameter);
        if (paramValue != null) {
            String paramValue2 = paramValue.toString();
            return paramValue2.equalsIgnoreCase("true") || paramValue2.equalsIgnoreCase("on")
                    || paramValue2.equalsIgnoreCase("yes") || paramValue2.equals("1");
        }
        return false;
    }

}
MyUserDetails
public class MyUserDetails implements UserDetails {

    private final String uname;
    private final String passwd;

    public MyUserDetails(String uname, String passwd) {
        this.uname = uname;
        this.passwd = passwd;
    }

    // 这里是设置权限的,需要使用的话,你自定义吧
    // 也就是说,在MyUserDetailsService中从数据库里获取,然后调用set方法即可
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return AuthorityUtils.createAuthorityList();
    }

    @Override
    public String getPassword() {
        return passwd;
    }

    @Override
    public String getUsername() {
        return uname;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

MyUserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {

    // 这里是获取数据库里的数据,你自定义把
    @Resource
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. find user
        List<UserEntity> users = userRepository.findByName(username);
        if (CollectionUtils.isEmpty(users)) throw new UsernameNotFoundException("用户不存在");

        UserEntity user = users.get(0);

        return new MyUserDetails(user.getName(), user.getPasswd());
    }
}

handler
MyAccessDeniedHandler
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.getWriter().write("无权访问!");
    }
}
MyAuthEntryPointHandler
public class MyAuthEntryPointHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        response.getWriter().println("必须认证之后才能访问!");
    }
}
MyAuthFailureHandler
public class MyAuthFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "登录失败: " + exception.getMessage()); // 用户名或密码错误
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}

MyAuthSuccessHandler
public class MyAuthSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "登录成功");
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}
MyLogoutSuccessHandler
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "注销成功");
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}

MySessionExpiredHandler
public class MySessionExpiredHandler implements SessionInformationExpiredStrategy {
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        HttpServletResponse response = event.getResponse();
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "当前会话已经失效,请重新登录!");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
        response.flushBuffer();
    }
}


全部评论: 0

    我有话说: