因为HIS登录认证采用了 Spring Security , 因此, 要想理解目前的HIS登录流程,需要对 Spring Security 有一定了解,才能在其基础上添加新功能和额外扩展。
Spring Security 登录流程解析:
一句话来总结Spring Security 的认证流程就是: 验证信息 (Authentication) 在 过滤器链中传播认证的过程。
Spring Security 的核心组件易于理解,其基本校验流程是: 验证信息(Authentication)传递过来,验证通过,将验证信息存储到 SecurityContext 中;验证失败,做出相应的处理。
Spring Security 核心设计
Spring Security 有五个核心组件:SecurityContext、SecurityContextHolder、Authentication、Userdetails 和 AuthenticationManager。下面分别介绍一下各个组件。
- SecurityContext
SecurityContext 即安全上下文,关联当前用户的安全信息。用户通过 Spring Security 的校验之后,SecurityContext 会存储验证信息,下文提到的 Authentication 对象包含当前用户的身份信息。
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
SecurityContext 存储在 SecurityContextHolder 中。
- SecurityContextHolder
SecurityContextHolder 存储 SecurityContext 对象。SecurityContextHolder 是一个存储代理,SecurityContextHolder 默认使用 MODE_THREADLOCAL 模式,SecurityContext 存储在当前线程中。
-
Authentication
Authentication 即验证,表明当前用户是谁。什么是验证,比如一组用户名和密码就是验证,当然错误的用户名和密码也是验证,只不过 Spring Security 会校验失败。
public interface Authentication extends Principal, Serializable { // 获取用户权限,一般情况下获取到的是用户的角色信息。 Collection<? extends GrantedAuthority> getAuthorities(); // 获取证明用户认证的信息,通常情况下获取到的是密码等信息。 Object getCredentials(); // 获取用户的额外信息,比如 IP 地址、经纬度等。 Object getDetails(); // 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (暂时理解为,当前应用用户对象的扩展)。 Object getPrincipal(); // 获取当前 Authentication 是否已认证。 boolean isAuthenticated(); // 设置当前 Authentication 是否已认证。 void setAuthenticated(boolean isAuthenticated); }
在验证前,principal 填充的是用户名,credentials 填充的是密码,detail 填充的是用户的 IP 或者经纬度之类的信息。通过验证后,Spring Security 对 Authentication 重新注入,principal 填充用户信息(包含用户名、年龄等), authorities 会填充用户的角色信息,authenticated 会被设置为 true。重新注入的 Authentication 会被填充到 SecurityContext 中。
-
UserDetails
UserDetails 提供 Spring Security 需要的用户核心信息。
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
Spring Security 在 Web 中的设计
默认 Spring Security 在 Web 中的认证流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LG417wUO-1613872141227)(/tfl/captures/2020-02/tapd_44641644_base64_1581473824_51.png)]
Spring Security 在 Web 中的入口是 Filter。
Spring Security 在 Filter 中创建 Authentication 对象,并调用 AuthenticationManager 进行校验。Spring Security 选择 Filter,而没有采用 Controller 的方式有以下优点。Spring Security 依赖 J2EE 标准,无需依赖特定的 MVC 框架。另一方面 Spring MVC 通过 Servlet 做请求转发,如果 Spring Security 采用 Servlet,那么 Spring Security 和 Spring MVC 的集成会存在问题。FilterChain 维护了很多 Filter,每个 Filter 都有自己的功能,因此在 Spring Security 中添加新功能时,推荐通过 Filter 的方式来实现。
下面是在 Spring Security 默认的web认证流程中起关键作用的 Filter。
- UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
-
AbstractAuthenticationProcessingFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } Authentication authResult; try { // 调用 UsernamePasswordAuthenticationFilter.attemptAuthentication 方法 authResult = attemptAuthentication(request, response); if (authResult == null) { return; } // 认证成功保存到session sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // Authentication failed unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } // 认证成功,把 Authentication 保存到 SecurityContext 中,并调用认证成功处理器 // AuthenticationSuccessHandler successfulAuthentication(request, response, chain, authResult); }
以上就是 Spring Security 在 Web 环境中对于用户名密码校验的整个流程,简言之:
- UsernamePasswordAuthenticationFilter 接受用户名密码登录请求,将 Authentication 传递给 ProviderManager 进行校验。
- ProviderManager 将校验任务代理给 DaoAuthenticationProvider。
- DaoAuthenticationProvider 对 Authentication 的用户名和密码进行校验,校验通过后返回重新注入的 Authentication 对象。
- 认证成功后,UsernamePasswordAuthenticationFilter 的父类 AbstractAuthenticationProcessingFilter 将重新注入的 Authentication 对象填充到 SecurityContext 中。
HIS 中 Spring Security 认证流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1SJ0GFmk-1613872141230)(/tfl/captures/2020-02/tapd_44641644_base64_1581473839_50.png)]
HIS 对 Spring Security 做了一些扩展,用来支持 三级等保的认证逻辑。
在此基础上,HIS 要接入 SSO登陆, 有以下两种接入方式:
HIS 接入 SSO 认证
由于目前HIS 有自己的权限,SSO 登陆HIS只需要拿到 normdy 的用户id,找到在中控绑定的HIS用户即可。
SSO 登陆 API
URL: /login/sso
方式一:
参考登录流程的 SecurityContextPersistenceFilter , 以及 HttpSessionSecurityContextRepository 。
直接往Session 写入 SecurityContext, SecurityContext 持有 已认证的 Authentication.
流程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5RQJnIzu-1613872141231)(/tfl/captures/2020-02/tapd_44641644_base64_1581579840_25.png)]
当SSO登陆成功后,若要登陆HIS需要做以下几件事情:
- 根据
NormandyUserId
找到 绑定 的HIS用户 ,获取其手机号。 OAuth2Authentication
转UsernamePasswordAuthenticationToken
并设置 SecurityContext 上下文。- session 中设置 Authentication 标识,和 当前绑定HIS用户的权限信息。