简介
根据最近一段时间的设计以及摸索,对SpringSecurity进行总结,目前security采用的是5.7+版本,和以前的版本最大的差别就是,以前创建SecurityConfig需要继承WebSecurityConfigurerAdapter,而到了5.7以后,并不推荐这种做法,查了网上一些教程,其实并不好,绝大多数用的都是老版本,所以出此文案。
一些原理什么的,就不过多说明了,一般搜索资料的,其实根本不想你说什么原理 T·T。
写法区别
最大的区别就是不需要继承WebSecurityConfigurerAdapter(官方也开始弃用此方法),所有配置不需要用and()方法链接,采用lambda处理,个人觉得lambda写法更加的美观,可阅读性更高
这是以前的写法
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {@Overrideprotected 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{@Beanpublic 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漏洞保护、跨域处理、异常处理 等核心模块,授权以后再说
目录结构
核心代码
主配置SecurityConfig
@Configuration
public class SecurityConfig<S extends Session> {// 基于数据库验证,自定义实现UserDetailService@ResourceMyUserDetailsService myUserDetailsService;// 注入自定义认证失败处理器@BeanMyAuthFailureHandler myAuthFailureHandler() {return new MyAuthFailureHandler();}// 注入数据源@ResourceDataSource dataSource;// 注入自定义认证成功处理器@BeanMyAuthSuccessHandler myAuthSuccessHandler() {return new MyAuthSuccessHandler();}// 注入自定义注销登录处理器@BeanMyLogoutSuccessHandler myLogoutSuccessHandler() {return new MyLogoutSuccessHandler();}// 注入自定义未认证访问处理器@BeanMyAuthEntryPointHandler myAuthEntryPointHandler() {return new MyAuthEntryPointHandler();}// 注入自定义session会话管理处理器@BeanMySessionExpiredHandler mySessionExpiredHandler() {return new MySessionExpiredHandler();}// 注入自定义未授权访问处理器@BeanMyAccessDeniedHandler myAccessDeniedHandler() {return new MyAccessDeniedHandler();}// 注入redis-session管理@Resourceprivate FindByIndexNameSessionRepository<S> sessionRepository;// 注入redis-session管理@Beanpublic SpringSessionBackedSessionRegistry<S> sessionRegistry() {return new SpringSessionBackedSessionRegistry<>(sessionRepository);}// 登录urlprivate final String loginUrl = "/login";/*** 配置放行请求*/private String[] loadExcludePath() {return new String[]{"/pm", loginUrl, "/error"};}/*** 配置密码加密规则*/@Beanpublic BCryptPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 跨域配置*/@BeanCorsConfigurationSource 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;}/*** 记住我 令牌 持久化存储*/@Beanpublic 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,后续都要设置为falsejdbcTokenRepository.setCreateTableOnStartup(false);jdbcTokenRepository.setDataSource(dataSource);return jdbcTokenRepository;}/*** 记住我 service 注入*/@Beanpublic RememberMeServices rememberMeServices() {return new MyRememberMeServices(UUID.randomUUID().toString(), myUserDetailsService, persistentTokenRepository());}/*** 构建认证管理器*/@BeanAuthenticationManager authenticationManager(HttpSecurity httpSecurity) throws Exception {// 开启自定义userDetail,开启密码加密return httpSecurity.getSharedObject(AuthenticationManagerBuilder.class).userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder()).and().build();}/*** 自定义认证过滤器*/@Beanpublic 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;}/*** 安全认证过滤器链*/@Beanpublic 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 {@Overridepublic 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 方式*/@Overrideprotected 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方法即可@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return AuthorityUtils.createAuthorityList();}@Overridepublic String getPassword() {return passwd;}@Overridepublic String getUsername() {return uname;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
MyUserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {// 这里是获取数据库里的数据,你自定义把@ResourceUserRepository userRepository;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 1. find userList<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 {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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();}
}