除了spring security的基础的用法,我们还有一些他的拓展功能,这篇文章会给大家介绍一下。Spring security有哪些拓展功能,并且怎么去使用它才能发挥spring security的全部形态。
开启记住我功能
相信大家在网上冲浪的时候,肯定见过这个,记住我的功能。例如我们拿这次的网易邮箱来举例,大家在登录邮箱的时候,会发现这里有一个30天内免登录,它其实的原理就是一个记住我的功能。那么记住我选了之后呢,也就是说下次我们再进入浏览器,进入这个网页之后。我们就可以不输入用户名和密码,直接进入这个系统的登录页面。 。
那么spring security是默认是没有记住我这个功能的,我们仅仅需要一行代码就可以让security实现这个功能。
会话管理
挤出另外一个用户
大家看到这个标题可能有些不理解,但是我说一个场景大家就可以理解了。例如我们在使用QQ的时候,如果另外一个人从另外一台手机上登录QQ,我们这台的QQ就会被挤下线。在spring security中,这就叫会话管理,我们可以通过如下配置进行一个实现。
cpp
package com.masiyi.springsecuritydemo.config;
import com.masiyi.springsecuritydemo.handler.MyAuthenticationFailureHandler;
import com.masiyi.springsecuritydemo.handler.MyAuthenticationSuccessHandler;
import com.masiyi.springsecuritydemo.handler.MyLogoutSuccessHandler;
import com.masiyi.springsecuritydemo.service.MyUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.session.HttpSessionEventPublisher;
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
/**
* 这里有两种方式 authorizeHttpRequests 和 authorizeRequests
* @param http the {@link HttpSecurity} to modify
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/user/register").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(new MyAuthenticationFailureHandler())
.and()
.logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
.and().csrf().disable()
.rememberMe() //开启记住我功能
.and()
.sessionManagement() //开启会话管理
.maximumSessions(1); //设置会话并发数为 1 在SessionRegistryImpl中管理key,key存的是对象,必须要重写User的equals和hashcode方法才能判断用户相等
;
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public UserDetailsService userDetailsService() {
return new MyUserDetailService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
/**
* 指定加密方式
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCrypt加密密码
return new BCryptPasswordEncoder();
}
}
其中这段代码就是核心:
java
.and()
.sessionManagement() //开启会话管理
.maximumSessions(1); //设置会话并发数为 1 在SessionRegistryImpl中管理key,key存的是对象,必须要重写User的equals和hashcode方法才能判断用户相等
key存的是对象,必须要重写User的equals和hashcode方法才能判断用户相等,否则的话,spring security就认为不是同一个用户。至于为什么要重启hashcode跟equal方法,其实这就是JAVA基础的问题了,因为只有重写了这两个方法,然后这里面的属性如果相等,那么就证明这个引用对象是相等的。。
java
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
private List<Role> roles = new ArrayList<>();
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
写完了这些配置之后,我们可以用两个浏览器模拟两个session。一个模拟器正常登陆之后,另外一个模拟器也正常登陆,这个时候我们返回第一个浏览器,刷新一下,就会发现第一个浏览器已经被挤出去了。
用redis存储session
但是我们刚刚写的东西它是基于内存的。那如果说我们的应用在发布的时候会重启服务,重启服务之后,那么内存里面的东西全部会清空。那么我们肯定会想办法把它持久化到一个数据库中,所以说现在数据库我们就选择用redis。而且市面上最常见的解决方案也是用redis去存储这个会话。
如果我们要实现这个功能,第一步先在pom文件中引入两个jar包。
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
之后我们在properties文件中设置redis的连接地址。
java
spring.redis.host=192.168.0.73
spring.redis.port=30197
spring.redis.password=E(cSuYdp76
spring.redis.database=2
最后我们只需要在配置类中按照如下配置即可。
java
package com.masiyi.springsecuritydemo.config;
import com.masiyi.springsecuritydemo.handler.MyAuthenticationFailureHandler;
import com.masiyi.springsecuritydemo.handler.MyAuthenticationSuccessHandler;
import com.masiyi.springsecuritydemo.handler.MyLogoutSuccessHandler;
import com.masiyi.springsecuritydemo.service.MyUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
private FindByIndexNameSessionRepository sessionRepository;
@Autowired
@Lazy
public void SecurityConfig(FindByIndexNameSessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
/**
* 这里有两种方式 authorizeHttpRequests 和 authorizeRequests
* @param http the {@link HttpSecurity} to modify
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/user/register").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(new MyAuthenticationFailureHandler())
.and()
.logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
.and().csrf().disable()
.rememberMe() //开启记住我功能
.and()
.sessionManagement() //开启会话管理
.maximumSessions(1); //设置会话并发数为 1 在SessionRegistryImpl中管理key,key存的是对象,必须要重写User的equals和hashcode方法才能判断用户相等
;
}
@Bean
public SpringSessionBackedSessionRegistry sessionRegistry() {
return new SpringSessionBackedSessionRegistry(sessionRepository);
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public UserDetailsService userDetailsService() {
return new MyUserDetailService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
/**
* 指定加密方式
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCrypt加密密码
return new BCryptPasswordEncoder();
}
}
开启 CSRF
CSRF(Cross-Site Request Forgery)跨站请求伪造,是一种网络安全攻击方式,攻击者通过伪造用户的请求,利用用户在其他网站已经登录的身份来执行非法操作。攻击者可以在用户不知情的情况下发送恶意请求,例如转账、更改密码等操作,从而造成用户的损失或泄露敏感信息。
举例来说,假设用户在银行网站A上已经登录并保持会话,攻击者在恶意网站B上放置了一个恶意链接。当用户访问恶意网站B时,网站B中的恶意代码会自动向银行网站A发送一个请求,比如转账请求。由于用户在银行网站A上已经登录,该请求会被银行网站A误认为是用户的合法操作,从而执行转账操作,造成损失。
而spring security开启CSRF也非常简单
java
/**
* 这里有两种方式 authorizeHttpRequests 和 authorizeRequests
* @param http the {@link HttpSecurity} to modify
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/user/register").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(new MyAuthenticationFailureHandler())
.and()
.logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.rememberMe() //开启记住我功能
.and()
.sessionManagement() //开启会话管理
.maximumSessions(1); //设置会话并发数为 1 在SessionRegistryImpl中管理key,key存的是对象,必须要重写User的equals和hashcode方法才能判断用户相等
;
}
把前面的disable
换成.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and()
即可
跨域处理方案
跨域(Cross-Origin)指的是在浏览器中,一个网页的脚本试图访问另一个网页的内容时所涉及的安全限制。为了保护用户数据安全,浏览器会限制跨域。我们现在的开发模式是前后端分离。那么这个时候我们就会遇到跨域的问题。为前端的端口跟后端的端口不是一个端口。
常见的跨域处理方案包括:
-
CORS(跨域资源共享):通过在服务器端设置响应头部信息,允许指定的源(域名、协议、端口)访问资源,从而实现跨域请求。可以通过配置Access-Control-Allow-Origin等响应头来控制跨域访问。
-
JSONP(JSON with Padding):利用
<script>
标签的跨域特性,通过动态创建<script>
标签来加载包含回调函数的JSON数据,从而实现跨域数据传输。 -
代理服务器:在服务器端设置代理,将跨域请求转发到目标服务器,然后将响应返回给客户端,绕过浏览器的跨域限制。
-
跨域资源嵌入(Cross-Origin Resource Inclusion,CORS):允许在不同域名下加载资源,如图片、样式表、脚本等,但不允许对资源进行操作。
而spring security中同样已经通过CORS解决了这样的问题。
java
/**
* 这里有两种方式 authorizeHttpRequests 和 authorizeRequests
*
* @param http the {@link HttpSecurity} to modify
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/user/register").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(new MyAuthenticationFailureHandler())
.and()
.logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.cors() //跨域处理方案
.configurationSource(configurationSource())
.and()
.rememberMe() //开启记住我功能
.and()
.sessionManagement() //开启会话管理
.maximumSessions(1) //设置会话并发数为 1 在SessionRegistryImpl中管理key,key存的是对象,必须要重写User的equals和hashcode方法才能判断用户相等
;
}
/**
*允许所有来源的请求(*)使用任意的请求头和请求方法,并且设置了最大缓存时间为 3600 秒。
* @return
*/
CorsConfigurationSource configurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
corsConfiguration.setAllowedMethods(Arrays.asList("*"));
corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
这段代码的作用是允许所有来源("")的请求访问该服务,并设置了允许的请求头和请求方法,以及缓存时间。通过这样的配置,可以解决跨域请求时浏览器的安全限制,实现跨域资源共享。
好了,本篇文章就讲完了,但是spring ecurity的拓展功能远不止如此。关于更多的拓展功能,这里就不在一一做介绍,这里只列出了我们平时工作中和学习中常用的一些拓展功能。
项目的地址就在 gitee.com/WangFuGui-M...
如果大家对这篇文章或者专栏有兴趣或者对大家有所帮助的话,欢迎关注点赞。加评论。 我们spring security的进阶专栏见。