12、密码加密
12.1、不指定具体加密方式,通过DelegatingPasswordEncoder,根据前缀自动选择
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
12.2、指定具体加密方式
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
13、密码自动更新
实现UserDetailsPasswordService接口即可
@Component("userDetailsImpl")
public class UserDetailsImpl implements UserDetailsService, UserDetailsPasswordService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Override
public org.springframework.security.core.userdetails.UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if(user == null){
throw new UsernameNotFoundException("用户名不存在");
}
List<Role> roles = roleMapper.getRoleByUserId(user.getUserId());
user.setRoles(roles);
return user;
}
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
int state = userMapper.updatePassword(newPassword, user.getUsername());
if (state == 1) {
((User) user).setPassword(newPassword);
}
return user;
}
}
14、Remember-Me
14.1、传统web方式
14.1.1、login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form th:action="@{/doLogin}" method="post">
<p>用户名:<label>
<input name="uname" type="text"/>
</label></p>
<p>密码:<label>
<input name="pwd" type="password"/>
</label></p>
<p>记住我:<label>
<input name="remember-me" type="checkbox"/>
</label></p>
<p>
<input type="submit">
</p>
</form>
</body>
</html>
14.1.2、开启rememberMeServices
- 基于内存实现
package com.wanqi.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import java.util.UUID;
@Configuration
@EnableWebSecurity
public class WebSecurityConfigurer {
@Bean
public PasswordEncoder bcryptPasswordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
public UserDetailsService inMemoryUsers(PasswordEncoder encoder) {
// The builder will ensure the passwords are encoded before saving in memory
UserDetails user = User.withUsername("user")
.password(encoder.encode("123"))
.roles("USER")
.build();
UserDetails admin = User.withUsername("admin")
.password(encoder.encode("123"))
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean(name = "rememberMeServices")
public PersistentTokenBasedRememberMeServices rememberMeServices(){
return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), inMemoryUsers(bcryptPasswordEncoder()), new InMemoryTokenRepositoryImpl());
}
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return
//开启权限验证
httpSecurity
.authorizeRequests()
//permitAll直接放行,必须在anyRequest().authenticated()前面
.mvcMatchers("/toLogin").permitAll()
//anyRequest所有请求都需要认证
.anyRequest().authenticated()
.and()
//使用form表单验证
.formLogin(login ->
login
//自定义登陆页面
.loginPage("/toLogin")
//自定义登陆页面后必须指定处理登陆请求的url
.loginProcessingUrl("/doLogin")
// 自定义接收用户名的参数名为uname
.usernameParameter("uname")
//自定义接收密码的参数名为pwd
.passwordParameter("pwd")
// 认证成功后跳转的页面(转发),必须使用POST请求
//.successForwardUrl("/test")
// 证成功后跳转的页面(重定向),必须使用GET请求
//.defaultSuccessUrl("/test")
//不会每次都跳转定义的页面,默认会记录认证拦截的请求,如果是拦截的受限资源会优先跳转到之前被拦截的请求。需要每次都跳转使defaultSuccessUrl("/test",true)
.defaultSuccessUrl("/toLogout",true)
//前后端分离时代自定义认证成功处理
// .successHandler(new MyAuthenticationSuccessHandler())
//前后端分离时代自定义认证失败处理
// .failureHandler(new MyAuthenticationFailureHandler())
.failureUrl("/404")
)
//注销
.logout(logout -> {
logout
//指定默认注销url,默认请求方式GET
// .logoutUrl("/logout")
.logoutRequestMatcher(
//自定义注销url
new OrRequestMatcher(
new AntPathRequestMatcher("/aa", "GET"),
new AntPathRequestMatcher("/bb", "POST")))
//注销成功后跳转页面
.logoutSuccessUrl("/toLogin")
//前后端分离时代自定义注销登录处理器
// .logoutSuccessHandler(new MyLogoutSuccessHandler())
//销毁session,默认为true
.invalidateHttpSession(true)
//清除认证信息,默认为true
.clearAuthentication(true);
})
//指定UserDetailsService来切换认证信息不同的存储方式(数据源)
.userDetailsService(inMemoryUsers(bcryptPasswordEncoder()))
//开启rememberMe
.rememberMe()
//自定义rememberMe参数名
// .rememberMeParameter();
.rememberMeServices(rememberMeServices())
.and()
//禁止csrf跨站请求保护
.csrf().disable()
.build();
}
}
- 持久化,基于mysql实现
@Bean
public PersistentTokenRepository jdbcTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//自动创建表结构,首次启动设置为true
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return
//开启权限验证
httpSecurity
.authorizeRequests()
//permitAll直接放行,必须在anyRequest().authenticated()前面
.mvcMatchers("/toLogin").permitAll()
.mvcMatchers("/404").permitAll()
//anyRequest所有请求都需要认证
.anyRequest().authenticated()
.and()
//使用form表单验证
.formLogin(login ->
login
//自定义登陆页面
.loginPage("/toLogin")
//自定义登陆页面后必须指定处理登陆请求的url
.loginProcessingUrl("/doLogin")
// 自定义接收用户名的参数名为uname
.usernameParameter("uname")
//自定义接收密码的参数名为pwd
.passwordParameter("pwd")
// 认证成功后跳转的页面(转发),必须使用POST请求
//.successForwardUrl("/test")
// 证成功后跳转的页面(重定向),必须使用GET请求
//.defaultSuccessUrl("/test")
//不会每次都跳转定义的页面,默认会记录认证拦截的请求,如果是拦截的受限资源会优先跳转到之前被拦截的请求。需要每次都跳转使defaultSuccessUrl("/test",true)
.defaultSuccessUrl("/toLogout",true)
//前后端分离时代自定义认证成功处理
// .successHandler(new MyAuthenticationSuccessHandler())
//前后端分离时代自定义认证失败处理
// .failureHandler(new MyAuthenticationFailureHandler())
.failureUrl("/404")
)
//注销
.logout(logout -> {
logout
//指定默认注销url,默认请求方式GET
// .logoutUrl("/logout")
.logoutRequestMatcher(
//自定义注销url
new OrRequestMatcher(
new AntPathRequestMatcher("/aa", "GET"),
new AntPathRequestMatcher("/bb", "POST")))
//注销成功后跳转页面
.logoutSuccessUrl("/toLogin")
//前后端分离时代自定义注销登录处理器
// .logoutSuccessHandler(new MyLogoutSuccessHandler())
//销毁session,默认为true
.invalidateHttpSession(true)
//清除认证信息,默认为true
.clearAuthentication(true);
})
//指定UserDetailsService来切换认证信息不同的存储方式(数据源)
.userDetailsService(inMemoryUsers(bcryptPasswordEncoder()))
//开启rememberMe
.rememberMe()
.tokenRepository(jdbcTokenRepository())
.and()
//禁止csrf跨站请求保护
.csrf().disable()
.build();
}
}
14.2、前后端分离
14.2.1、自定义RememberMeServices实现类
package com.wanqi.service;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.servlet.http.HttpServletRequest;
/**
* @Description 自定义RememberMeServices实现类
* @Version 1.0.0
* @Date 2022/9/5
* @Author wandaren
*/
public class RememberMeServices extends PersistentTokenBasedRememberMeServices {
public RememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
super(key, userDetailsService, tokenRepository);
}
@Override
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
String paramValue = request.getAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER).toString();
return paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
|| paramValue.equalsIgnoreCase("yes") || paramValue.equals("1");
}
}
14.2.2、自定义认证方式
package com.wanqi.security.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.util.ObjectUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
/**
* @Description 处理Json方式的登录请求
* @Version 1.0.0
* @Date 2022/8/21
* @Author wandaren
*/
public class JsonLoginFilter extends UsernamePasswordAuthenticationFilter {
public JsonLoginFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
try {
Map<String, String> map = new ObjectMapper().readValue(request.getInputStream(), Map.class);
String username = map.get(getUsernameParameter());
username = (username != null) ? username.trim() : "";
String password = map.get(getPasswordParameter());
password = (password != null) ? password : "";
String rememberMe = map.get(AbstractRememberMeServices.DEFAULT_PARAMETER);
if (!ObjectUtils.isEmpty(rememberMe)) {
request.setAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER, rememberMe);
}
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
System.out.println(username);
System.out.println(password);
setDetails(request, authRequest);
return getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
e.printStackTrace();
}
}
throw new AuthenticationServiceException("Authentication ContentType not supported: " + request.getContentType());
}
}
14.2.3、配置
filter.setRememberMeServices(rememberMeServices());
.rememberMe()
.rememberMeServices(rememberMeServices())
.tokenRepository(jdbcTokenRepository())
package com.wanqi.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wanqi.security.filter.JsonLoginFilter;
import com.wanqi.service.RememberMeServices;
import com.wanqi.service.impl.UserDetailsImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2022/9/5
* @Author wandaren
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
DataSource dataSource;
UserDetailsImpl userDetailsImpl;
@Autowired
public void setUserDetailsImpl(UserDetailsImpl userDetailsImpl) {
this.userDetailsImpl = userDetailsImpl;
}
@Bean
public PasswordEncoder bcryptPasswordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Autowired
public AuthenticationConfiguration authenticationConfiguration;
/**
* 获取AuthenticationManager(认证管理器),登录时认证使用
*
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public JsonLoginFilter jsonLoginFilter() throws Exception {
JsonLoginFilter filter = new JsonLoginFilter(authenticationManager());
//指定认证url
filter.setFilterProcessesUrl("/doLogin");
//指定接收json,用户名key
filter.setUsernameParameter("uname");
//指定接收json,密码key
filter.setPasswordParameter("pwd");
filter.setAuthenticationManager(authenticationManager());
filter.setRememberMeServices(rememberMeServices());
//前后端分离时代自定义认证成功处理
filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
map.put("msg", "登陆成功");
map.put("code", HttpStatus.OK.value());
map.put("authentication", authentication);
String s = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(s);
}
});
//前后端分离时代自定义认证失败处理
filter.setAuthenticationFailureHandler((request, response, exception) -> {
Map<String, Object> map = new HashMap<>();
map.put("msg", exception.getMessage());
map.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
String s = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(s);
});
return filter;
}
@Bean
public PersistentTokenRepository jdbcTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//自动创建表结构,首次启动设置为true
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Bean
public RememberMeServices rememberMeServices(){
return new RememberMeServices(UUID.randomUUID().toString(), userDetailsImpl, jdbcTokenRepository());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.addFilterAt(jsonLoginFilter(), UsernamePasswordAuthenticationFilter.class)
.logout(logout -> {
logout
.logoutRequestMatcher(
//自定义注销url
new OrRequestMatcher(
new AntPathRequestMatcher("/aa", "GET"),
new AntPathRequestMatcher("/bb", "POST")))
//前后端分离时代自定义注销登录处理器
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
map.put("msg", "注销成功");
map.put("code", HttpStatus.OK.value());
map.put("authentication", authentication);
String s = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(s);
}
})
//销毁session,默认为true
.invalidateHttpSession(true)
//清除认证信息,默认为true
.clearAuthentication(true);
})
//指定UserDetailsService来切换认证信息不同的存储方式(数据源)
.userDetailsService(userDetailsImpl)
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> {
// response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setHeader("content-type", "application/json;charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("请先认证后重试!");
})
.and()
.rememberMe()
.rememberMeServices(rememberMeServices())
.tokenRepository(jdbcTokenRepository())
.and()
//禁止csrf跨站请求保护
.csrf().disable()
.build();
}
}
15、会话管理
15.1、配置
package com.wanqi.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.session.HttpSessionEventPublisher;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2022/9/5
* @Author wandaren
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
httpSecurity.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.defaultSuccessUrl("/hello",true)
.and()
.csrf().disable()
.sessionManagement(session -> session
//最多一个会话
.maximumSessions(1)
//true:超过会话上限不再容许登录
// .maxSessionsPreventsLogin(true)
// 会话失效(用户被挤下线后)跳转地址
// .expiredUrl("/login")
// 前后端分离处理方式
.expiredSessionStrategy(new SessionInformationExpiredStrategy() {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
event.getResponse().setHeader("content-type", "application/json;charset=UTF-8");
event.getResponse().setStatus(HttpStatus.UNAUTHORIZED.value());
event.getResponse().getWriter().write("会话超时!");
}
})
);
return httpSecurity.build();
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
15.2、共享会话
15.2.1、引入依赖
<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>
15.2.2、编写配置
- application.yml
spring:
redis:
host: 172.16.156.139
port: 6379
password: qifeng
database: 0
main:
allow-circular-references: true
- RedisSessionConfig
package com.wanqi.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.security.web.session.HttpSessionEventPublisher;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2022/9/5
* @Author wandaren
*/
@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.database}")
private int database;
@Bean
RedisStandaloneConfiguration redisStandaloneConfiguration() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setPassword(password);
redisStandaloneConfiguration.setDatabase(database);
return redisStandaloneConfiguration;
}
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisStandaloneConfiguration());
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
- SecurityConfig
package com.wanqi.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;
import javax.servlet.ServletException;
import java.io.IOException;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2022/9/5
* @Author wandaren
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig<S extends Session> {
@Autowired
private FindByIndexNameSessionRepository<S> sessionRepository;
@Bean
public PasswordEncoder bcryptPasswordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public UserDetailsService inMemoryUsers(PasswordEncoder encoder) {
// The builder will ensure the passwords are encoded before saving in memory
UserDetails user = User.withUsername("user")
.password(encoder.encode("123"))
.roles("USER")
.build();
UserDetails admin = User.withUsername("admin")
.password(encoder.encode("123"))
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
httpSecurity.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.defaultSuccessUrl("/hello",true)
.and()
.csrf().disable()
.sessionManagement(session -> session
//最多一个会话
.maximumSessions(1)
//true超过会话上限不再容许登录
.maxSessionsPreventsLogin(true)
// 被踢下线后跳转地址
// .expiredUrl("/login")
.expiredSessionStrategy(new SessionInformationExpiredStrategy() {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
event.getResponse().setHeader("content-type", "application/json;charset=UTF-8");
event.getResponse().setStatus(HttpStatus.UNAUTHORIZED.value());
event.getResponse().getWriter().write("会话超时!");
}
})
.sessionRegistry(sessionRegistry())
)
.userDetailsService(inMemoryUsers(bcryptPasswordEncoder()))
;
return httpSecurity.build();
}
@Bean
public SpringSessionBackedSessionRegistry<S> sessionRegistry() {
return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
}
}
16、CSRF防御
16.1、传统web开发
- 登录页面加上_csrf
<input type="hidden" name="_csrf" th:value="${_csrf.getToken()}">
- SecurityConfig配置修改
httpSecurity.csrf()
16.2、前后端分离开发
16.2.1、登陆页面不需要令牌
httpSecurity.csrf()
//SpringSecurity处理登陆的默认方法不加令牌
.ignoringAntMatchers("/doLogin")
//将令牌保存到cookie,容许前端获取
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
- 登陆后返回cookie中包含XSRF-TOKEN
- 后续请求在请求头增加X-XSRF-TOKEN,值为登陆cookie中的XSRF-TOKEN值
17、跨越处理
17.1、spring跨越处理
17.1.1、使用@CrossOrigin,可以作用到类上也可以作用到具体方法上
@RestController
public class HelloController {
@RequestMapping("/hello")
@CrossOrigin
public String hello(){
return "hello";
}
}
17.1.2、实现WebMvcConfigurer接口重写addCorsMappings方法
package com.wanqi.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2022/9/6
* @Author wandaren
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//对哪些请求进行跨域处理
registry.addMapping("/**")
.allowCredentials(false)
.allowedHeaders("*")
.allowedMethods("*")
.allowedOrigins("*")
.exposedHeaders("")
.maxAge(3600)
;
}
}
17.1.3、使用CorsFilter
@Bean
FilterRegistrationBean<CorsFilter> corsFilter() {
FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));
corsConfiguration.setAllowedMethods(Collections.singletonList("*"));
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
registrationBean.setFilter(new CorsFilter(source));
//指定Filter执行顺序
registrationBean.setOrder(-1);
return registrationBean;
}
17.2、SpringSecurity跨域处理
17.2.1、 Security配置
@Bean
public CorsConfigurationSource configurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Collections.singletonList("*"));
corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));
corsConfiguration.setAllowedMethods(Collections.singletonList("*"));
corsConfiguration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
httpSecurity.cors()
.configurationSource(configurationSource())
相关文章
SpringSecurity入门(一)
SpringSecurity入门(二)
SpringSecurity入门(四)
未完待续