身份认证
1. 用户认证信息
用户认证信息包含了用户的身份信息,用户的凭证信息和用户被授予的权限,在SpringSecurity中用Authentication表示,里面分别对应包含了:用户的Principal、Credential和Authority信息。在Spring Security框架中,在代码的任何地方要拿到当前的用户信息,涉及的类有SecurityContextHolder、SecurityContext、Authentication、Principal和Credential, 他们都是与身份验证和授权相关的重要概念。它们之间的关系如下:
- SecurityContextHolder:SecurityContextHolder 是Spring Security存储已认证用户详细信息的地方。
- SecurityContext:SecurityContext 是从SecurityContextHolder获取的内容,包含当前已认证用户的Authentication信息。
- Authentication:Authentication 表示用户的身份认证信息。它包含了用户的Principal、Credential和Authority信息。
- Principal:表示用户的身份标识。它通常是一个表示用户的实体对象,例如用户名。Principal可以通过Authentication对象的
getPrincipal()
方法获取。 - Credentials:表示用户的凭证信息,例如密码、证书或其他认证凭据。Credential可以通过Authentication对象的
getCredentials()
方法获取。 - GrantedAuthority:表示用户被授予的权限
总结起来,SecurityContextHolder用于管理当前线程的安全上下文,存储已认证用户的详细信息,其中包含了SecurityContext对象,该对象包含了Authentication对象,后者表示用户的身份验证信息,包括Principal(用户的身份标识)和Credential(用户的凭证信息)。
2. Controller中获取用户信息
修改IndexController,访问根路径不再需要返回首页,使用@RestController修饰
@RestController
public class IndexController {
@GetMapping("/")
public Map<String, Object> index(){
SecurityContext context = SecurityContextHolder.getContext();//存储认证对象的上下文
Authentication authentication = context.getAuthentication();//认证对象
String username = authentication.getName();//用户名
Object principal =authentication.getPrincipal();//身份
Object credentials = authentication.getCredentials();//凭证(脱敏)
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();//权限
//创建结果对象
HashMap result = new HashMap();
result.put("code", 0);
result.put("data", username);
return result;
}
}
3. 会话并发处理
默认策略是后登录的账号会使先登录的账号失效,这时需要提醒用户。
3.1 实现会话过期处理器接口
需要实现SessionInformationExpiredStrategy接口:
public class ResponseJsonSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
private HttpMessageConverter<Object> converter;
public ResponseJsonSessionInformationExpiredStrategy(HttpMessageConverter<Object> converter) {
this.converter = converter;
}
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
HashMap<String, Object> result = new HashMap<>();
result.put("code", -1);
this.converter.write(result, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(event.getResponse()));
}
}
配置WebSecurityConfig:
// 配置session管理
http.sessionManagement(session -> {
session
.maximumSessions(1) // 最大会话数
.expiredSessionStrategy(new ResponseJsonSessionInformationExpiredStrategy(converter))
;
});
3.2 阻止后面相同用户登录
可以修改策略配置,设置阻止相同用户再次在其他设备登录,此时我们配置ResponseJsonSessionInformationExpiredStrategy不再起作用,因为SessionInformationExpiredStrategy接口用来处理已经登录阶段同时会话过期的策略。需要调整登录失败的ResponseJsonAuthenticationFailureHandler类:
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//获取错误信息
String localizedMessage = exception.getLocalizedMessage();
HashMap<String, Object> result = new HashMap<>();
result.put("code", -1);
// 判断属于会话限制导致的登录失败
if(localizedMessage.startsWith("Maximum sessions of")){
result.put("message", "用户已经登录,请先退出!");
}else{
result.put("message", localizedMessage);
}
this.converter.write(result, MediaType.APPLICATION_JSON_UTF8, new ServletServerHttpResponse(response));
}
配置WebSecurityConfig:
// 配置session管理
http.sessionManagement(session -> {
session
.maximumSessions(1) // 最大会话数
.maxSessionsPreventsLogin(true) // 阻止新的登录
.expiredSessionStrategy(new ResponseJsonSessionInformationExpiredStrategy(converter))
;
});
在ResponseJsonAuthenticationFailureHandler中是通过判断错误信息字符串来判断重复登录的,而这些错误信息底层是通过ConcurrentSessionControlAuthenticationStrategy类的调用allowableSessionsExceeded()
产生的: