Skip to content

身份认证

1. 用户认证信息

用户认证信息包含了用户的身份信息,用户的凭证信息和用户被授予的权限,在SpringSecurity中用Authentication表示,里面分别对应包含了:用户的Principal、Credential和Authority信息。在Spring Security框架中,在代码的任何地方要拿到当前的用户信息,涉及的类有SecurityContextHolder、SecurityContext、Authentication、Principal和Credential, 他们都是与身份验证和授权相关的重要概念。它们之间的关系如下: Alt text

  1. SecurityContextHolder:SecurityContextHolder 是Spring Security存储已认证用户详细信息的地方。
  2. SecurityContext:SecurityContext 是从SecurityContextHolder获取的内容,包含当前已认证用户的Authentication信息。
  3. Authentication:Authentication 表示用户的身份认证信息。它包含了用户的Principal、Credential和Authority信息。
  4. Principal:表示用户的身份标识。它通常是一个表示用户的实体对象,例如用户名。Principal可以通过Authentication对象的getPrincipal()方法获取。
  5. Credentials:表示用户的凭证信息,例如密码、证书或其他认证凭据。Credential可以通过Authentication对象的getCredentials()方法获取。
  6. GrantedAuthority:表示用户被授予的权限

总结起来,SecurityContextHolder用于管理当前线程的安全上下文,存储已认证用户的详细信息,其中包含了SecurityContext对象,该对象包含了Authentication对象,后者表示用户的身份验证信息,包括Principal(用户的身份标识)和Credential(用户的凭证信息)。

2. Controller中获取用户信息

修改IndexController,访问根路径不再需要返回首页,使用@RestController修饰

java
@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接口:

java
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:

java
// 配置session管理
http.sessionManagement(session -> {
    session
            .maximumSessions(1) // 最大会话数
            .expiredSessionStrategy(new ResponseJsonSessionInformationExpiredStrategy(converter))
    ;
});

3.2 阻止后面相同用户登录

可以修改策略配置,设置阻止相同用户再次在其他设备登录,此时我们配置ResponseJsonSessionInformationExpiredStrategy不再起作用,因为SessionInformationExpiredStrategy接口用来处理已经登录阶段同时会话过期的策略。需要调整登录失败的ResponseJsonAuthenticationFailureHandler类:

java
@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:

java
// 配置session管理
http.sessionManagement(session -> {
    session
            .maximumSessions(1) // 最大会话数
            .maxSessionsPreventsLogin(true) // 阻止新的登录
            .expiredSessionStrategy(new ResponseJsonSessionInformationExpiredStrategy(converter))
    ;
});

在ResponseJsonAuthenticationFailureHandler中是通过判断错误信息字符串来判断重复登录的,而这些错误信息底层是通过ConcurrentSessionControlAuthenticationStrategy类的调用allowableSessionsExceeded()产生的:
Alt text