ITPub博客

首页 > 数据库 > 数据库开发技术 > SpringSecurity5.7+最新案例 -- 用户名密码+验证码+记住我······

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

数据库开发技术 作者:TechSpartan 时间:2023-08-03 18:06:08 0 删除 编辑

## 简介

根据最近一段时间的设计以及摸索,对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漏洞保护、跨域处理、异常处理 等核心模块,授权以后再说

#### 目录结构

0900086000300134184.20201216095126.86523331460016843504112994983392.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();

    }

}

原文作者:;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2019-08-14

  • 博文量
    8
  • 访问量
    500