Spring Security
Spring Security 是 Spring 社区的一个顶级项目,也是 Spring Boot 官方推荐使用的安全框架。除了常规的认证(Authentication)和授权(Authorization)之外,Spring Security还提供了诸如ACLs,LDAP,JAAS,CAS等高级特性以满足复杂场景下的安全需求。
Spring Security 应用级别的安全主要包含两个主要部分,即登录认证(Authentication)和访问授权(Authorization),首先用户登录的时候传入登录信息,登录验证器完成登录认证并将登录认证好的信息存储到请求上下文,然后在进行其他操作,如接口访问、方法调用时,权限认证器从上下文中获取登录认证信息,然后根据认证信息获取权限信息,通过权限信息和特定的授权策略决定是否授权。
接下来,本教程将分别对登录认证和访问授权的执行流程进行剖析,并在最后给出完整的案例实现,如果觉得先读前面原理比较难懂,可以先学习后面的实现案例,再结合案例理解登录认证和访问授权的执行原理。
登录认证
登录认证过滤器
如果在继承 WebSecurityConfigurerAdapter 的配置类中的 configure(HttpSecurity http) 方法中有配置 HttpSecurity 的 formLogin,则会返回一个 FormLoginConfigurer 对象。如下是一个 Spring Security 的配置样例, formLogin().x.x 就是配置使用内置的登录验证过滤器,默认实现为 UsernamePasswordAuthenticationFilter。
WebSecurityConfig.java

1@Configuration 2@EnableWebSecurity 3@EnableGlobalMethodSecurity(prePostEnabled = true) 4public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 5 6 @Autowired 7 private UserDetailsService userDetailsService; 8 9 @Override 10 public void configure(AuthenticationManagerBuilder auth) throws Exception { 11 // 使用自定义身份验证组件 12 auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService)); 13 } 14 15 @Override 16 protected void configure(HttpSecurity http) throws Exception { 17 http.cors().and().csrf().disable() 18 .authorizeRequests() 19 // 首页和登录页面 20 .antMatchers("/").permitAll() 21 // 其他所有请求需要身份认证 22 .anyRequest().authenticated() 23 // 配置登录认证 24 .and().formLogin().loginProcessingUrl("/login"); 25 } 26}

查看 HttpSecurity , formLogion 方法返回一个 FormLoginConfigurer 对象。
HttpSecurity.java
1public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception { 2 return getOrApply(new FormLoginConfigurer<>()); 3 }
而 FormLoginConfigurer 的构造函数内绑定了一个 UsernamePasswordAuthenticationFilter 过滤器。
FormLoginConfigurer.java
1public FormLoginConfigurer() { 2 super(new UsernamePasswordAuthenticationFilter(), null); 3 usernameParameter("username"); 4 passwordParameter("password"); 5 }
再看 UsernamePasswordAuthenticationFilter 过滤器的构造函数内绑定了 POST 类型的 /login 请求,也就是说,如果配置了 formLogin 的相关信息,那么在使用 POST 类型的 /login URL进行登录的时候就会被这个过滤器拦截,并进行登录验证,登录验证过程我们下面继续分析。
UsernamePasswordAuthenticationFilter.java
1public UsernamePasswordAuthenticationFilter() { 2 super(new AntPathRequestMatcher("/login", "POST")); 3 }
查看 UsernamePasswordAuthenticationFilter,发现它继承了 AbstractAuthenticationProcessingFilter,AbstractAuthenticationProcessingFilter 中的 doFilter 包含了触发登录认证执行流程的相关逻辑。
AbstractAuthenticationProcessingFilter.java

1public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 2 throws IOException, ServletException { 3 4 ... 5 Authentication authResult; 6 try { 7 authResult = attemptAuthentication(request, response); 8 if (authResult == null) { 9 // return immediately as subclass has indicated that it hasn't completed 10 // authentication 11 return; 12 } 13 sessionStrategy.onAuthentication(authResult, request, response); 14 } 15 ... 16 successfulAuthentication(request, response, chain, authResult); 17 }

上面的登录逻辑主要步骤有两个:
1. attemptAuthentication(request, response)
这是 AbstractAuthenticationProcessingFilter 中的一个抽象方法,包含登录主逻辑,由其子类实现具体的登录验证,如 UsernamePasswordAuthenticationFilter 是使用表单方式登录的具体实现。如果是非表单登录的方式,如JNDI等其他方式登录的可以通过继承 AbstractAuthenticationProcessingFilter 自定义登录实现。UsernamePasswordAuthenticationFilter 的登录实现逻辑如下。
UsernamePasswordAuthenticationFilter.java

1public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { 2 if (postOnly && !request.getMethod().equals("POST")) { 3 throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); 4 } 5 // 获取用户名和密码 6 String username = obtainUsername(request); 7 String password = obtainPassword(request); 8 9 if (username == null) { 10 username = ""; 11 } 12 13 if (password == null) { 14 password = ""; 15 } 16 17 username = username.trim(); 18 19 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); 20 21 // Allow subclasses to set the "details" property 22 setDetails(request, authRequest); 23 24 return this.getAuthenticationManager().authenticate(authRequest); 25 }

2. successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
登录成功之后,将认证后的 Authentication 对象存储到请求线程上下文,这样在授权阶段就可以获取到 Authentication 认证信息,并利用 Authentication 内的权限信息进行访问控制判断。
AbstractAuthenticationProcessingFilter.java

1protected void successfulAuthentication(HttpServletRequest request, 2 HttpServletResponse response, FilterChain chain, Authentication authResult) 3 throws IOException, ServletException { 4 5 if (logger.isDebugEnabled()) { 6 logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); 7 } 8 // 登录成功之后,把认证后的 Authentication 对象存储到请求线程上下文,这样在授权阶段就可以获取到此认证信息进行访问控制判断 9 SecurityContextHolder.getContext().setAuthentication(authResult); 10 11 rememberMeServices.loginSuccess(request, response, authResult); 12 13 // Fire event 14 if (this.eventPublisher != null) { 15 eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( 16 authResult, this.getClass())); 17 } 18 19 successHandler.onAuthenticationSuccess(request, response, authResult); 20 }

从上面的登录逻辑我们可以看到,Spring Security的登录认证过程是委托给 AuthenticationManager 完成的,它先是解析出用户名和密码,然后把用户名和密码封装到一个UsernamePasswordAuthenticationToken 中,传递给 AuthenticationManager,交由 AuthenticationManager 完成实际的登录认证过程。
AuthenticationManager.java

package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
/**
* Processes an {@link Authentication} request.
* @author Ben Alex
*/
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

AuthenticationManager 提供了一个默认的 实现 ProviderManager,而 ProviderManager 又将验证委托给了 AuthenticationProvider。
ProviderManager.java

1public Authentication authenticate(Authentication authentication) 2 throws AuthenticationException { 3 ... for (AuthenticationProvider provider : getProviders()) { 4 if (!provider.supports(toTest)) { 5 continue; 6 }try { 7 result = provider.authenticate(authentication); 8 if (result != null) { 9 copyDetails(authentication, result); 10 break; 11 } 12 } ... 13 }

根据验证方式的多样化,AuthenticationProvider 衍生出多种类型的实现,AbstractUserDetailsAuthenticationProvider 是 AuthenticationProvider 的抽象实现,定义了较为统一的验证逻辑,各种验证方式可以选择直接继承 AbstractUserDetailsAuthenticationProvider 完成登录认证,如 DaoAuthenticationProvider 就是继承了此抽象类,完成了从DAO方式获取验证需要的用户信息的。
AbstractUserDetailsAuthenticationProvider.java

1public Authentication authenticate(Authentication authentication) throws AuthenticationException {// Determine username 2 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); 3 boolean cacheWasUsed = true; 4 UserDetails user = this.userCache.getUserFromCache(username); 5 if (user == null) { 6 cacheWasUsed = false; 7 try { // 子类根据自身情况从指定的地方加载认证需要的用户信息 8 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); 9 } 10 ...try { // 前置检查,一般是检查账号状态,如是否锁定之类 11 preAuthenticationChecks.check(user); // 进行一般逻辑认证,如 DaoAuthenticationProvider 实现中的密码验证就是在这里完成的 12 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); 13 } 14 ... 15 // 后置检查,如可以检查密码是否过期之类 16 postAuthenticationChecks.check(user); 17 18 ... // 验证成功之后返回包含完整认证信息的 Authentication 对象 19 return createSuccessAuthentication(principalToReturn, authentication, user); 20 }

如上面所述, AuthenticationProvider 通过 retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) 获取验证信息,对于我们一般所用的 DaoAuthenticationProvider 是由 UserDetailsService 专门负责获取验证信息的。
DaoAuthenticationProvider.java

1protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { 2 try { 3 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); 4 if (loadedUser == null) { 5 throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); 6 } 7 return loadedUser; 8 } 9 }

UserDetailsService 接口只有一个方法,loadUserByUsername(String username),一般需要我们实现此接口方法,根据用户名加载登录认证和访问授权所需要的信息,并返回一个 UserDetails的实现类,后面登录认证和访问授权都需要用到此中的信息。

1public interface UserDetailsService { 2 /** 3 * Locates the user based on the username. In the actual implementation, the search 4 * may possibly be case sensitive, or case insensitive depending on how the 5 * implementation instance is configured. In this case, the <code>UserDetails</code> 6 * object that comes back may have a username that is of a different case than what 7 * was actually requested.. 8 * 9 * @param username the username identifying the user whose data is required. 10 * 11 * @return a fully populated user record (never <code>null</code>) 12 * 13 * @throws UsernameNotFoundException if the user could not be found or the user has no 14 * GrantedAuthority 15 */ 16 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 17}

UserDetails 提供了一个默认实现 User,主要包含用户名(username)、密码(password)、权限(authorities)和一些账号或密码状态的标识。
如果默认实现满足不了你的需求,可以根据需求定制自己的 UserDetails,然后在 UserDetailsService 的 loadUserByUsername 中返回即可。

1public class User implements UserDetails, CredentialsContainer {// ~ Instance fields 2 // ================================================================================================ 3 private String password; 4 private final String username; 5 private final Set<GrantedAuthority> authorities; 6 private final boolean accountNonExpired; 7 private final boolean accountNonLocked; 8 private final boolean credentialsNonExpired; 9 private final boolean enabled; 10 11 // ~ Constructors 12 // =================================================================================================== 13 public User(String username, String password, 14 Collection<? extends GrantedAuthority> authorities) { 15 this(username, password, true, true, true, true, authorities); 16 } ... 17}

退出登录
Spring Security 提供了一个默认的登出过滤器 LogoutFilter,默认拦截路径是 /logout,当访问 /logout 路径的时候,LogoutFilter 会进行退出处理。
LogoutFilter.java

1package org.springframework.security.web.authentication.logout; 2 3public class LogoutFilter extends GenericFilterBean { 4 5 // ~ Instance fields 6 // ================================================================================================ 7 private RequestMatcher logoutRequestMatcher; 8 private final LogoutHandler handler; 9 private final LogoutSuccessHandler logoutSuccessHandler; 10 11 // ~ Constructors 12 // =================================================================================================== 13 public LogoutFilter(LogoutSuccessHandler logoutSuccessHandler, 14 LogoutHandler... handlers) { 15 this.handler = new CompositeLogoutHandler(handlers); 16 Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null"); 17 this.logoutSuccessHandler = logoutSuccessHandler; 18 setFilterProcessesUrl("/logout"); // 绑定 /logout 19 }// ~ Methods 20 // ======================================================================================================== 21 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 22 throws IOException, ServletException { 23 HttpServletRequest request = (HttpServletRequest) req; 24 HttpServletResponse response = (HttpServletResponse) res; 25 26 if (requiresLogout(request, response)) { 27 Authentication auth = SecurityContextHolder.getContext().getAuthentication();this.handler.logout(request, response, auth); // 登出处理,可能包含session、cookie、认证信息的清理工作 28 29 logoutSuccessHandler.onLogoutSuccess(request, response, auth); // 退出后的操作,可能是跳转、返回成功状态等 30 31 return; 32 } 33 34 chain.doFilter(request, response); 35 } 36 37 ... 38}

如下是 SecurityContextLogoutHandler 中的登出处理实现。
SecurityContextLogoutHandler.java

1public void logout(HttpServletRequest request, HttpServletResponse response, 2 Authentication authentication) { 3 // 让 session 失效 if (invalidateHttpSession) { 4 HttpSession session = request.getSession(false); 5 if (session != null) { 6 logger.debug("Invalidating session: " + session.getId()); 7 session.invalidate(); 8 } 9 } 10 // 清理 Security 上下文,其中包含登录认证信息 11 if (clearAuthentication) { 12 SecurityContext context = SecurityContextHolder.getContext(); 13 context.setAuthentication(null); 14 } 15 SecurityContextHolder.clearContext(); 16 }

访问授权
访问授权主要分为两种:通过URL方式的接口访问控制和方法调用的权限控制。
接口访问权限
在通过比如浏览器使用URL访问后台接口时,是否允许访问此URL,就是接口访问权限。
在进行接口访问时,会由 FilterSecurityInterceptor 进行拦截并进行授权。
FilterSecurityInterceptor 继承了 AbstractSecurityInterceptor 并实现了 javax.servlet.Filter 接口, 所以在URL访问的时候都会被过滤器拦截,doFilter 实现如下。
FilterSecurityInterceptor.java
1public void doFilter(ServletRequest request, ServletResponse response, 2 FilterChain chain) throws IOException, ServletException { 3 FilterInvocation fi = new FilterInvocation(request, response, chain); 4 invoke(fi); 5 }
doFilter 方法又调用了自身的 invoke 方法, invoke 方法又调用了父类 AbstractSecurityInterceptor 的 beforeInvocation 方法。
FilterSecurityInterceptor.java

1public void invoke(FilterInvocation fi) throws IOException, ServletException { 2 if ((fi.getRequest() != null) 3 && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) 4 && observeOncePerRequest) { 5 // filter already applied to this request and user wants us to observe 6 // once-per-request handling, so don't re-do security checking 7 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); 8 } 9 else { 10 // first time this request being called, so perform security checking 11 if (fi.getRequest() != null && observeOncePerRequest) { 12 fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); 13 } 14 15 InterceptorStatusToken token = super.beforeInvocation(fi); 16 17 try { 18 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); 19 } 20 finally { 21 super.finallyInvocation(token); 22 } 23 24 super.afterInvocation(token, null); 25 } 26 }

方法调用权限
在进行后台方法调用时,是否允许该方法调用,就是方法调用权限。比如在方法上添加了此类注解 @PreAuthorize("hasRole('ROLE_ADMIN')") ,Security 方法注解的支持需要在任何配置类中(如 WebSecurityConfigurerAdapter )添加 @EnableGlobalMethodSecurity(prePostEnabled = true) 开启,才能够使用。
1@Configuration 2@EnableWebSecurity 3@EnableGlobalMethodSecurity(prePostEnabled = true) 4public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 5 6}
在进行方法调用时,会由 MethodSecurityInterceptor 进行拦截并进行授权。
MethodSecurityInterceptor 继承了 AbstractSecurityInterceptor 并实现了AOP 的 org.aopalliance.intercept.MethodInterceptor 接口, 所以可以在方法调用时进行拦截。
MethodSecurityInterceptor .java

1public Object invoke(MethodInvocation mi) throws Throwable { 2 InterceptorStatusToken token = super.beforeInvocation(mi); 3 4 Object result; 5 try { 6 result = mi.proceed(); 7 } 8 finally { 9 super.finallyInvocation(token); 10 } 11 return super.afterInvocation(token, result); 12 }

我们看到,MethodSecurityInterceptor 跟 FilterSecurityInterceptor 一样, 都是通过调用父类 AbstractSecurityInterceptor 的相关方法完成授权,其中 beforeInvocation 是完成权限认证的关键。
AbstractSecurityInterceptor.java

1protected InterceptorStatusToken beforeInvocation(Object object) { 2 ... 3 // 通过 SecurityMetadataSource 获取权限配置信息,可以定制实现自己的权限信息获取逻辑 4 Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object); 5 6 ... // 确认是否经过登录认证 7 Authentication authenticated = authenticateIfRequired(); 8 9 // Attempt authorization 10 try { // 通过 AccessDecisionManager 完成授权认证,默认实现是 AffirmativeBased 11 this.accessDecisionManager.decide(authenticated, object, attributes); 12 } 13 ... 14 }

上面代码显示 AbstractSecurityInterceptor 又是委托授权认证器 AccessDecisionManager 完成授权认证,默认实现是 AffirmativeBased, decide 方法实现如下。
AffirmativeBased.java

1public void decide(Authentication authentication, Object object, 2 Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { 3 int deny = 0; 4 for (AccessDecisionVoter voter : getDecisionVoters()) { // 通过各种投票策略,最终决定是否授权 5 int result = voter.vote(authentication, object, configAttributes); 6switch (result) { 7 case AccessDecisionVoter.ACCESS_GRANTED: 8 return; 9 case AccessDecisionVoter.ACCESS_DENIED: 10 deny++; 11 break; 12 default: 13 break; 14 } 15 ... 16 }

而 AccessDecisionManager 决定授权又是通过一个授权策略集合(AccessDecisionVoter )决定的,授权决定的原则是:
1. 遍历所有授权策略, 如果有其中一个返回 ACCESS_GRANTED,则同意授权。
2. 否则,等待遍历结束,统计 ACCESS_DENIED 个数,只要拒绝数大于1,则不同意授权。
对于接口访问授权,也就是 FilterSecurityInterceptor 管理的URL授权,默认对应的授权策略只有一个,就是 WebExpressionVoter,它的授权策略主要是根据 WebSecurityConfigurerAdapter 内配置的路径访问策略进行匹配,然后决定是否授权。
WebExpressionVoter.java

1/** 2 * Voter which handles web authorisation decisions. 3 * @author Luke Taylor 4 * @since 3.0 5 */ 6public class WebExpressionVoter implements AccessDecisionVoter<FilterInvocation> { 7 private SecurityExpressionHandler<FilterInvocation> expressionHandler = new DefaultWebSecurityExpressionHandler(); 8 9 public int vote(Authentication authentication, FilterInvocation fi, 10 Collection<ConfigAttribute> attributes) { 11 assert authentication != null; 12 assert fi != null; 13 assert attributes != null; 14 15 WebExpressionConfigAttribute weca = findConfigAttribute(attributes); 16 17 if (weca == null) { 18 return ACCESS_ABSTAIN; 19 } 20 21 EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, fi); 22 ctx = weca.postProcess(ctx, fi); 23 24 return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED : ACCESS_DENIED; 25 } 26 27 ... 28}

对于方法调用授权,在全局方法安全配置类里,可以看到给 MethodSecurityInterceptor 默认配置的有 RoleVoter、AuthenticatedVoter、Jsr250Voter、和 PreInvocationAuthorizationAdviceVoter,其中 Jsr250Voter、PreInvocationAuthorizationAdviceVoter 都需要打开指定的开关,才会添加支持。
GlobalMethodSecurityConfiguration.java

1@Configuration 2public class GlobalMethodSecurityConfiguration implements ImportAware, SmartInitializingSingleton { 3 ...private MethodSecurityInterceptor methodSecurityInterceptor; 4 @Bean 5 public MethodInterceptor methodSecurityInterceptor() throws Exception { 6 this.methodSecurityInterceptor = isAspectJ() 7 ? new AspectJMethodSecurityInterceptor() 8 : new MethodSecurityInterceptor(); 9 methodSecurityInterceptor.setAccessDecisionManager(accessDecisionManager()); 10 methodSecurityInterceptor.setAfterInvocationManager(afterInvocationManager()); 11 methodSecurityInterceptor 12 .setSecurityMetadataSource(methodSecurityMetadataSource()); 13 RunAsManager runAsManager = runAsManager(); 14 if (runAsManager != null) { 15 methodSecurityInterceptor.setRunAsManager(runAsManager); 16 } 17 18 return this.methodSecurityInterceptor; 19 } 20 21 protected AccessDecisionManager accessDecisionManager() { 22 List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<AccessDecisionVoter<? extends Object>>(); 23 ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice(); 24 expressionAdvice.setExpressionHandler(getExpressionHandler()); 25 if (prePostEnabled()) { 26 decisionVoters 27 .add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice)); 28 } 29 if (jsr250Enabled()) { 30 decisionVoters.add(new Jsr250Voter()); 31 } 32 decisionVoters.add(new RoleVoter()); 33 decisionVoters.add(new AuthenticatedVoter()); 34 return new AffirmativeBased(decisionVoters); 35 } ...}

RoleVoter 是根据角色进行匹配授权的策略。
RoleVoter.java

1public class RoleVoter implements AccessDecisionVoter<Object> { 2 // RoleVoter 默认角色名以 "ROLE_" 为前缀。 3 private String rolePrefix = "ROLE_";public boolean supports(ConfigAttribute attribute) { 4 if ((attribute.getAttribute() != null) 5 && attribute.getAttribute().startsWith(getRolePrefix())) { 6 return true; 7 } 8 else { 9 return false; 10 } 11 }public int vote(Authentication authentication, Object object, 12 Collection<ConfigAttribute> attributes) { 13 if(authentication == null) { 14 return ACCESS_DENIED; 15 } 16 int result = ACCESS_ABSTAIN; 17 Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication); 18 // 逐个角色进行匹配,入股有一个匹配得上,则进行授权 19 for (ConfigAttribute attribute : attributes) { 20 if (this.supports(attribute)) { 21 result = ACCESS_DENIED; 22 // Attempt to find a matching granted authority 23 for (GrantedAuthority authority : authorities) { 24 if (attribute.getAttribute().equals(authority.getAuthority())) { 25 return ACCESS_GRANTED; 26 } 27 } 28 } 29 } 30 31 return result; 32 } 33 34 Collection<? extends GrantedAuthority> extractAuthorities(Authentication authentication) { 35 return authentication.getAuthorities(); 36 } 37}

AuthenticatedVoter 主要是针对有配置以下几个属性来决定授权的策略。
IS_AUTHENTICATED_REMEMBERED:记住我登录状态
IS_AUTHENTICATED_ANONYMOUSLY:匿名认证状态
IS_AUTHENTICATED_FULLY: 完全登录状态,即非上面两种类型
AuthenticatedVoter.java

1public int vote(Authentication authentication, Object object, 2 Collection<ConfigAttribute> attributes) { 3 int result = ACCESS_ABSTAIN; 4 5 for (ConfigAttribute attribute : attributes) { 6 if (this.supports(attribute)) { 7 result = ACCESS_DENIED; 8 // 完全登录状态 9 if (IS_AUTHENTICATED_FULLY.equals(attribute.getAttribute())) { 10 if (isFullyAuthenticated(authentication)) { 11 return ACCESS_GRANTED; 12 } 13 } 14 // 记住我登录状态 15 if (IS_AUTHENTICATED_REMEMBERED.equals(attribute.getAttribute())) { 16 if (authenticationTrustResolver.isRememberMe(authentication) 17 || isFullyAuthenticated(authentication)) { 18 return ACCESS_GRANTED; 19 } 20 } 21 // 匿名登录状态 22 if (IS_AUTHENTICATED_ANONYMOUSLY.equals(attribute.getAttribute())) { 23 if (authenticationTrustResolver.isAnonymous(authentication) 24 || isFullyAuthenticated(authentication) 25 || authenticationTrustResolver.isRememberMe(authentication)) { 26 return ACCESS_GRANTED; 27 } 28 } 29 } 30 } 31 32 return result; 33 }

PreInvocationAuthorizationAdviceVoter 是针对类似 @PreAuthorize("hasRole('ROLE_ADMIN')") 注解解析并进行授权的策略。
PreInvocationAuthorizationAdviceVoter.java

1public class PreInvocationAuthorizationAdviceVoter implements AccessDecisionVoter<MethodInvocation> {private final PreInvocationAuthorizationAdvice preAdvice; 2public int vote(Authentication authentication, MethodInvocation method, 3 Collection<ConfigAttribute> attributes) { 4 5 PreInvocationAttribute preAttr = findPreInvocationAttribute(attributes); 6 7 if (preAttr == null) { 8 // No expression based metadata, so abstain 9 return ACCESS_ABSTAIN; 10 } 11 12 boolean allowed = preAdvice.before(authentication, method, preAttr); 13 14 return allowed ? ACCESS_GRANTED : ACCESS_DENIED; 15 } 16 17 private PreInvocationAttribute findPreInvocationAttribute( 18 Collection<ConfigAttribute> config) { 19 for (ConfigAttribute attribute : config) { 20 if (attribute instanceof PreInvocationAttribute) { 21 return (PreInvocationAttribute) attribute; 22 } 23 } 24 return null; 25 } 26}

PreInvocationAuthorizationAdviceVoter 解析出注解属性配置, 然后通过调用 PreInvocationAuthorizationAdvice 的前置通知方法进行授权认证,默认实现类似 ExpressionBasedPreInvocationAdvice,通知内主要进行了内容的过滤和权限表达式的匹配。
ExpressionBasedPreInvocationAdvice.java

1public class ExpressionBasedPreInvocationAdvice implements PreInvocationAuthorizationAdvice { 2 private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); 3 4 public boolean before(Authentication authentication, MethodInvocation mi, PreInvocationAttribute attr) { 5 PreInvocationExpressionAttribute preAttr = (PreInvocationExpressionAttribute) attr; 6 EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi); 7 Expression preFilter = preAttr.getFilterExpression(); 8 Expression preAuthorize = preAttr.getAuthorizeExpression(); 9 10 if (preFilter != null) { 11 Object filterTarget = findFilterTarget(preAttr.getFilterTarget(), ctx, mi); 12 expressionHandler.filter(filterTarget, preFilter, ctx); 13 } 14 15 if (preAuthorize == null) { 16 return true; 17 } 18 19 return ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx); 20 } 21 22 ... 23}

案例实现
接下来,我们以一个实现案例来进行说明讲解。
新建工程
新建一个 Spring Boot 项目 springboot-spring-security。

添加依赖
添加项目依赖,主要是 Spring Security 和 JWT,另外添加 Swagger 和 fastjson 作为辅助工具。
pom.xml

1<?xml version="1.0" encoding="UTF-8"?> 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4 <modelVersion>4.0.0</modelVersion> 5 6 <groupId>top.ivan.demo</groupId> 7 <artifactId>springboot-spring-security</artifactId> 8 <version>0.0.1-SNAPSHOT</version> 9 <packaging>jar</packaging> 10 11 <name>springboot-spring-security</name> 12 <description>Demo project for Spring Boot</description> 13 14 <parent> 15 <groupId>org.springframework.boot</groupId> 16 <artifactId>spring-boot-starter-parent</artifactId> 17 <version>2.0.4.RELEASE</version> 18 <relativePath/> <!-- lookup parent from repository --> 19 </parent> 20 21 <properties> 22 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 23 <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> 24 <java.version>1.8</java.version> 25 <mybatis.spring.version>1.3.2</mybatis.spring.version> 26 <swagger.version>2.8.0</swagger.version> 27 <jwt.version>0.9.1</jwt.version> 28 <fastjson.version>1.2.48</fastjson.version> 29 </properties> 30 31 <dependencies> 32 <!-- spring boot --> 33 <dependency> 34 <groupId>org.springframework.boot</groupId> 35 <artifactId>spring-boot-starter-web</artifactId> 36 </dependency> 37 <dependency> 38 <groupId>org.springframework.boot</groupId> 39 <artifactId>spring-boot-starter-test</artifactId> 40 <scope>test</scope> 41 </dependency> 42 <!-- swagger --> 43 <dependency> 44 <groupId>io.springfox</groupId> 45 <artifactId>springfox-swagger2</artifactId> 46 <version>${swagger.version}</version> 47 </dependency> 48 <dependency> 49 <groupId>io.springfox</groupId> 50 <artifactId>springfox-swagger-ui</artifactId> 51 <version>${swagger.version}</version> 52 </dependency> 53 <!-- spring security --> 54 <dependency> 55 <groupId>org.springframework.boot</groupId> 56 <artifactId>spring-boot-starter-security</artifactId> 57 </dependency> 58 <!-- jwt --> 59 <dependency> 60 <groupId>io.jsonwebtoken</groupId> 61 <artifactId>jjwt</artifactId> 62 <version>${jwt.version}</version> 63 </dependency> 64 <!-- fastjson --> 65 <dependency> 66 <groupId>com.alibaba</groupId> 67 <artifactId>fastjson</artifactId> 68 <version>${fastjson.version}</version> 69 </dependency> 70 </dependencies> 71 72 <build> 73 <plugins> 74 <plugin> 75 <groupId>org.springframework.boot</groupId> 76 <artifactId>spring-boot-maven-plugin</artifactId> 77 </plugin> 78 </plugins> 79 </build> 80 81</project>

启动类
启动类没什么,主要开启以下包扫描。
SpringSecurityApplication.java

1package com.louis.springboot.spring.security; 2 3import org.springframework.boot.SpringApplication; 4import org.springframework.boot.autoconfigure.SpringBootApplication; 5import org.springframework.context.annotation.ComponentScan; 6 7/** 8 * 启动器 9 * @author Louis 10 * @date Nov 28, 2018 11 */ 12@SpringBootApplication 13@ComponentScan(basePackages = "com.louis.springboot") 14public class SpringSecurityApplication { 15 16 public static void main(String[] args) { 17 SpringApplication.run(SpringSecurityApplication.class, args); 18 } 19}

跨域配置类
跨域配置类,不多说,都懂得。
CorsConfig.java

1package com.louis.springboot.spring.security.config; 2 3import org.springframework.context.annotation.Configuration; 4import org.springframework.web.servlet.config.annotation.CorsRegistry; 5import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 6 7/** 8 * 跨域配置 9 * @author Louis 10 * @date Nov 28, 2018 11 */ 12@Configuration 13public class CorsConfig implements WebMvcConfigurer { 14 15 @Override 16 public void addCorsMappings(CorsRegistry registry) { 17 registry.addMapping("/**") // 允许跨域访问的路径 18 .allowedOrigins("*") // 允许跨域访问的源 19 .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE") // 允许请求方法 20 .maxAge(168000) // 预检间隔时间 21 .allowedHeaders("*") // 允许头部设置 22 .allowCredentials(true); // 是否发送cookie 23 } 24}

Swagger配置类
Swagger配置类,除了常规配置外,加了一个令牌属性,可以在接口调用的时候传递令牌。
SwaggerConfig.java

1package com.louis.springboot.spring.security.config; 2import java.util.ArrayList; 3import java.util.List; 4 5import org.springframework.context.annotation.Bean; 6import org.springframework.context.annotation.Configuration; 7 8import springfox.documentation.builders.ApiInfoBuilder; 9import springfox.documentation.builders.ParameterBuilder; 10import springfox.documentation.builders.PathSelectors; 11import springfox.documentation.builders.RequestHandlerSelectors; 12import springfox.documentation.schema.ModelRef; 13import springfox.documentation.service.ApiInfo; 14import springfox.documentation.service.Parameter; 15import springfox.documentation.spi.DocumentationType; 16import springfox.documentation.spring.web.plugins.Docket; 17import springfox.documentation.swagger2.annotations.EnableSwagger2; 18 19/** 20 * Swagger配置 21 * @author Louis 22 * @date Nov 28, 2018 23 */ 24@Configuration 25@EnableSwagger2 26public class SwaggerConfig { 27 28 @Bean 29 public Docket createRestApi(){ 30 // 添加请求参数,我们这里把token作为请求头部参数传入后端 31 ParameterBuilder parameterBuilder = new ParameterBuilder(); 32 List<Parameter> parameters = new ArrayList<Parameter>(); 33 parameterBuilder.name("Authorization").description("令牌").modelRef(new ModelRef("string")).parameterType("header") 34 .required(false).build(); 35 parameters.add(parameterBuilder.build()); 36 return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.any()) 37 .paths(PathSelectors.any()).build().globalOperationParameters(parameters); 38 } 39 40 private ApiInfo apiInfo(){ 41 return new ApiInfoBuilder().build(); 42 } 43 44}

加了令牌属性后的 Swagger 接口调用界面。

安全配置类
下面这个配置类是Spring Security的关键配置。
在这个配置类中,我们主要做了以下几个配置:
1. 访问路径URL的授权策略,如登录、Swagger访问免登录认证等
2. 指定了登录认证流程过滤器 JwtLoginFilter,由它来触发登录认证
3. 指定了自定义身份认证组件 JwtAuthenticationProvider,并注入 UserDetailsService
4. 指定了访问控制过滤器 JwtAuthenticationFilter,在授权时解析令牌和设置登录状态
5. 指定了退出登录处理器,因为是前后端分离,防止内置的登录处理器在后台进行跳转
WebSecurityConfig.java

1package com.louis.springboot.spring.security.config; 2 3import org.springframework.beans.factory.annotation.Autowired; 4import org.springframework.context.annotation.Bean; 5import org.springframework.context.annotation.Configuration; 6import org.springframework.http.HttpMethod; 7import org.springframework.security.authentication.AuthenticationManager; 8import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 9import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 10import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 12import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 13import org.springframework.security.core.userdetails.UserDetailsService; 14import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 15import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; 16 17import com.louis.springboot.spring.security.security.JwtAuthenticationFilter; 18import com.louis.springboot.spring.security.security.JwtAuthenticationProvider; 19import com.louis.springboot.spring.security.security.JwtLoginFilter; 20 21/** 22 * Security Config 23 * @author Louis 24 * @date Nov 28, 2018 25 */ 26@Configuration 27@EnableWebSecurity 28@EnableGlobalMethodSecurity(prePostEnabled = true) 29public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 30 31 @Autowired 32 private UserDetailsService userDetailsService; 33 34 @Override 35 public void configure(AuthenticationManagerBuilder auth) throws Exception { 36 // 使用自定义登录身份认证组件 37 auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService)); 38 } 39 40 @Override 41 protected void configure(HttpSecurity http) throws Exception { 42 // 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf 43 http.cors().and().csrf().disable() 44 .authorizeRequests() 45 // 跨域预检请求 46 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() 47 // 登录URL 48 .antMatchers("/login").permitAll() 49 // swagger 50 .antMatchers("/swagger-ui.html").permitAll() 51 .antMatchers("/swagger-resources").permitAll() 52 .antMatchers("/v2/api-docs").permitAll() 53 .antMatchers("/webjars/springfox-swagger-ui/**").permitAll() 54 // 其他所有请求需要身份认证 55 .anyRequest().authenticated(); 56 // 退出登录处理器 57 http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()); 58 // 开启登录认证流程过滤器 59 http.addFilterBefore(new JwtLoginFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class); 60 // 访问控制时登录状态检查过滤器 61 http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class); 62 } 63 64 @Bean 65 @Override 66 public AuthenticationManager authenticationManager() throws Exception { 67 return super.authenticationManager(); 68 } 69 70}

登录认证触发过滤器
JwtLoginFilter 是在通过访问 /login 的POST请求是被首先被触发的过滤器,默认实现是 UsernamePasswordAuthenticationFilter,它继承了 AbstractAuthenticationProcessingFilter,抽象父类的 doFilter 定义了登录认证的大致操作流程,这里我们的 JwtLoginFilter 继承了 UsernamePasswordAuthenticationFilter,并进行了两个主要内容的定制。
1. 覆写认证方法,修改用户名、密码的获取方式,具体原因看代码注释
2. 覆写认证成功后的操作,移除后台跳转,添加生成令牌并返回给客户端
JwtLoginFilter.java

1package com.louis.springboot.spring.security.security; 2 3import java.io.BufferedReader; 4import java.io.IOException; 5import java.io.InputStream; 6import java.io.InputStreamReader; 7import java.nio.charset.Charset; 8 9import javax.servlet.FilterChain; 10import javax.servlet.ServletException; 11import javax.servlet.ServletRequest; 12import javax.servlet.ServletResponse; 13import javax.servlet.http.HttpServletRequest; 14import javax.servlet.http.HttpServletResponse; 15 16import org.springframework.security.authentication.AuthenticationManager; 17import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent; 18import org.springframework.security.core.Authentication; 19import org.springframework.security.core.AuthenticationException; 20import org.springframework.security.core.context.SecurityContextHolder; 21import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 22 23import com.alibaba.fastjson.JSON; 24import com.alibaba.fastjson.JSONObject; 25import com.louis.springboot.spring.security.utils.HttpUtils; 26import com.louis.springboot.spring.security.utils.JwtTokenUtils; 27 28/** 29 * 启动登录认证流程过滤器 30 * @author Louis 31 * @date Nov 28, 2018 32 */ 33public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter { 34 35 public JwtLoginFilter(AuthenticationManager authManager) { 36 setAuthenticationManager(authManager); 37 } 38 39 @Override 40 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 41 throws IOException, ServletException { 42 // POST 请求 /login 登录时拦截, 由此方法触发执行登录认证流程,可以在此覆写整个登录认证逻辑 43 super.doFilter(req, res, chain); 44 } 45 46 @Override 47 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { 48 // 可以在此覆写尝试进行登录认证的逻辑,登录成功之后等操作不再此方法内 49 // 如果使用此过滤器来触发登录认证流程,注意登录请求数据格式的问题 50 // 此过滤器的用户名密码默认从request.getParameter()获取,但是这种 51 // 读取方式不能读取到如 application/json 等 post 请求数据,需要把 52 // 用户名密码的读取逻辑修改为到流中读取request.getInputStream() 53 54 String body = getBody(request); 55 JSONObject jsonObject = JSON.parseObject(body); 56 String username = jsonObject.getString("username"); 57 String password = jsonObject.getString("password"); 58 59 if (username == null) { 60 username = ""; 61 } 62 63 if (password == null) { 64 password = ""; 65 } 66 67 username = username.trim(); 68 69 JwtAuthenticatioToken authRequest = new JwtAuthenticatioToken(username, password); 70 71 // Allow subclasses to set the "details" property 72 setDetails(request, authRequest); 73 74 return this.getAuthenticationManager().authenticate(authRequest); 75 76 } 77 78 @Override 79 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, 80 Authentication authResult) throws IOException, ServletException { 81 // 存储登录认证信息到上下文 82 SecurityContextHolder.getContext().setAuthentication(authResult); 83 // 记住我服务 84 getRememberMeServices().loginSuccess(request, response, authResult); 85 // 触发事件监听器 86 if (this.eventPublisher != null) { 87 eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); 88 } 89 // 生成并返回token给客户端,后续访问携带此token 90 JwtAuthenticatioToken token = new JwtAuthenticatioToken(null, null, JwtTokenUtils.generateToken(authResult)); 91 HttpUtils.write(response, token); 92 } 93 94 /** 95 * 获取请求Body 96 * @param request 97 * @return 98 */ 99 public String getBody(HttpServletRequest request) { 100 StringBuilder sb = new StringBuilder(); 101 InputStream inputStream = null; 102 BufferedReader reader = null; 103 try { 104 inputStream = request.getInputStream(); 105 reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); 106 String line = ""; 107 while ((line = reader.readLine()) != null) { 108 sb.append(line); 109 } 110 } catch (IOException e) { 111 e.printStackTrace(); 112 } finally { 113 if (inputStream != null) { 114 try { 115 inputStream.close(); 116 } catch (IOException e) { 117 e.printStackTrace(); 118 } 119 } 120 if (reader != null) { 121 try { 122 reader.close(); 123 } catch (IOException e) { 124 e.printStackTrace(); 125 } 126 } 127 } 128 return sb.toString(); 129 } 130}

登录控制器
除了使用上面的登录认证过滤器拦截 /login Post请求之外,我们也可以不使用上面的过滤器,通过自定义登录接口实现,只要在登录接口手动触发登录流程并生产令牌即可。
其实 Spring Security 的登录认证过程只需 调用 AuthenticationManager 的 authenticate(Authentication authentication) 方法,最终返回认证成功的 Authentication 实现类并存储到SpringContexHolder 上下文即可,这样后面授权的时候就可以从 SpringContexHolder 中获取登录认证信息,并根据其中的用户信息和权限信息决定是否进行授权。
LoginController.java

1package com.louis.springboot.spring.security.controller; 2 3import java.io.IOException; 4 5import javax.servlet.http.HttpServletRequest; 6 7import org.springframework.beans.factory.annotation.Autowired; 8import org.springframework.security.authentication.AuthenticationManager; 9import org.springframework.web.bind.annotation.PostMapping; 10import org.springframework.web.bind.annotation.RequestBody; 11import org.springframework.web.bind.annotation.RestController; 12 13import com.louis.springboot.spring.security.security.JwtAuthenticatioToken; 14import com.louis.springboot.spring.security.utils.SecurityUtils; 15import com.louis.springboot.spring.security.vo.HttpResult; 16import com.louis.springboot.spring.security.vo.LoginBean; 17 18/** 19 * 登录控制器 20 * @author Louis 21 * @date Nov 28, 2018 22 */ 23@RestController 24public class LoginController { 25 26 @Autowired 27 private AuthenticationManager authenticationManager; 28 29 /** 30 * 登录接口 31 */ 32 @PostMapping(value = "/login") 33 public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException { 34 String username = loginBean.getUsername(); 35 String password = loginBean.getPassword(); 36 37 // 系统登录认证 38 JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager); 39 40 return HttpResult.ok(token); 41 } 42 43}

注意:如果使用此登录控制器触发登录认证,需要禁用登录认证过滤器,即将 WebSecurityConfig 中的以下配置项注释即可,否则访问登录接口会被过滤拦截,执行不会再进入此登录接口,大家根据使用习惯二选一即可。
1// 开启登录认证流程过滤器,如果使用LoginController的login接口, 需要注释掉此过滤器,根据使用习惯二选一即可 2http.addFilterBefore(new JwtLoginFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
如下是登录认证的逻辑, 可以看到部分逻辑跟上面的登录认证过滤器差不多。
1. 执行登录认证过程,通过调用 AuthenticationManager 的 authenticate(token) 方法实现
2. 将认证成功的认证信息存储到上下文,供后续访问授权的时候获取使用
3. 通过JWT生成令牌并返回给客户端,后续访问和操作都需要携带此令牌
SecurityUtils.java

1/** 2 * Security相关操作 3 * @author Louis 4 * @date Nov 28, 2018 5 */ 6public class SecurityUtils { 7 8 /** 9 * 系统登录认证 10 * @param request 11 * @param username 12 * @param password 13 * @param authenticationManager 14 * @return 15 */ 16 public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) { 17 JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password); 18 token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); 19 // 执行登录认证过程 20 Authentication authentication = authenticationManager.authenticate(token); 21 // 认证成功存储认证信息到上下文 22 SecurityContextHolder.getContext().setAuthentication(authentication); 23 // 生成令牌并返回给客户端 24 token.setToken(JwtTokenUtils.generateToken(authentication)); 25 return token; 26 } ... 27}

令牌生成器
我们令牌是使用JWT生成的,下面是令牌生成的简要逻辑,详细参见源码。
JwtTokenUtils.java

1/** 2 * JWT工具类 3 * @author Louis 4 * @date Nov 28, 2018 5 */ 6public class JwtTokenUtils implements Serializable { 7 8 .../** 9 * 生成令牌 10 * 11 * @param userDetails 用户 12 * @return 令牌 13 */ 14 public static String generateToken(Authentication authentication) { 15 Map<String, Object> claims = new HashMap<>(3); 16 claims.put(USERNAME, SecurityUtils.getUsername(authentication)); 17 claims.put(CREATED, new Date()); 18 claims.put(AUTHORITIES, authentication.getAuthorities()); 19 return generateToken(claims); 20 } 21 22 /** 23 * 从数据声明生成令牌 24 * 25 * @param claims 数据声明 26 * @return 令牌 27 */ 28 private static String generateToken(Map<String, Object> claims) { 29 Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME); 30 return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact(); 31 } 32 33 ... 34 35}

登录身份认证组件
上面说到登录认证是通过调用 AuthenticationManager 的 authenticate(token) 方法实现的,而 AuthenticationManager 又是通过调用 AuthenticationProvider 的 authenticate(Authentication authentication) 来完成认证的,所以通过定制 AuthenticationProvider 也可以完成各种自定义的需求,我们这里只是简单的继承 DaoAuthenticationProvider 展示如何自定义,具体的大家可以根据各自的需求按需定制。
JwtAuthenticationProvider.java

1package com.louis.springboot.spring.security.security; 2 3import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 4import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 5import org.springframework.security.core.Authentication; 6import org.springframework.security.core.AuthenticationException; 7import org.springframework.security.core.userdetails.UserDetails; 8import org.springframework.security.core.userdetails.UserDetailsService; 9import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 10 11/** 12 * 身份验证提供者 13 * @author Louis 14 * @date Nov 20, 2018 15 */ 16public class JwtAuthenticationProvider extends DaoAuthenticationProvider { 17 18 public JwtAuthenticationProvider(UserDetailsService userDetailsService) { 19 setUserDetailsService(userDetailsService); 20 setPasswordEncoder(new BCryptPasswordEncoder()); 21 } 22 23 @Override 24 public Authentication authenticate(Authentication authentication) throws AuthenticationException { 25 // 可以在此处覆写整个登录认证逻辑 26 return super.authenticate(authentication); 27 } 28 29 @Override 30 protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) 31 throws AuthenticationException { 32 // 可以在此处覆写密码验证逻辑 33 super.additionalAuthenticationChecks(userDetails, authentication); 34 } 35 36}

认证信息获取服务
通过跟踪代码运行,我们发现像默认使用的 DaoAuthenticationProvider,在认证的使用都是通过一个叫 UserDetailsService 的来获取用户认证所需信息的。
AbstractUserDetailsAuthenticationProvider 定义了在 authenticate 方法中通过 retrieveUser 方法获取用户信息,子类 DaoAuthenticationProvider 通过 UserDetailsService 来进行获取, 一般情况,这个 UserDetailsService 需要我们自定义,实现从用户服务获取用户和权限信息封装到 UserDetails 的实现类。
AbstractUserDetailsAuthenticationProvider.java

1public Authentication authenticate(Authentication authentication) throws AuthenticationException { 2 3 ...if (user == null) { 4 cacheWasUsed = false; 5 try { 6 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); 7 } 8 9 ...return createSuccessAuthentication(principalToReturn, authentication, user); 10 }

DaoAuthenticationProvider.java

1protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) 2 throws AuthenticationException { 3 try { 4 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); 5 return loadedUser; 6 } 7 ... 8 }

我们自定义的 UserDetailsService,从我们的用户服务 UserService 中获取用户和权限信息。
UserDetailsServiceImpl.java

1package com.louis.springboot.spring.security.security; 2import java.util.List; 3import java.util.Set; 4import java.util.stream.Collectors; 5 6import org.springframework.beans.factory.annotation.Autowired; 7import org.springframework.security.core.GrantedAuthority; 8import org.springframework.security.core.userdetails.UserDetails; 9import org.springframework.security.core.userdetails.UserDetailsService; 10import org.springframework.security.core.userdetails.UsernameNotFoundException; 11import org.springframework.stereotype.Service; 12 13import com.louis.springboot.spring.security.model.User; 14import com.louis.springboot.spring.security.service.UserService; 15 16/** 17 * 用户登录认证信息查询 18 * @author Louis 19 * @date Nov 20, 2018 20 */ 21@Service 22public class UserDetailsServiceImpl implements UserDetailsService { 23 24 @Autowired 25 private UserService userService; 26 27 @Override 28 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 29 User user = userService.findByUsername(username); 30 if (user == null) { 31 throw new UsernameNotFoundException("该用户不存在"); 32 } 33 // 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口 34 Set<String> permissions = userService.findPermissions(username); 35 List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList()); 36 return new JwtUserDetails(username, user.getPassword(), grantedAuthorities); 37 } 38}

一般而言,定制 UserDetailsService 就可以满足大部分需求了,在 UserDetailsService 满足不了我们的需求的时候考虑定制 AuthenticationProvider。
如果直接定制UserDetailsService ,而不自定义 AuthenticationProvider,可以直接在配置文件 WebSecurityConfig 中这样配置。
WebSecurityConfig.java
1@Override 2 public void configure(AuthenticationManagerBuilder auth) throws Exception { 3 // 指定自定义的获取信息获取服务 4 auth.userDetailsService(userDetailsService) 5 }
用户认证信息
上面 UserDetailsService 加载好用户认证信息后会封装认证信息到一个 UserDetails 的实现类。
默认实现是 User 类,我们这里没有特殊需要,简单继承即可,复杂需求可以在此基础上进行拓展。
JwtUserDetails.java

1package com.louis.springboot.spring.security.security; 2import java.util.Collection; 3 4import org.springframework.security.core.GrantedAuthority; 5import org.springframework.security.core.userdetails.User; 6 7/** 8 * 安全用户模型 9 * @author Louis 10 * @date Nov 28, 2018 11 */ 12public class JwtUserDetails extends User { 13 14 private static final long serialVersionUID = 1L; 15 16 public JwtUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) { 17 this(username, password, true, true, true, true, authorities); 18 } 19 20 public JwtUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, 21 boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { 22 super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); 23 } 24 25}

用户操作代码
简单的用户模型,包含用户名密码。
User.java

1package com.louis.springboot.spring.security.model; 2 3/** 4 * 用户模型 5 * @author Louis 6 * @date Nov 28, 2018 7 */ 8public class User { 9 10 private Long id; 11 12 private String username; 13 14 private String password; 15 16 ... 17 18}

用户服务接口,只提供简单的用户查询和权限查询接口用于模拟。
UserService.java

1/** 2 * 用户管理 3 * @author Louis 4 * @date Nov 28, 2018 5 */ 6public interface UserService { 7 8 /** 9 * 根据用户名查找用户 10 * @param username 11 * @return 12 */ 13 User findByUsername(String username); 14 15 /** 16 * 查找用户的菜单权限标识集合 17 * @param userName 18 * @return 19 */ 20 Set<String> findPermissions(String username); 21 22}

用户服务实现,只简单获取返回模拟数据,实际场景根据情况从DAO获取即可。
SysUserServiceImpl.java

1@Service 2public class SysUserServiceImpl implements UserService { 3 4 @Override 5 public User findByUsername(String username) { 6 User user = new User(); 7 user.setId(1L); 8 user.setUsername(username); 9 String password = new BCryptPasswordEncoder().encode("123"); 10 user.setPassword(password); 11 return user; 12 } 13 14 @Override 15 public Set<String> findPermissions(String username) { 16 Set<String> permissions = new HashSet<>(); 17 permissions.add("sys:user:view"); 18 permissions.add("sys:user:add"); 19 permissions.add("sys:user:edit"); 20 return permissions; 21 } 22 23}

用户控制器,提供三个测试接口,其中权限列表中未包含删除接口定义的权限('sys:user:delete'),登录之后也将无权限调用。
UserController.java

1/** 2 * 用户控制器 3 * @author Louis 4 * @date Oct 31, 2018 5 */ 6@RestController 7@RequestMapping("user") 8public class UserController { 9 10 11 @PreAuthorize("hasAuthority('sys:user:view')") 12 @GetMapping(value="/findAll") 13 public HttpResult findAll() { 14 return HttpResult.ok("the findAll service is called success."); 15 } 16 17 @PreAuthorize("hasAuthority('sys:user:edit')") 18 @GetMapping(value="/edit") 19 public HttpResult edit() { 20 return HttpResult.ok("the edit service is called success."); 21 } 22 23 @PreAuthorize("hasAuthority('sys:user:delete')") 24 @GetMapping(value="/delete") 25 public HttpResult delete() { 26 return HttpResult.ok("the delete service is called success."); 27 } 28 29}

登录认证检查过滤器
访问接口的时候,登录认证检查过滤器 JwtAuthenticationFilter 会拦截请求校验令牌和登录状态,并根据情况设置登录状态。
JwtAuthenticationFilter.java

1/** 2 * 登录认证检查过滤器 3 * @author Louis 4 * @date Nov 20, 2018 5 */ 6public class JwtAuthenticationFilter extends BasicAuthenticationFilter { 7 8 @Autowired 9 public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { 10 super(authenticationManager); 11 } 12 13 @Override 14 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 15 // 获取token, 并检查登录状态 16 SecurityUtils.checkAuthentication(request); 17 chain.doFilter(request, response); 18 } 19 20}

SecurityUtils.java

1/** 2 * 获取令牌进行认证 3 * @param request 4 */ 5 public static void checkAuthentication(HttpServletRequest request) { 6 // 获取令牌并根据令牌获取登录认证信息 7 Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request); 8 // 设置登录认证信息到上下文 9 SecurityContextHolder.getContext().setAuthentication(authentication); 10 }

JwtTokenUtils.java

1/** 2 * 根据请求令牌获取登录认证信息 3 * @param token 令牌 4 * @return 用户名 5 */ 6 public static Authentication getAuthenticationeFromToken(HttpServletRequest request) { 7 Authentication authentication = null; 8 // 获取请求携带的令牌 9 String token = JwtTokenUtils.getToken(request); 10 if(token != null) { 11 // 请求令牌不能为空 12 if(SecurityUtils.getAuthentication() == null) { 13 // 上下文中Authentication为空 14 Claims claims = getClaimsFromToken(token); 15 if(claims == null) { 16 return null; 17 } 18 String username = claims.getSubject(); 19 if(username == null) { 20 return null; 21 } 22 if(isTokenExpired(token)) { 23 return null; 24 } 25 Object authors = claims.get(AUTHORITIES); 26 List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); 27 if (authors != null && authors instanceof List) { 28 for (Object object : (List) authors) { 29 authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority"))); 30 } 31 } 32 authentication = new JwtAuthenticatioToken(username, null, authorities, token); 33 } else { 34 if(validateToken(token, SecurityUtils.getUsername())) { 35 // 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息 36 authentication = SecurityUtils.getAuthentication(); 37 } 38 } 39 } 40 return authentication; 41 }

接口测试
找到 SpringSecurityApplication, 启动程序, 访问 http://localhost:8080/swagger-ui.html,进入Swagger。

我们先再未登录没有令牌的时候直接访问接口,发现都返回无权限,禁止访问的结果。

返回拒绝访问结果。

打开 LoginController,输入我们用户名和密码(username:amdin, password:123)

登录成功之后,成功返回令牌,如下图所示。

拷贝返回的令牌,粘贴到令牌参数输入框,再次访问 /user/edit 接口。

这个时候,成功的返回了结果: the edit service is called success.

同样的,拷贝返回的令牌,粘贴到令牌参数输入框,访问 /user/delete 接口。

发现还是返回拒绝访问的结果,那是因为访问这个接口需要 'sys:user:delete' 权限,而我们之前返回的权限列表中并没有包含,所以授权访问失败。

我们修改一下 SysUserServiceImpl,添加上‘sys:user:delete’ 权限,重新登录,再次访问一遍。

发现删除接口也可以访问了,记住务必要重新调用登录接口,获取令牌后拷贝到删除接口,再次访问删除接口。

到此,Spring Security 的讲解就结束了,本人知识有限,有不正确的地方,烦请指正,不尽感激。