一个web工程,如果涉及到私人信息或不可公开的资源,则必然要对访问者做过滤和认证,访问者只能获取跟自己相关或有权限访问的信息,这就是我们所熟知的登陆。简单的登陆通常是这样实现的:提供一个登陆接口,和一个过滤器。登陆接口用来用户名和密码校验,并将用户信息保存在http session中。过滤器则用来拦截所有未经授权的访问。
@RestController
@RequestMapping("/login")
public class LoginController {
public void login(HttpServletRequest request, HttpServletResponse response) {
String json;
PrintWriter out = response.getWriter();
String user = request.getParameter("usr");
String pwd= request.getParameter("password");
if (pwd.equals(getPasswordByUserName(user))) {
request.getSession().setAttribute("user", user);
json = "{\"success\":true}";
}
else {
json = "{\"success\":false}";
}
out.print(json);
out.flush;
out.close;
}
}
public class AuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
if (!httpServletRequest.getServletPath().equals("/login")) {
HttpSession session = ((HttpServletRequest) request).getSession(false);
if (session == null || session.getAttribute("user") == null) {
((HttpServletResponse) response).setStatus(401);
return;
}
}
chain.doFilter(request, response);
}
}
Spring security 作为一个强大且可高度定制化的认证和访问控制框架,是安全化 spring 应用的实际标准。它为Java应用提供认证和授权的功能,用户能够轻易地扩展以适应自己的需求。
Spring security 认证的过程
- 用户向url /login Post登陆请求,该请求从由loginPage指定的前台页面发送,携带表单参数username和password。(/login 是spring security 4 默认拦截的认证url,它并不指向任何物理资源,在spring security 4之前,该url是/j_spring_security_check,参数是j_username和j_password;可以通过login-processing-url自定义认证url。事实上,我们还可以绕过鉴权过滤器,直接向自定义的登陆rest接口post登陆请求,只要我们引入了spring security相关的包,所有认证的逻辑将会在request.login(username, password)里完成,如下所示)。
@RequestMapping(value = "/my_login", method = RequestMethod.POST) public void login(@RequestParam("username") String username, @RequestParam("password") String password, HttpServletRequest request, HttpServletResponse response) throws DmsException { try { request.login(username, password); } catch (ServletException e) { throw new MyException("0000", e); } request.getSession(); }
- 根据传入的 username 和 password 生成 UsernamePasswordAuthenticationToken,AuthenticationManager接口的 authenticate 方法将对生成的Authentication进行认证处理。AuthenticationManager的默认实现类是ProviderManager,ProviderManager将认证委托给AuthenticatiionProvider处理。认证的本质是将Authentication和UserDetails中的用户信息进行比较。UserDetails包装的是拥有权限的用户信息,通常是从数据库中获取。
- UsernamePasswordAuthenticationFilter 过滤器在登陆成功后会通过SecurityContextHolder.getContext().setAuthentication() 将认证信息Authentication绑定到SecurityContext。
- 下一次请求时,过滤器链头的 SecurityContextPersistenceFilter 会从 Session 中获取已登陆用户信息并生成 Authentication,并将 认证信息对象绑定到 SecurityContext。如果是对需要权限的接口或资源的请求,Spring Security将从SecurityContext中获取用户的权限来判定是否可以访问该接口或资源。
Spring Security的核心组件
-
SecurityContextHolder,提供了一系列静态方法,代理一个SecurityContextHolderStrategy实例,方便用户对安全上下文的访问和设置。其初始化方法在静态块中被调用static { initialize(); } private static void initialize() { if (!StringUtils.hasText(strategyName)) { // Set default // 如果未设置strategyName属性,则默认使用ThreadLocal策略 strategyName = MODE_THREADLOCAL; } if (strategyName.equals(MODE_THREADLOCAL)) { // 线程局部变量安全持有策略,每个线程分别持有各自的SecurityContext strategy = new ThreadLocalSecurityContextHolderStrategy(); } else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) { // 可继承线程局部变量安全持有策略,每新起一个线程,则从父线程继承SecurityContext strategy = new InheritableThreadLocalSecurityContextHolderStrategy(); } else if (strategyName.equals(MODE_GLOBAL)) { // 全局安全上下文持有策略,所有的实例都共享同一个SecurityContext // 适用于富客户端应用,如swing应用。 strategy = new GlobalSecurityContextHolderStrategy(); } else { // Try to load a custom strategy // 用户可以自定义一个SecurityContextHolderStrategy实现类 try { Class<?> clazz = Class.forName(strategyName); Constructor<?> customStrategy = clazz.getConstructor(); strategy = (SecurityContextHolderStrategy) customStrategy.newInstance(); } catch (Exception ex) { ReflectionUtils.handleReflectionException(ex); } } initializeCount++; }
提供的api包括getContext(),setContext(),clearContext(),createEmptyContext()。
- Authentication,继承自 Principal,有如下接口方法:getAuthorities(),获取当前principal拥有的所有权限列表。如果还未通过鉴权,则返回空集合。getCredentials(),获取用户输入的密码凭证。通过认证之后被移除以确保安全。getDetails(),获取认证的额外信息,可能是ip地址或者证书的序列号,如果未使用的话则为null。getPrincipal(),获取正在被鉴权或已经通过鉴权的用户信息。
- UserDetails,用户详情接口。getAuthorities(),获取用户的权限列表,不可为null。getPassword(),获取用户密码。getUserName(),获取用户名字。isAccountNonExpired(),用户是否未过期。isAccountNonLocked(),用户是否未锁定。isCredentialsNonExpired(),密码凭证是否未过期。isEnabled(),用户是否被启用。UserDetails的默认实现是org.springframework.security.core.userdetails.User。
- UserDetailsService接口只有一个方法:UserDetails loadUserByUsername(String username),根据用户名获取用户详情。常用的实现类有InMemoryUserDetailsManager(从内存中获取)和JdbcUserDetailsManager(从数据库获取)。
- AuthenticationManager接口同样只有一个方法:Authentication authenticate(Authentication authentication),对用户输入的authentication进行认证。默认的实现类是ProviderManager,实际的认证逻辑被委托给了AuthenticationProvider。源码非常直白易懂,不做赘述。
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; boolean debug = logger.isDebugEnabled(); for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null) { // Allow the parent to try. try { result = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { // ignore as we will throw below if no other exception occurred prior to // calling parent and the parent // may throw ProviderNotFound even though a provider in the child already // handled the request } catch (AuthenticationException e) { lastException = e; } } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication ((CredentialsContainer) result).eraseCredentials(); } eventPublisher.publishAuthenticationSuccess(result); return result; } // Parent was null, or didn't authenticate (or throw an exception). if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } prepareException(lastException, authentication); throw lastException; }
AuthenticationProvider的抽象实现类之一 AbstractUserDetailsAuthenticationProvider,这里完成了认证前后的一些检验工作,具体的认证细节须由子类完成。子类须实现抽象方法:
UserDetails retrieveUser(String username, usernamePasswordAuthenticationToken authentication) throws AuthenticationException
最终会调用UserDetailsService来获取userDetails。
protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
DaoAuthenticationProvider实现该抽象方法如下:
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { Object salt = null; if (this.saltSource != null) { salt = this.saltSource.getSalt(userDetails); } if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); // 校验密码是否有效 if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }
Spring Security的核心过滤器链
核心过滤器链依次为:
- ChannelProcessingFilter,确保web请求通过要求的协议传递,如果访问的channel错了,则在channel之间跳转。最普遍的用法是确保请求通过https协议传输,配置方式参考如下:
<bean id="channelProcessingFilter" class="org.springframework.security.web.access.channel.ChannelProcessingFilter">
<property name="channelDecisionManager" ref="channelDecisionManager"/>
<property name="securityMetadataSource">
<security:filter-security-metadata-source request-matcher="regex">
<security:intercept-url pattern="\A/secure/.*\Z" access="REQUIRES_SECURE_CHANNEL"/>
<security:intercept-url pattern="\A/login.jsp.*\Z" access="REQUIRES_SECURE_CHANNEL"/>
<security:intercept-url pattern="\A/.*\Z" access="ANY_CHANNEL"/>
</security:filter-security-metadata-source>
</property>
</bean>
<bean id="channelDecisionManager" class="org.springframework.security.web.access.channel.ChannelDecisionManagerImpl">
<property name="channelProcessors">
<list>
<ref bean="secureChannelProcessor"/>
<ref bean="insecureChannelProcessor"/>
</list>
</property>
</bean>
<bean id="secureChannelProcessor"
class="org.springframework.security.web.access.channel.SecureChannelProcessor"/>
<bean id="insecureChannelProcessor"
class="org.springframework.security.web.access.channel.InsecureChannelProcessor"/>
- SecurityContextPersistenceFilter,请求到达时,从SecurityContextRepository提取SecurityContext,填充到SecurityContextHolder,请求完成后将SecurityContext回存到SecurityContextRepository(通常是HttpSessionSecurityContextRepository),并清除SecurityContextHolder。
// 从repo提取security context
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
// 设置到SecurityContextHolder
SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything
// else.
// 清空context holder
SecurityContextHolder.clearContext();
// 将context 存回repo
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
- ConcurrentSessionFilter,检查session是否过期,如果过期,则调用LogoutHandler进行登出。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
HttpSession session = request.getSession(false);
if (session != null) {
// sessionRegistry通常维护了一个map,缓存了session信息,key为sessionID
SessionInformation info = sessionRegistry.getSessionInformation(session
.getId());
if (info != null) {
if (info.isExpired()) {
// Expired - abort processing
if (logger.isDebugEnabled()) {
logger.debug("Requested session ID "
+ request.getRequestedSessionId() + " has expired.");
}
// session 过期,登出
doLogout(request, response);
this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
return;
}
else {
// Non-expired - update last request date/time
// 没过期,则刷新session的最后一次请求时间
sessionRegistry.refreshLastRequest(info.getSessionId());
}
}
}
chain.doFilter(request, response);
}
- 认证过滤器,包括UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter等,进行认证并在认证成功后往SecurityContextHolder中填充Authentication信息。
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);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 最终调用AuthenticationProvider进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
- SecurityContextHolderAwareRequestFilter,该过滤器将HttpServletRequest封装成继承自HttpServletRequestWrapper的SecurityContextHolderAwareRequestWrapper,并将登陆认证的逻辑添加到了request.login()方法里。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(this.requestFactory.create((HttpServletRequest) req,
(HttpServletResponse) res), res);
}
- JaasApiIntegrationFilter,如果SecurityContextHolder中持有JaasAuthenticationToken,该Filter将使用包含在JaasAuthenticationToken中的Subject继续下游过滤器。
- RememberMeAuthenticationFilter如果上述的认证过滤器没有更新SecurityContextHolder中的Authentication,那么该过滤器将尝试从请求中抽取一个remenber-me cookie,并使用对应的authentication进行认证和填充SecurityContextHolder。
if (SecurityContextHolder.getContext().getAuthentication() == null) {
// 从request中抽取Remember-me cookie
Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
response);
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
// Store to SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
- AnonymousAuthenticationFilter,如果上游认证机制仍然没有更新SecurityContextHolder,AnonymousAuthenticationFilter将给SecurityContextHolder填充一个AnonimousAuthenticationToken,即一个匿名用户。
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(
// 创建匿名用户认证信息
createAuthentication((HttpServletRequest) req));
if (logger.isDebugEnabled()) {
logger.debug("Populated SecurityContextHolder with anonymous token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
}
- ExceptionTranslationFilter捕获security filter链抛出的异常,并只处理其中的AuthenticationException和AccessDeniedException。
public class ExceptionTranslationFilter extends GenericFilterBean {
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
// 如果认证异常,则重定向到登陆页
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
// 被拒绝访问的匿名用户或携带remember-me cookie的用户,重定向到登录页
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
"Full authentication is required to access this resource"));
}
else {
// accessDeniedHandler处理拒绝访问异常
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
}
}
- FilterSecurityInterceptor,用于权限控制。从Authentication中获取用户的权限authorities,以及资源需要的访问权限,accessDecisionManager来决定当前用户是否有权访问该资源
try {
// authenticated认证对象,object访问的资源,attributes受保护的资源列表
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
DelegatingFilterProxy
一个标准Servlet过滤器的代理,代理的对象是一个实现了Filter接口的spring bean,通过过滤器名和bean名称的关联来指定被代理的过滤器。如在web.xml中这样配置(当然也可以使用基于Java的配置):
<filter>
<filter-name>myFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
然后我们在spring 上下文中维护一个实现了Filter接口并且名称为myFilter的bean,那么这个过滤器bean将被代理到ServletContext中,当请求到达时,真正被执行的是过滤器bean中的过滤逻辑。
FilterChainProxy
spring security的过滤器链中包括了多个过滤器,如果将每个过滤器都通过一个DelegatingFilterProxy注册到web.xml,将非常的紊乱,FilterChainProxy的作用是代理security中所有的过滤器,这样,只需要将FilterChainProxy配置成bean,由DelegatingFilterProxy代理到ServletContext,所有的security过滤逻辑便可交由FilterChainProxy来控制执行。spring security已经在WebSecurityConfiguration中配置了这个bean,可通过@EnableWebSecurity注解来使security过滤器链生效:
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}
我们只需要将其注册到web.xml中(filter-name和bean名称保持一致):
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
开启spring security
开启spring security有两种方式:
(1)使用@EnableWebSecurity注解来开启创建springSecurityFilterCharin bean,并将过滤器链通过DelegatingFilterProxy注册到web.xml中。
(2)启动类继承AbstractSecurityWebApplicationInitializer,AbstractSecurityWebApplicationInitializer实现了WebApplicationInitializer ,因此会被spring检测到(参考https://mp.youkuaiyun.com/postedit/82085192)。AbstractSecurityWebApplicationInitializer会注册名为springSecurityFilterCharin的DelegatingFilterProxy过滤器,我们不需要再做额外操作。
多个spring security配置
有时候我们对于应用的不同部分要启用不同的鉴权机制,而spring security允许我们在一个应用中启用多个security配置。参考spring官方文档的配置:
@EnableWebSecurity
public class MultiHttpSecurityConfig {
@Bean
public UserDetailsService userDetailsService() throws Exception {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user").password("password").roles("USER").build());
manager.createUser(User.withUsername("admin").password("password").roles("USER","ADMIN").build());
return manager;
}
@Configuration
@Order(1) 1
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/**") 2
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.httpBasic();
}
}
@Configuration 3
public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
}
其中@Order注解用于指定过滤器链的顺序,以确保对于每个请求,都被正确的过滤器链拦截。没有@Order注解的配置默认处于最后。如果不使用@Order注解,实现Ordered接口重写getOrder()方法效果是一样的。
自定义过滤器并纳入security过滤器链中
自定义Filter,并通过http.addFilter系列方法插入到过滤器链中,如:
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(new CurrentUserPopulateFilter(userPool, userService), BasicAuthenticationFilter.class)
.requestMatcher(getRequestMatcher())
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.exceptionHandling()
.accessDeniedHandler(new DmsAccessDeniedHandler())
.and()
.csrf().disable();
}
如何配置spring security
细节太多不详谈了,看官方文档吧https://docs.spring.io/spring-security/site/docs/4.2.10.RELEASE/reference/htmlsingle/#jc