上两篇我们主要讨论了一下从SecurityContextHolder中获取数据以及他的原理,这里我们来看一下第二种方法获取请登录数据,从当前请求种对象种获取。
从请求对象中获取
我们首先来看一下获取登录数据的代码,如下:
@GetMapping("/authentication")
public void authentication(Authentication authentication){
System.out.println("authentication = "+ authentication);
}
@GetMapping("/principal")
public void principal(Principal principal){
System.out.println("principal = " + principal);
}
获取代码如上,我们可以直接在Controller的请求参数中放入Authenticaion对象来获取登录用户信息,通过前面文章的分析,我们已经知道Authentication是Principal的子类,所以也可以直接在请求参数中放入Principal来接收当前登录用户信息。需要注意的是,即使参数是Principal,真正的实例依然是Authentication的实例。
用过MVC的都知道,Controller中方法的参数都是当前请求HttpServletRequest带来的,毫无疑问,前面的Authenticaion和Principal参数也都是HttpServletRequest带来的,那么这里就存在两个问题了。
(1)、这些登录用户数据是何时放入HttpServletRequest的?
(2)、这些数据又是以何种形式存在的?
我们来看看:
在Servlet规范中,最早由三个和安全管理相关的方法:
//1.用来获取登录用户名
public String getRemoteUser();
//2.用来判断当前登录用户是否具备某一个指定的角色
public boolean isUserInRole(String role);
//3.用来获取当前认证主体
public java.security.Principal getUserPrincipal()
从Servlet3.0开始,又在这三个方法的基础上,又增加了三个和安全管理相关的方法
//1.判断当前请求是否认证成功。
public boolean authenticate(HttpServletResponse response) throws IOException,ServletException;
//2.可以执行登录操作
public void login(String username,String password) throws ServletException;
//3.可以执行注销操作
public void logout() throws ServletException;
不过HttpServletRequest只是一个接口,这些安全认证相关的方法,在不同环境下会有不同的实现。
如果是一个普通的Web项目,不使用任何框架,HttpServletRequest的默认实现类是Tomcat中的RequestFacade,从这个类的名字上就可以看出来,这是一个用了Facade模式(外观模式)的类,真正提供底层服务的是Tomcat中的Request对象,只不过这个Request对象在实现Servlet规范的同时,还定义了很多Tomcat内部的方法,为了避免我们直接调用到这些内部方法,这里使用了外观模式。
在Tomcat的Request类中,对上面这些方法都做了实现,基本上都是基于Tomcat提供的Realm来实现的,这种认证方法非常冷门,项目中也很少使用,因此这里不做介绍。
如果使用了SpringSecurity框架,那么我们在Controller参数中拿到的HttpServletRequest实例将是Servlet3SecurityContextHolderAwareRequestWrapper,很明显,这是被SpringSecurity封装过的请求。
我们来看一下Servlet3SecurityContextHolderAwareRequestWrapper的继承关系。
HttpServletRequestWrapper就不用过多介绍了,有兴趣的可以看看前几篇文章,SecurityContextHolderAwareRequestWrapper类主要实现了Servlet3.0之前和安全管理相关的三个方法,也就是getRemoteUser()、isUserInRole()以及getUserPrincipal()。Servlet3.0中新增的三个安全管理相关的方法则在Servlet3SecurityContextHolderAwareRequestWrapper类中实现。获取用户登录信息主要和前面三个方法有关,因此我们主要来看一下
SecurityContextHolderAwareRequestWrapper类中相关方法的实现。
public class SecurityContextHolderAwareRequestWrapper extends HttpServletRequestWrapper {
private final AuthenticationTrustResolver trustResolver;
private final String rolePrefix;
public SecurityContextHolderAwareRequestWrapper(HttpServletRequest request, String rolePrefix) {
this(request, new AuthenticationTrustResolverImpl(), rolePrefix);
}
public SecurityContextHolderAwareRequestWrapper(HttpServletRequest request,
AuthenticationTrustResolver trustResolver, String rolePrefix) {
super(request);
Assert.notNull(trustResolver, "trustResolver cannot be null");
this.rolePrefix = rolePrefix;
this.trustResolver = trustResolver;
}
private Authentication getAuthentication() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return (!this.trustResolver.isAnonymous(auth)) ? auth : null;
}
@Override
public String getRemoteUser() {
Authentication auth = getAuthentication();
if ((auth == null) || (auth.getPrincipal() == null)) {
return null;
}
if (auth.getPrincipal() instanceof UserDetails) {
return ((UserDetails) auth.getPrincipal()).getUsername();
}
if (auth instanceof AbstractAuthenticationToken) {
return auth.getName();
}
return auth.getPrincipal().toString();
}
@Override
public Principal getUserPrincipal() {
Authentication auth = getAuthentication();
if ((auth == null) || (auth.getPrincipal() == null)) {
return null;
}
return auth;
}
private boolean isGranted(String role) {
Authentication auth = getAuthentication();
if (this.rolePrefix != null && role != null && !role.startsWith(this.rolePrefix)) {
role = this.rolePrefix + role;
}
if ((auth == null) || (auth.getPrincipal() == null)) {
return false;
}
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
if (authorities == null) {
return false;
}
for (GrantedAuthority grantedAuthority : authorities) {
if (role.equals(grantedAuthority.getAuthority())) {
return true;
}
}
return false;
}
@Override
public boolean isUserInRole(String role) {
return isGranted(role);
}
@Override
public String toString() {
return "SecurityContextHolderAwareRequestWrapper[ " + getRequest() + "]";
}
}
SecurityContextHolderAwareRequestWrapper类其实非常好理解:
(1)、getAuthentication:用来获取当前登录对象Authentication,获取方式就是上一篇说的SecurityContextHolder中获取,如果不是匿名对象就返回,否则返回null
(2)、getRemoteUser:返回了当前登录用户的用户名,如果Authentication对象中存储的Principal是当前登录用户对象,则返回用户,如果Authentication对象中存储的Principal是当前登录用户名,则直接返回即可。
(3)、isGranted:是一个私有方法,作用是判断当前登陆用户是否具备某一个指定的角色。判断的逻辑也很简单,先对传入进来的角色进行预处理,有的情况下可能需要添加ROLE_前缀。然后调用Authentication#getAuthorities方法,获取当前登录用户所具备的所有角色,最后在和传入进来的参数进行比较
(4)、isUserInRole:该方法调用isGranted方法,进而实现判断当前用户是否具备某一个指定角色的功能。
到这里我们应该就能明白,在使用了SpringSecurity之后,我们通过HttpServletRequest就可以获取到很多当前登录用户信息了,如下:
@GetMapping("/info")
public void info(HttpServletRequest request){
String remoteUser = request.getRemoteUser();
Authentication auth = (Authentication) request.getUserPrincipal();
boolean admin = request.isUserInRole("admin");
System.out.println("remoteUser = "+ remoteUser);
System.out.println("auth.getName() = " + auth.getName());
System.out.println("admin = "+ admin);
}
登录成功后,可以看到打印如下
remoteUser = test
auth.getName() = test
admin = true
前面我们直接将Authentication或者Principal写到Controller参数中,实际上就是SpringMVC框架从Servlet3SecurityContextHolderAwareRequestWrapper中提取到的用户信息。
那么SpringSecurity是如何将默认的请求对象转换为了Servlet3SecurityContextHolderAwareRequestWrapper的呢?这里就设计到SpringSecurity过滤器链中另外一个重要的过滤器-SecurityContextHolderAwareRequestFilter。
前面我们提到SpringSecurity过滤器中,有一个SecurityContextHolderAwareRequestFilter过滤器,该过滤器的主要作用就是对HttpServletRequest请求进行在包装,重写HttpServletRequest中和安全管理相关的方法。HttpServletRequest在整个请求过程中会被包装多次,每一次的包装都会给它增添新的功能。例如在经过SecurityContextPersistenceFilter请求时就会对它进行包装。
我们来看一下SecurityContextHolderAwareRequestFilter过滤器:
public class SecurityContextHolderAwareRequestFilter extends GenericFilterBean {
private String rolePrefix = "ROLE_";
private HttpServletRequestFactory requestFactory;
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationManager authenticationManager;
private List<LogoutHandler> logoutHandlers;
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
public void setRolePrefix(String rolePrefix) {
Assert.notNull(rolePrefix, "Role prefix must not be null");
this.rolePrefix = rolePrefix;
updateFactory();
}
public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
this.authenticationEntryPoint = authenticationEntryPoint;
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
public void setLogoutHandlers(List<LogoutHandler> logoutHandlers) {
this.logoutHandlers = logoutHandlers;
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(this.requestFactory.create((HttpServletRequest) req, (HttpServletResponse) res), res);
}
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
updateFactory();
}
private void updateFactory() {
String rolePrefix = this.rolePrefix;
this.requestFactory = createServlet3Factory(rolePrefix);
}
public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
Assert.notNull(trustResolver, "trustResolver cannot be null");
this.trustResolver = trustResolver;
updateFactory();
}
private HttpServletRequestFactory createServlet3Factory(String rolePrefix) {
HttpServlet3RequestFactory factory = new HttpServlet3RequestFactory(rolePrefix);
factory.setTrustResolver(this.trustResolver);
factory.setAuthenticationEntryPoint(this.authenticationEntryPoint);
factory.setAuthenticationManager(this.authenticationManager);
factory.setLogoutHandlers(this.logoutHandlers);
return factory;
}
}
我们看到Filter中最主要的方法doFilter,这里会低矮用requestFactory.create方法对请求重新进行包装,requestFactory就是HttpServletRequestFactory类的实例,他的create方法礼包直接创建了一个Servlet3SecurityContextHolderAwareRequestWrapper实例。
对请求的HttpServletRequest包装之后,接下来在过滤器链中传递的HttpServletRequest对象,它的getRemoteUser()、isUserInRole()以及getUserPrincipal()方法就可以直接使用了。
HttpServletRequest中getUserPrincipal()方法有了返回值之后,最终在SpringMVC的ServletRequestMethodArgumentResolver#resolveArgument(Class<?>,HttpServletRequest)方法中进行默认参数解析,自动解析出Principal对象。我们在Controller中既可以通过Principal来接收参数,也可以通过Authentication对象来接收参数。
对于SpringSecurity中两种获取登录用户的方式以及一些基本的原理我们这里就介绍到这,如果不对还请不吝赐教。