简介
根据最近一段时间的设计以及摸索,对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漏洞保护、跨域处理、异常处理 等核心模块,授权以后再说
目录结构
核心代码
主配置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();
}
}