注:本文基于
Spring Boot 3.2.1
以及Spring Security 6.2.1
相关文章
【SpringBoot3】Spring Security 核心概念
【SpringBoot3】Spring Security 常用注解
【SpringBoot3】Spring Security 详细使用实例(简单使用、JWT模式)
【SpringBoot3】Spring Security使用mybatis-plus存储用户角色权限,实现动态权限处理
一、Spring Security 常用配置总结
1、记住我 rememberMe
1)配置HttpSecurity,如下代码
// 开启rememberMe,登录表单增加 <input type="checkbox" name="remember-me">
http.rememberMe(Customizer.withDefaults());
2)登录表单页面增加勾选参数,在登录时勾选即可
注意参数名
remember-me
是默认的,也可以通过配置修改
<input type="checkbox" name="remember-me">
登录成功后,会生成一个cookie(remember-me)
3)配置rememberMe其他参数
- rememberMeParameter,表单参数名称
- rememberMeCookieName,记录在浏览器的cookieName
- tokenValiditySeconds,有效时间,单位秒
http.rememberMe(rm -> rm
.rememberMeParameter("rememberMe")
.rememberMeCookieName("rememberMeCookieName")
.key("rememberMe")
.tokenValiditySeconds(1800));
2、退出处理
// 退出时,设置session无效
http.logout(logout -> logout.invalidateHttpSession(true));
3、持久化登录令牌
持久化登录令牌是记住我功能的补充,可以将登录的token存储在数据库中。
参考 JdbcTokenRepositoryImpl
可以得知需要创建一张表 persistent_logins
本例使用 mybatis-plus + mysql 存储
1)创建持久化令牌表 persistent_logins
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)
2)创建实体和Mapper
@Data
@TableName("persistent_logins")
public class UserToken {
private String username;
private String series;
@TableField("token")
private String tokenValue;
@TableField("last_used")
private Date date;
}
@Mapper
public interface UserTokenMapper extends BaseMapper<UserToken> {
}
3)创建自定义持久化类 MyPersistentTokenRepositoryImpl
实现接口 PersistentTokenRepository
@Service
public class MyPersistentTokenRepositoryImpl implements PersistentTokenRepository {
@Resource
private UserTokenMapper userTokenMapper;
@Override
public void createNewToken(PersistentRememberMeToken token) {
UserToken userToken = BeanUtil.toBean(token, UserToken.class);
userTokenMapper.insert(userToken);
}
@Override
public void updateToken(String series, String tokenValue, Date lastUsed) {
LambdaUpdateWrapper<UserToken> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.eq(UserToken::getSeries, series)
.set(UserToken::getTokenValue, tokenValue)
.set(UserToken::getDate, lastUsed);
userTokenMapper.update(updateWrapper);
}
@Override
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
UserToken userToken = userTokenMapper.selectOne(new LambdaQueryWrapper<UserToken>().eq(UserToken::getSeries, seriesId));
PersistentRememberMeToken meToken = BeanUtil.toBean(userToken, PersistentRememberMeToken.class);
return meToken;
}
@Override
public void removeUserTokens(String username) {
userTokenMapper.delete(new LambdaQueryWrapper<UserToken>().eq(UserToken::getUsername, username));
}
}
4)在 HttpSecurity 中配置使用
@Resource
private MyPersistentTokenRepositoryImpl tokenRepository;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// --- 忽略其他代码 --- //
http.rememberMe(rm -> rm
.rememberMeParameter("rememberMe")
.rememberMeCookieName("rememberMeCookieName")
.key("rememberMe")
// 配置持久化类
.tokenRepository(tokenRepository)
.tokenValiditySeconds(1800));
// --- 忽略其他代码 --- //
}
5)登录,勾选记住我
打开数据库查看,存储正常
4、并发登录控制,后登录踢掉前面登录
设置每个账户最大的session数量
// 每个账户最大的session数量
http.sessionManagement(sm->sm.maximumSessions(1));
5、主动踢人下线
1)配置 SessionRegistry
/**
* 通过 SessionRegistry 可以获取到当前登录的所有用户
* @return
*/
@Bean
public SessionRegistry sessionRegistry(){
return new SessionRegistryImpl();
}
在 HttpSecurity 中配置
// 每个账户最大的session数量,配置 sessionRegistry
http.sessionManagement(sm->sm.maximumSessions(1).sessionRegistry(sessionRegistry()));
2)在controller中获取在线用户,并展示
@GetMapping("/index")
public String index(Model model) {
// 获取所有登录过的用户
List<Object> allPrincipals = sessionRegistry.getAllPrincipals();
// 移除不存在session的用户
List<Object> list = allPrincipals.stream().filter(p -> {
List<SessionInformation> allSessions = sessionRegistry.getAllSessions(p, false);
return !allSessions.isEmpty();
}).toList();
model.addAttribute("users", list);
return "index";
}
在 index.html中显示
<table>
<tr>
<th>用户名</th>
<th>操作</th>
</tr>
<tr th:each="user:${users}">
<td th:text="${user.username}"></td>
<td><a th:href="@{/kickout(username=${user.username})}">下线</a></td>
</tr>
</table>
3)增加踢人接口
@GetMapping("/kickout")
public String kickout(String username) {
List<Object> allPrincipals = sessionRegistry.getAllPrincipals();
for (Object principal : allPrincipals) {
List<SessionInformation> allSessions = sessionRegistry.getAllSessions(principal, false);
User user = (User) principal;
if (user.getUsername().equals(username)) {
//将所有已登录的 session 都失效
allSessions.forEach(SessionInformation::expireNow);
}
}
return "redirect:/index";
}
6、允许跨域处理
什么是跨域
CORS跨域,全称是“跨域资源共享”(Cross-Origin Resource Sharing),是一种基于HTTP标头的机制,它允许服务器指示除其自身之外的任何来源(域、方案或端口),浏览器应允许从中加载资源。这种机制允许浏览器向跨源服务器发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
当浏览器从一个源(域)向另一个源(域)发出请求时,由于安全原因,浏览器会限制这种跨源请求。但是,CORS通过服务器和浏览器的协商,允许某些跨源请求得以通过。具体来说,浏览器会先向服务器发送一个“预检”请求,检查服务器是否允许实际请求。如果服务器允许,那么浏览器就会发送实际的跨域请求。
CORS需要浏览器和服务器同时支持,目前几乎所有的浏览器都支持CORS,但IE浏览器不能低于IE10版本。实现CORS的关键在于服务器,只要服务器实现了CORS接口,就可以实现跨域通信。
Spring Security 配置允许跨域
配置 HttpSecurity
// 开启跨域
http.cors(Customizer.withDefaults())
7、跨域攻击防护
什么是CSRF
CSRF,全称是跨站请求攻击(Cross-Site Request Forgery),是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。简单来说,攻击者通过一些技术手段欺骗用户的浏览器去访问一个用户之前已经认证过的站点,并运行一些操作,如发邮件、发消息,甚至进行财产操作(如转账和购买商品)等。由于浏览器之前已经认证过,被访问的站点会误以为是真正的用户操作而去运行。这种攻击利用了Web中用户身份认证验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。因此,CSRF攻击可以被理解为攻击者盗用了用户的身份,以用户的名义发送恶意请求。
为了防范CSRF攻击,Web应用程序可以采取一些措施,如使用验证码、检查请求来源、使用CSRF令牌等。这些措施可以增加攻击的难度,降低攻击成功的可能性。同时,用户也应该保持警惕,不要随意点击不明链接或下载未知来源的文件,以保护自己的隐私和财产安全。
Spring Security 处理 CSRF
默认情况下,Spring Security 会自动启用 CSRF 保护,这包括在表单中包含 CSRF 令牌(token)并在处理请求时验证这个令牌。
// 开启csrf 保护
http.csrf(Customizer.withDefaults());
在登录表单中,会自动生成参数_csrf
,如下所示:
参考
- https://docs.spring.io/spring-security/reference/index.html