理解Spring Security和实现动态授权
一、Spring Security架构#
SpringSecurity 是基于 Spring AOP 和 Servlet 过滤器的安全框架,提供全面的安全性解决方案。
Spring Security核心功能包括用户认证(Authentication)、用户授权(Authorization)和攻击防护3个部分:
- 用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程
- 用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限
- 攻击防护即防止伪造身份
Spring security大量使用了责任链和委托的代码设计风格,过滤器负责对请求进行安全校验和设置,某个过滤器涉及认证或授权时,认证/授权具体实现委派给认证管理器和授权管理器,过滤器不负责具体实现
SpringSecurity过滤器链采用的是责任链的设计模式,它有一条很长的过滤器链:
- ChannelProcessingFilter:ChannelProcessingFilter 通常是用来过滤哪些请求必须用 https 协议, 哪些请求必须用 http协议, 哪些请求随便用哪个协议都行
- ConcurrentSessionFilter:ConcurrentSessionFilter 主要用来判断session 是否过期以及更新最新的访问时间。
- WebAsyncManagerIntegrationFilter:WebAsyncManagerIntegrationFilter 用于集成SecurityContext到Spring异步执行机制中的
- SecurityContextPersistenceFilter:SecurityContextPersistenceFilter 主要控制 SecurityContext 的在一次请求中的生命周期 。请求来临时,创建SecurityContext 安全上下文信息,请求结束时清空 SecurityContextHolder 。SecurityContextPersistenceFilter 通过 HttpScurity#securityContext() 及相关方法引入其配置对象SecurityContextConfigurer 来进行配置。
- HeaderWriterFilter:HeaderWriterFilter 用来给 http 响应添加一些 Header ,比如 X-Frame-Options , X-XSSProtection, X-Content-Type-Options 。
- CorsFilter:跨域相关的过滤器。这是Spring MVC Java 配置和XML 命名空间 CORS 配置的替代方法, 仅对依赖于spring-web 的应用程序有用(不适用于spring-webmvc )或 要求在javax.servlet.Filter 级别进行CORS检查的安全约束链接。
- CsrfFilter:CsrfFilter 用于防止csrf 攻击,一旦开启了CSRF(默认开启),所有经过springsecurity的http请求以及资源都会被CsrfFilter拦截,仅仅GET|HEAD|TRACE|OPTIONS这4类方法会被放行,也就是说post、delete等方法依旧是被拦截掉。前后端使用json交互可以通过 HttpSecurity.csrf() 来关闭它。在你使用 jwt 等 token 技术时,是不需要这个的。
- LogoutFilter:LogoutFilter是处理注销的过滤器。
- OAuth2AuthorizationRequestRedirectFilter:和上面的有所不同,这个需要依赖 spring-scurity-oauth2 相关的模块。该过滤器是处理 OAuth2 请求首选重定向相关逻辑的。
- Saml2WebSsoAuthenticationRequestFilter:这个需要用到 Spring Security SAML 模块,这是一个基于 SMAL 的 SSO 单点登录请求认证过滤器。
- X509AuthenticationFilter:X509 认证过滤器。
- AbstractPreAuthenticatedProcessingFilter:AbstractPreAuthenticatedProcessingFilter 处理经过预先认证的身份验证请求的过滤器的基类,目的是从传入请求中提取主体上的必要信息
- CasAuthenticationFilter:CAS 单点登录认证过滤器 。依赖 Spring Security CAS 模块
- OAuth2LoginAuthenticationFilter:这个需要依赖 spring-scurity-oauth2 相关的模块,OAuth2 登录认证过滤器,处理通过 OAuth2进行认证登录的逻辑。
- Saml2WebSsoAuthenticationFilter:这个需要用到 Spring Security SAML 模块,这是一个基于 SMAL 的 SSO 单点登录认证过滤器。
- UsernamePasswordAuthenticationFilter:处理用户以及密码认证的核心过滤器,认证请求提交的username 和 password 被封装成token 进行一系列的认证
- OpenIDAuthenticationFilter:基于OpenID 认证协议的认证过滤器,需要依赖额外的相关模块才能启用它。
- DefaultLoginPageGeneratingFilter:生成默认的登录页,默认 /login 。
- DefaultLogoutPageGeneratingFilter:生成默认的退出页,默认 /logout 。
- DigestAuthenticationFilter:Digest 身份验证是 Web 应用程序中流行的可选的身份验证机制 。DigestAuthenticationFilter能够处理 HTTP 头中显示的摘要式身份验证凭据。
- BasicAuthenticationFilter:和Digest 身份验证一样都是Web 应用程序中流行的可选的身份验证机制 。BasicAuthenticationFilter 负责处理 HTTP 头中显示的基本身份验证凭据。这个 Spring Security的 Spring Boot 自动配置默认是启用的
- RequestCacheAwareFilter:用于用户认证成功后,重新恢复因为登录被打断的请求。当匿名访问一个需要授权的资源时会跳转到认证处理逻辑,此时请求被缓存。在认证逻辑处理完毕后,从缓存中获取最开始的资源请求进行再次请求
- SecurityContextHolderAwareRequestFilter:用来实现j2ee中 Servlet Api 一些接口方法, 比如 getRemoteUser 方法、isUserInRole 方法在使用 Spring Security 时其实就是通过这个过滤器来实现的
- JaasApiIntegrationFilter:适用于JAAS ( Java 认证授权服务)。 如果 SecurityContextHolder 中拥有的Authentication 是一个 JaasAuthenticationToken ,那么该 JaasApiIntegrationFilter 将使用包含在 JaasAuthenticationToken 中的 Subject 继续执行 FilterChain 。
- RememberMeAuthenticationFilter:处理记住我功能的过滤器
- AnonymousAuthenticationFilter:匿名认证过滤器。对于 Spring Security 来说,所有对资源的访问都是有 Authentication 的。对于无需登录( UsernamePasswordAuthenticationFilter )直接可以访问的资源,会授予其匿名用户身份
- SessionManagementFilter:Session 管理器过滤器,内部维护了一个SessionAuthenticationStrategy 用于管理 Session
- ExceptionTranslationFilter:主要来传输异常事件
- FilterSecurityInterceptor:这个过滤器决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限?这些判断和处理都是由该类进行的。如果你要实现动态权限控制就必须研究该类 。
- SwitchUserFilter:SwitchUserFilter 是用来做账户切换的
1.1 FilterChainProxy#
Spring Security Filter并不是直接嵌入到 Web Filter中的,而是通过 FilterChainProxy来统一管理 Spring Security Filter,FilterChainProxy本身则通过Spring提供的DelegatingFilterProxy代理过滤器嵌入到Servlet Filter 之中
1.1.1 DelegatingFilterProxy#
问题:在Spring MVC应用中,需要先启动Servlet容器再启动Spring容器,servlet过滤器位于spring容器前无法被spring容器管理(例如,无法在实现Filter接口的类中使用@Value和@Autowire注解)
Spring 提供了一个名为DelegatingFilterProxy
的Filter
实现。这个 Servet 在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间建立了桥接。Servlet 容器用自己的标准注册 Filter,但它对 Spring Bean 无感知。 DelegatingFilterProxy 通过标准 Servlet 容器机制注册到 Servlet 中,但将所有工作都委托给了实现 Filter 的 Spring Bean
DelegatingFilterProxy 伪代码如下:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy, delegate is an instance of Bean Filter0
Filter delegate = getFilterBean(someBeanName);
// delegate work to the Spring Bean
delegate.doFilter(request, response);
}
DelegatingFilterProxy通过过滤器名获取bean,并委托bean进行请求处理
1.1.2 FilterChainProxy#
Spring Security 对 Servlet 的支持包含在 FilterChainProxy。 FilterChainProxy 是 Spring Security 提供的一个特殊的 Filter。它通过过滤功能代理给 SecurityFilterChain 维护的一组Filter链
当请求到达 FilterChainProxy 之后,FilterChainProxy 会根据请求的路径,将请求转发到不同的 Spring Security Filters 上面去,不同的 Spring Security Filters 对应了不同的过滤器,也就是不同的请求将经过不同的过滤器
// FilterChainProxy源码
private final static String FILTER_APPLIED = FilterChainProxy.class.getName().concat(
".APPLIED");
private List<SecurityFilterChain> filterChains;
private FilterChainValidator filterChainValidator = new NullFilterChainValidator();
private HttpFirewall firewall = new StrictHttpFirewall();
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
}
finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
else {
doFilterInternal(request, response, chain);
}
}
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
private List<Filter> getFilters(HttpServletRequest request) {
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
-
filterChains
不是某个过滤器,而是多个过滤器链的集合 -
在 doFilter 方法中,正常来说,clearContext 参数每次都是 true,于是每次都先给 request 标记上 FILTER_APPLIED 属性,然后执行 doFilterInternal 方法去走过滤器,执行完毕后,最后在 finally 代码块中清除 SecurityContextHolder 中保存的用户信息,同时移除 request 中的标记
-
doFilterInternal
方法:- 调用 getFilters 方法找到过滤器链。该方法就是根据当前的请求,从 filterChains 中找到对应的过滤器链,然后由该过滤器链去处理请求
- 如果找出来的 filters 为 null,或者集合中没有元素,那就是说明当前请求不需要经过过滤器。直接执行 chain.doFilter ,这个就又回到原生过滤器中去了
- 如果查询到的 filters 中是有值的,那么这个 filters 集合中存放的就是我们要经过的过滤器链了。此时它会构造出一个虚拟的过滤器链 VirtualFilterChain 出来,并执行其中的 doFilter 方法
private static class VirtualFilterChain implements FilterChain { private final FilterChain originalChain; private final List<Filter> additionalFilters; private final FirewalledRequest firewalledRequest; private final int size; private int currentPosition = 0; private VirtualFilterChain(FirewalledRequest firewalledRequest, FilterChain chain, List<Filter> additionalFilters) { this.originalChain = chain; this.additionalFilters = additionalFilters; this.size = additionalFilters.size(); this.firewalledRequest = firewalledRequest; } @Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (currentPosition == size) { if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(firewalledRequest) + " reached end of additional filter chain; proceeding with original chain"); } // Deactivate path stripping as we exit the security filter chain this.firewalledRequest.reset(); originalChain.doFilter(request, response); } else { currentPosition++; Filter nextFilter = additionalFilters.get(currentPosition - 1); if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(firewalledRequest) + " at position " + currentPosition + " of " + size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'"); } nextFilter.doFilter(request, response, this); } } }
- VirtualFilterChain 类中首先声明了 5 个全局属性,originalChain 表示原生的过滤器链,也就是 Web Filter;additionalFilters 表示 Spring Security 中的过滤器链;firewalledRequest 表示当前请求;size 表示过滤器链中过滤器的个数;currentPosition 则是过滤器链遍历时候的下标
- doFilter 方法就是 Spring Security 中过滤器挨个执行的过程,如果
currentPosition == size
,表示过滤器链已经执行完毕,此时通过调用 originalChain.doFilter 进入到原生过滤链方法中,同时也退出了 Spring Security 过滤器链。否则就从 additionalFilters 取出 Spring Security 过滤器链中的一个个过滤器,挨个调用 doFilter 方法
1.1.3 SecurityFilterChain#
SecurityFilterChain中的 Filter是 Spring Bean,它们是注册 FilterChainProxy 中,而不是在 DelegatingFilterProxy 注册的。相比较直接向 Servlet 容器或 DelegatingFilterProxy 注册,FilterChainProxy 有许多优势。
- 首先,它为 Spring Security 提供了一个起点。如果您尝试对 Spring Security 进行故障 debug,那么在 FilterChainProxy 是个合适调试断点。
- 其次,由于 FilterChainProxy 是 Spring Security 的核心,它可以执行一些关键任务。例如,它可以清除 SecurityContext 以避免内存泄漏。它还可以用 Spring Security HttpFirewall 来保护应用免受某些类型的攻击。
- 此外,它在确定何时调用一个 SecurityFilterChain 方面提供了更大的灵活性。在 Servlet 容器中,Filter 只能根据 URL 模式匹配调用。但是,FilterChainProxy 可以使用 RequestMatcher 来匹配 HttpServletRequest 中任何内容来调用。
多个SecurityFilterChain, FilterChainProxy 使用第一个匹配的 SecurityFilterChain进行请求过滤
多过滤器链配置示例:
@Configuration
public class SecurityConfig {
@Configuration
@Order(1)
static class DefaultWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/foo/**")
.authorizeRequests()
.anyRequest().hasRole("admin")
.and()
.csrf().disable();
}
}
@Configuration
@Order(2)
static class DefaultWebSecurityConfig2 extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/bar/**")
.authorizeRequests()
.anyRequest().hasRole("user")
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable();
}
}
}
1.2 UsernamePasswordAuthenticationFilter#
UsernamePasswordAuthenticationFilter
负责表单认证,继承自AbstractAuthenticationProcessingFilter
抽象类。
其父类AbstractAuthenticationProcessingFilter
的doFilte
方法是一个模板方法,定义了认证的流程:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 根据请求路径,判断是否需要认证,不需要认证直接调用下个过滤器
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
// 返回请求认证,UsernamePasswordAuthenticationFilter实现此方法
Authentication authenticationResult = attemptAuthentication(request, response);
// token为空直接返回
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
// 会话相关策略设置
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
// 认证后是否继续调用下个过滤器,默认false
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 钩子,提供扩展点
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
UsernamePasswordAuthenticationFilter
实现attemptAuthentication
方法:
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 如果不是post请求,抛出异常
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();
// 构建token,现在token还未认证
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
// 把请求中的远传地址等信息设置到token中
setDetails(request, authRequest);
// 获取认证管理器,并委派认证管理器进行认证,返回认证token(此时,token携带认证是否成功信息)
return this.getAuthenticationManager().authenticate(authRequest);
}
1.3 AuthenticationManager#
在 Spring Security 中,用来处理身份认证的类是 AuthenticationManager,我们也称之为认证管理器。
AuthenticationManager 中规范了 Spring Security 的过滤器要如何执行身份认证,并在身份认证成功后返回一个经过认证的 Authentication 对象。AuthenticationManager 是一个接口,我们可以自定义它的实现,但是通常我们使用更多的是系统提供的 ProviderManager
1.3.1 ProviderManager#
ProviderManager 是的最常用的 AuthenticationManager 实现类。
ProviderManager 管理了一个 AuthenticationProvider 列表,每个 AuthenticationProvider 都是一个认证器,不同的 AuthenticationProvider 用来处理不同的 Authentication 对象的认证。一次完整的身份认证流程可能会经过多个 AuthenticationProvider。
每一个 ProviderManager 管理多个 AuthenticationProvider,同时每一个 ProviderManager 都可以配置一个 parent,如果当前的 ProviderManager 中认证失败了,还可以去它的 parent 中继续执行认证,所谓的 parent 实例,一般也是 ProviderManager,也就是 ProviderManager 的 parent 还是 ProviderManager
一个系统中,我们可以配置多个 HttpSecurity(多个过滤器链),而每一个 HttpSecurity 都有一个对应的 AuthenticationManager 实例(局部 AuthenticationManager),这些局部的 AuthenticationManager 实例都有一个共同的 parent,那就是全局的 AuthenticationManager。
ProviderManager
类认证方法authenticate
:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
// 获取当前认证管理器的所有Provider
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
// 如果token存在对应的Provider,则用此Provider进行认证
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
// 当前局部认证管理器没认证成功,则调用父认证管理器进行认证
if (result == null && parent != null) {
result = parentResult = parent.authenticate(authentication);
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
throw lastException;
}
- 首先获取 authentication 的 Class,判断当前 provider 是否支持该 authentication。
- 如果支持,则调用 provider 的 authenticate 方法开始做校验,校验完成后,会返回一个新的 Authentication。一会来和大家捋这个方法的具体逻辑。
- 这里的 provider 可能有多个,如果 provider 的 authenticate 方法没能正常返回一个 Authentication,则调用 provider 的 parent 的 authenticate 方法继续校验。
- copyDetails 方法则用来把旧的 Token 的 details 属性拷贝到新的 Token 中来。
- 接下来会调用 eraseCredentials 方法擦除凭证信息,也就是你的密码,这个擦除方法比较简单,就是将 Token 中的 credentials 属性置空。
- 最后通过 publishAuthenticationSuccess 方法将登录成功的事件广播出去
1.3.2 AuthenticationProvider#
AuthenticationProvider 定义了 Spring Security 中的验证逻辑:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
- authenticate 方法用来做验证,就是验证用户身份。
- supports 则用来判断当前的 AuthenticationProvider 是否支持对应的 Authentication
每个AuthenticationProvider和token一一对应,UsernamePasswordAuthenticationToken对应的Provider是DaoAuthenticationProvider,DaoAuthenticationProvider继承自AbstractUserDetailsAuthenticationProvider,其父类方法authenticate
定义认证逻辑:
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
}
- 首先从 Authentication 提取出登录用户名。
- 然后通过拿着 username 去调用 retrieveUser 方法去获取当前用户对象,这一步会调用我们自己在登录时候的写的 loadUserByUsername 方法(调用UserDetailsService的loadUserByUsername方法)
- 接下来调用 preAuthenticationChecks.check 方法去检验 user 中的各个账户状态属性是否正常,例如账户是否被禁用、账户是否被锁定、账户是否过期等等。
- additionalAuthenticationChecks 方法则是做密码比对的,
- 最后在 postAuthenticationChecks.check 方法中检查密码是否过期。
- 接下来有一个 forcePrincipalAsString 属性,这个是是否强制将 Authentication 中的 principal 属性设置为字符串,这个属性我们一开始在 UsernamePasswordAuthenticationFilter 类中其实就是设置为字符串的(即 username),但是默认情况下,当用户登录成功之后, 这个属性的值就变成当前用户这个对象了。之所以会这样,就是因为 forcePrincipalAsString 默认为 false,不过这块其实不用改,就用 false,这样在后期获取当前用户信息的时候反而方便很多。
- 最后,通过 createSuccessAuthentication 方法构建一个新的 UsernamePasswordAuthenticationToken。
DaoAuthenticationProvider
类主要实现了父类的additionalAuthenticationChecks
方法,定义如何比较密码:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
}
1.3.3 UserDetails#
public interface UserDetalls extends Serializble {
Collection<? extend GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccontNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
1.4 FilterSecurityInterceptor#
FilterSecurityInterceptor
决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
// 是否执行过该过滤器的标记
private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";
// 访问的资源元数据,默认是ExpressionBasedFilterInvocationSecurityMetadataSource
private FilterInvocationSecurityMetadataSource securityMetadataSource;
// 是否每次只请求一次该过滤器,例如在jsp进行转发的时候,会多次经过该过滤器,这个标记就是用来
// 判断此时需不需要spring-security再进行一次安全检查
private boolean observeOncePerRequest = true;
public void init(FilterConfig arg0) throws ServletException {
}
public void destroy() {
}
// 过滤方法,实际上是new一个FilterInvocation然后委托给它执行
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
// 核心调用
invoke(fi);
}
public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
return this.securityMetadataSource;
}
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
this.securityMetadataSource = newSource;
}
// 安全对象类型
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
// 如果request不为空并且已经执行过该过滤器并且observeOncePerRequest = true
// (只请求一次该过滤器)则过滤器继续往下走,不执行spring-security检查
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// 如果请求不为空并且只请求一次该过滤器,设置已经执行过该过滤器的标记
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 安全对象调用前进行权限判断
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// 过滤链继续执行
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
// 安全对象调用完成后,清理AbstractSecurityInterceptor的工作
super.finallyInvocation(token);
}
// 安全对象调用完成后,完成AbstractSecurityInterceptor的工作。
super.afterInvocation(token, null);
}
}
public boolean isObserveOncePerRequest() {
return observeOncePerRequest;
}
public void setObserveOncePerRequest(boolean observeOncePerRequest) {
this.observeOncePerRequest = observeOncePerRequest;
}
}
- 调用父类的
beforeInvocation
方法,执行授权关键操作:- 调用
obtainSecurityMetadataSource
方法获取当前请求需要的权限列表 - 调用
accessDecisionManager.decide
授权管理器方法进行授权
- 调用
1.5 FilterInvocationSecurityMetadataSource#
FilterInvocationSecurityMetadataSource
是一个标记接口,用来获取资源角色元数据,包含3个方法:
Collection getAttributes(Object object)
根据提供的受保护对象的信息,其实就是URI,获取该URI 配置的所有角色Collection getAllConfigAttributes()
获取全部角色boolean supports(Class<?> clazz)
对特定的安全对象是否提供 ConfigAttribute 支持
实现实例:
@Component
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService; //从数据库加载url及关联的角色
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
// 入参object就是受保护的对象
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 获取当前请求路径
String requestURI =
((FilterInvocation) object).getRequest().getRequestURI();
List<Menu> allMenu = menuService.getAllMenu();
// 遍历以查找当前请求路径所需要的角色/权限
for (Menu menu : allMenu) {
if (antPathMatcher.match(menu.getPattern(), requestURI)) {
String[] roles = menu.getRoles().stream()
.map(r -> r.getName()).toArray(String[]::new);
return SecurityConfig.createList(roles);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
如果当前请求的 URL 地址和数据库中 menu 表的所有项都匹配不上,那么最终返回 null。如果返回 null,那么受保护对象到底能不能访问呢?这就要看 AbstractSecurityInterceptor 对象中的 rejectPublicInvocations 属性了,该属性默认为 false,表示当 getAttributes 方法返回 null 时,允许访问受保护对象
1.6 AccessDecisionManager#
当用户想要访问某一个资源时,授权管理器通过持有的投票器根据用户的角色投出赞成或者反对票;
- 所谓投票器其实就是判断方法,授权管理器调用decide方法时会委派给持有的投票器进行判断
- 一个授权管理器可以持有多个投票器,如何综合每个投票器的结果做出判断就是所谓的表决机制
public interface AccessDecisionManager {
// 决策 主要通过其持有的 AccessDecisionVoter 来进行投票决策
void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws
AccessDeniedException,
InsufficientAuthenticationException;
// 以确定AccessDecisionManager是否可以处理传递的ConfigAttribute
boolean supports(ConfigAttribute attribute);
//以确保配置的AccessDecisionManager支持安全拦截器将呈现的安全 object 类型。
boolean supports(Class<?> clazz);
}
AccessDecisionManager
有三个默认实现(表决机制):
AffirmativeBased
基于肯定的决策器。 用户持有一个同意访问的角色就能通过ConsensusBased
基于共识的决策器。 用户持有同意的角色数量多于禁止的角色数UnanimousBased
基于一致的决策器。 用户持有的所有角色都同意访问才能放行
1.7 AccessDecisionVoter#
AccessDecisionManager
授权管理器依赖投票器AccessDecisionVoter
,AccessDecisionVoter
定义如下:
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
}
- 从常量名字中就可以看出每个常量的含义,1 表示赞成;0 表示弃权;-1 表示拒绝。
- 两个 supports 方法用来判断投票器是否支持当前请求。
- vote 则是具体的投票方法。在不同的实现类中实现。三个参数,authentication 表示当前登录主体;object 是一个 ilterInvocation,里边封装了当前请求;attributes 表示当前所访问的接口所需要的角色集合
常用投票器有:
- RoleVoter
- RoleHierarchyVoter
1.7.1 RoleVoter#
public int vote(Authentication authentication, Object object,
Collection<ConfigAttribute> attributes) {
if (authentication == null) {
return ACCESS_DENIED;
}
int result = ACCESS_ABSTAIN;
Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
for (ConfigAttribute attribute : attributes) {
if (this.supports(attribute)) {
result = ACCESS_DENIED;
for (GrantedAuthority authority : authorities) {
if (attribute.getAttribute().equals(authority.getAuthority())) {
return ACCESS_GRANTED;
}
}
}
}
return result;
}
如果当前登录主体为 null,则直接返回 ACCESS_DENIED 表示拒绝访问;否则就从当前登录主体 authentication 中抽取出角色信息,然后和 attributes 进行对比,如果具备 attributes 中所需角色的任意一种,则返回 ACCESS_GRANTED 表示允许访问
1.7.2 RoleHierarchyVoter#
RoleHierarchyVoter 是 RoleVoter 的一个子类,在 RoleVoter 角色判断的基础上,引入了角色分层管理,也就是角色继承
RoleHierarchyVoter接口定义如下:
public interface RoleHierarchy {
Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
Collection<? extends GrantedAuthority> authorities);
}
该接口中只有一个方法,返回值是一个可访问的权限集合
RoleHierarchy 接口有两个实现类:
- NullRoleHierarchy 这是一个空的实现,将传入的参数原封不动返回。
- RoleHierarchyImpl 这个会完成一些解析操作
public class RoleHierarchyImpl implements RoleHierarchy {
private static final Log logger = LogFactory.getLog(RoleHierarchyImpl.class);
private String roleHierarchyStringRepresentation = null;
private Map<GrantedAuthority, Set<GrantedAuthority>> rolesReachableInOneStepMap = null;
private Map<GrantedAuthority, Set<GrantedAuthority>> rolesReachableInOneOrMoreStepsMap = null;
public void setHierarchy(String roleHierarchyStringRepresentation) {
this.roleHierarchyStringRepresentation = roleHierarchyStringRepresentation;
logger.debug("setHierarchy() - The following role hierarchy was set: "
+ roleHierarchyStringRepresentation);
buildRolesReachableInOneStepMap();
buildRolesReachableInOneOrMoreStepsMap();
}
private void buildRolesReachableInOneStepMap() {
Pattern pattern = Pattern.compile("(\\s*([^\\s>]+)\\s*>\\s*([^\\s>]+))");
Matcher roleHierarchyMatcher = pattern
.matcher(this.roleHierarchyStringRepresentation);
this.rolesReachableInOneStepMap = new HashMap<GrantedAuthority, Set<GrantedAuthority>>();
while (roleHierarchyMatcher.find()) {
GrantedAuthority higherRole = new SimpleGrantedAuthority(
roleHierarchyMatcher.group(2));
GrantedAuthority lowerRole = new SimpleGrantedAuthority(
roleHierarchyMatcher.group(3));
Set<GrantedAuthority> rolesReachableInOneStepSet;
if (!this.rolesReachableInOneStepMap.containsKey(higherRole)) {
rolesReachableInOneStepSet = new HashSet<GrantedAuthority>();
this.rolesReachableInOneStepMap.put(higherRole,
rolesReachableInOneStepSet);
}
else {
rolesReachableInOneStepSet = this.rolesReachableInOneStepMap
.get(higherRole);
}
addReachableRoles(rolesReachableInOneStepSet, lowerRole);
logger.debug("buildRolesReachableInOneStepMap() - From role " + higherRole
+ " one can reach role " + lowerRole + " in one step.");
}
}
private void buildRolesReachableInOneOrMoreStepsMap() {
this.rolesReachableInOneOrMoreStepsMap = new HashMap<GrantedAuthority, Set<GrantedAuthority>>();
// iterate over all higher roles from rolesReachableInOneStepMap
for (GrantedAuthority role : this.rolesReachableInOneStepMap.keySet()) {
Set<GrantedAuthority> rolesToVisitSet = new HashSet<GrantedAuthority>();
if (this.rolesReachableInOneStepMap.containsKey(role)) {
rolesToVisitSet.addAll(this.rolesReachableInOneStepMap.get(role));
}
Set<GrantedAuthority> visitedRolesSet = new HashSet<GrantedAuthority>();
while (!rolesToVisitSet.isEmpty()) {
// take a role from the rolesToVisit set
GrantedAuthority aRole = rolesToVisitSet.iterator().next();
rolesToVisitSet.remove(aRole);
addReachableRoles(visitedRolesSet, aRole);
if (this.rolesReachableInOneStepMap.containsKey(aRole)) {
Set<GrantedAuthority> newReachableRoles = this.rolesReachableInOneStepMap
.get(aRole);
// definition of a cycle: you can reach the role you are starting from
if (rolesToVisitSet.contains(role)
|| visitedRolesSet.contains(role)) {
throw new CycleInRoleHierarchyException();
}
else {
// no cycle
rolesToVisitSet.addAll(newReachableRoles);
}
}
}
this.rolesReachableInOneOrMoreStepsMap.put(role, visitedRolesSet);
logger.debug("buildRolesReachableInOneOrMoreStepsMap() - From role " + role
+ " one can reach " + visitedRolesSet + " in one or more steps.");
}
}
}
-
用户传入的字符串变量(继承关系字符串)设置给 roleHierarchyStringRepresentation 属性,然后通过 buildRolesReachableInOneStepMap 和 buildRolesReachableInOneOrMoreStepsMap 方法完成对角色层级的解析
-
buildRolesReachableInOneStepMap 方法用来将角色关系解析成一层一层的形式
假设角色继承关系是
ROLE_A > ROLE_B \n ROLE_C > ROLE_D \n ROLE_C > ROLE_E
,Map 中的数据是这样- A-->B
- C-->[D,E]
假设角色继承关系是
ROLE_A > ROLE_B > ROLE_C > ROLE_D
,Map 中的数据是这样:- A-->B
- B-->C
- C-->D
-
buildRolesReachableInOneOrMoreStepsMap 方法则是对 rolesReachableInOneStepMap 集合进行再次解析,将角色的继承关系拉平。经过 buildRolesReachableInOneOrMoreStepsMap 方法解析之后,新的 Map 中保存的数据如下:
-
A-->[B、C、D]
-
B-->[C、D]
-
C-->D
-
1.8 ObjectPostProcessor#
在 Spring Security 中,由于框架本身大量采用了 Java 配置,并且没有将对象的各个属性都暴露出来,这样做的本意是为了简化配置。然而这样带来的一个问题就是需要我们手动将 Bean 注册到 Spring 容器中去,ObjectPostProcessor 就是为了解决该问题。一旦将 Bean 注册到 Spring 容器中了,我们可以用ObjectPostProcessor
去增强一个 Bean 的功能,或者需修改一个 Bean 的属性
package org.springframework.security.config.annotation;
public interface ObjectPostProcessor<T> {
<O extends T> O postProcess(O object);
}
Spring Security 框架源码中,随处可见手动装配。Spring Security 中,过滤器链中的所有过滤器都是通过对应的 xxxConfigure 来进行配置的,而所有的 xxxConfigure 都是继承自 SecurityConfigurerAdapter,而在这些 xxxConfigure 的 configure 方法中,无一例外的都会让他们各自配置的管理器去 Spring 容器中走一圈,例如 AbstractAuthenticationFilterConfigurer#configure 方法:
public void configure(B http) throws Exception {
...
...
F filter = postProcess(authFilter);
http.addFilter(filter);
}
例如,权限管理本身是由 FilterSecurityInterceptor 控制的,系统默认的 FilterSecurityInterceptor 已经创建好了,而且我也没办法修改它的属性,那么怎么办呢?我们可以利用 withObjectPostProcessor 方法,去修改 FilterSecurityInterceptor 中的相关属性
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
return object;
}
})
.and()
...
}
}
上面这个配置生效的原因之一是因为 FilterSecurityInterceptor 在创建成功后,会重走一遍 postProcess 方法,这里通过重写 postProcess 方法就能实现属性修改
二、自定义登录认证#
2.1 数据库建表#
-- ----------------------------
-- Table structure for menu
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`mid` bigint(20) NOT NULL COMMENT '菜单ID',
`pattern` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '菜单URL',
PRIMARY KEY (`mid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for menu_role
-- ----------------------------
DROP TABLE IF EXISTS `menu_role`;
CREATE TABLE `menu_role` (
`id` bigint(20) NOT NULL COMMENT 'ID',
`mid` bigint(20) NOT NULL COMMENT '菜单ID',
`rid` bigint(20) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单-角色权限映射表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`rid` bigint(20) NOT NULL COMMENT '角色ID',
`name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名称',
`note` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色描述',
PRIMARY KEY (`rid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`uid` bigint(20) NOT NULL COMMENT '用户ID',
`username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户帐号',
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户密码',
`enabled` tinyint(4) NOT NULL COMMENT '帐号是否启用',
`locked` tinyint(4) NOT NULL COMMENT '帐号是否锁定',
PRIMARY KEY (`uid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` bigint(20) NOT NULL COMMENT 'ID',
`uid` bigint(20) NOT NULL COMMENT '用户ID',
`rid` bigint(20) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户-角色映射表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
2.2 创建实体类和服务#
2.1 实体类(省略set/get方法)#
-
User类
@TableName(value ="user") public class User implements Serializable { @TableId(value = "uid") @TableField(value = "username") private String username; @TableField(value = "password") private String password; @TableField(value = "enabled") private Integer enabled; @TableField(value = "locked") private Integer locked; }
-
UserDetail类(主要用于loadUserByUsername方法)
@AllArgsConstructor @NoArgsConstructor public class UserDetail implements UserDetails { private Long uid; private String username; private String password; private Integer enabled; private Integer locked; private List<Role> roles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles.stream() .map(r -> new SimpleGrantedAuthority(r.getName())) .collect(Collectors.toList()); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return locked < 1 ? true : false; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled > 0 ? true : false; } }
-
MenuDetail类(主要用于动态权限)
@Data @AllArgsConstructor @NoArgsConstructor public class MenuDetail { private Long mid; private String pattern; private List<Role> roles; }
-
Role类
@TableName(value ="role") public class Role implements Serializable { @TableId(value = "rid") private Long rid; @TableField(value = "name") private String name; @TableField(value = "note") private String note; }
-
Menu类(资源菜单)
@TableName(value ="menu") public class Menu implements Serializable { @TableId(value = "mid") private Long mid; @TableField(value = "pattern") private String pattern; }
-
UserRole类
user-role关联表,略
-
MenuRole类
menu-role关联表,略
2.3 编写服务层#
-
UserService
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserRoleMapper userRoleMapper; @Override public ResultVO regist(User user) { String username = user.getUsername(); QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("username", username); List<User> users = userMapper.selectList(wrapper); if (user == null || users.size() == 0) { user.setEnabled(1); user.setLocked(0); user.setPassword(passwordEncoder.encode(user.getPassword())); int i = userMapper.insert(user); if (i > 0) { user.setPassword(null); return new ResultVO(ResultStatus.OK, "注册成功", user); } else { return new ResultVO(ResultStatus.NO, "注册失败,请重新注册", null); } } else { return new ResultVO(ResultStatus.NO, "用户已存在!", null); } } @Override public ResultVO setRoles(long uid, List<Long> rids) { Long[] success = new Long[rids.size()]; boolean isSuccess = true; for (int i = 0;i < rids.size();i++) { Long rid = rids.get(i); UserRole userRole = new UserRole(); userRole.setUid(uid); userRole.setRid(rid); int j = userRoleMapper.insert(userRole); if (j > 0) { success[i] = userRole.getId(); } else { isSuccess = false; break; } } if (isSuccess) { return new ResultVO(ResultStatus.OK, "角色绑定成功", null); } else { for (int k = 0; k < success.length; k++) { userRoleMapper.deleteById(success[k]); } return new ResultVO(ResultStatus.NO, "角色绑定失败!", null); } } @Override public UserDetail loadUserByUsername(String username) { return userMapper.loadUserByUsername(username); } }
-
MenuService
@Service @Slf4j public class MenuServiceImpl implements MenuService { @Autowired private MenuMapper menuMapper; @Autowired private MenuRoleMapper menuRoleMapper; @Override public ResultVO save(Menu menu) { int i = menuMapper.insert(menu); if (i > 0) { return new ResultVO(ResultStatus.OK, "success", menu); } else { return new ResultVO(ResultStatus.NO, "fail!", null); } } @Override public ResultVO setRoles(long mid, List<Long> rids) { long[] success = new long[rids.size()]; boolean isSuccess = true; for (int i = 0; i < rids.size(); i++) { long rid = rids.get(i); MenuRole menuRole = new MenuRole(); menuRole.setMid(mid); menuRole.setRid(rid); int j = menuRoleMapper.insert(menuRole); if (j > 0) { success[i] = menuRole.getId(); } else { isSuccess = false; break; } } if (isSuccess) { return new ResultVO(ResultStatus.OK, "success", null); } else { for (int k = 0; k < success.length; k++) { menuRoleMapper.deleteById(success[k]); } return new ResultVO(ResultStatus.NO, "fail!", null); } } @Override public List<MenuDetail> queryAllMenuDetails() { return menuMapper.queryAllMenuDetails(); } }
2.4 认证相关配置#
2.4.1 配置userDetailsService#
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetail userDetail = userService.loadUserByUsername(username);
if (userDetail == null) {
throw new UsernameNotFoundException("用户不存在!");
}
return userDetail;
}
};
}
}
2.4.2 认证成功、失败回调#
-
认证成功回调
@Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json"); ResultVO result = new ResultVO(ResultStatus.OK, "登录成功", authentication); objectMapper.writeValue(httpServletResponse.getOutputStream(), result); } }
-
认证失败回调
@Component public class LoginFailHandler implements AuthenticationFailureHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json"); ResultVO result = new ResultVO(ResultStatus.NO, "登录失败", null); objectMapper.writeValue(httpServletResponse.getOutputStream(), result); } }
-
配置回调
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private LoginFailHandler loginFailHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests().anyRequest().authenticated() .and() .formLogin() .loginProcessingUrl("/login").permitAll() .successHandler(loginSuccessHandler) .failureHandler(loginFailHandler); } }
三、自定义动态授权#
动态授权配置分为两步:
- 实现FilterInvocationSecurityMetadataSource接口,返回请求资源所需要的权限
- 配置权限管理器,实现权限鉴定,有两种方式:
- 实现AccessDecisionManager接口,自定义投票逻辑,不依赖系统内置投票器,但实现权限继承比较麻烦
- 配置内置授权管理器和分层投票器,实现角色继承
3.1 实现FilterInvocationSecurityMetadataSource#
@Component
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private MenuService menuService;
@Autowired
private AntPathMatcher antPathMatcher;
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
// 获取当前保护对象的url
String requestURI = ((FilterInvocation) o).getRequest().getRequestURI();
// 查询所有url对应的角色
List<MenuDetail> menuDetails = menuService.queryAllMenuDetails();
// 查找当前请求所需要的角色
for (MenuDetail menuDetail : menuDetails) {
if (antPathMatcher.match(menuDetail.getPattern(), requestURI)) {
String[] list = menuDetail.getRoles().stream().map(r -> r.getName()).toArray(String[]::new);
return SecurityConfig.createList(list);
}
}
return null;
}
@Override
// 如果不为空,security启动时会做校验,一般直接返回null即可
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
// 类的isAssignableFrom判断当前类是否是入参的接口或父类
// 权限过滤器会封装请求、响应、调用链为FilterInvocation。所已本方法返回true
// 当方法返回true,才能调用getAttributes
return FilterInvocation.class.isAssignableFrom(aClass);
}
}
3.2 自定义AccessDecisionManager#
FilterInvocationSecurityMetadataSource返回值会作为给AccessDecisionManager.decide方法入参
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
@Override
/**
*
* @author weixia
* @date 2022/8/27
* @param authentication 当前请求的用户,包含当前用户所拥有的权限信息
* @param o 待保护对象
* @param collection 目标请求所需要的权限
*/
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
if (collection == null || collection.size() == 0) {
// 如果受保护对象不需要权限,则直接返回放行
return;
}
if (authentication == null) {
throw new AccessDeniedException("请用户登录再访问");
}
Iterator<ConfigAttribute> iterator = collection.iterator();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
while (iterator.hasNext()) {
ConfigAttribute configAttribute = iterator.next();
for (GrantedAuthority authority : authorities) {
if (configAttribute.getAttribute().equalsIgnoreCase(authority.getAuthority())) {
return;
}
}
throw new AccessDeniedException("请用户登录再访问");
}
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
3.3 使用内置管理器和投票器实现权限继承#
利用ObjectPostProcessor重写权限过滤器的属性:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomSecurityMetadataSource customSecurityMetadataSource;
@Autowired
private CustomAccessDecisionManager customAccessDecisionManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
// 可以注入系统内置管理器也可以注入自己实现的授权管理器
o.setAccessDecisionManager(accessDecisionManager());
o.setSecurityMetadataSource(customSecurityMetadataSource);
return o;
}
});
}
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy(
"ROLE_admin > ROLE_student\n" + "ROLE_admin > ROLE_teacher"
);
return roleHierarchy;
}
@Bean
public AffirmativeBased accessDecisionManager() {
RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy());
return new AffirmativeBased(
Arrays.asList(roleHierarchyVoter)
);
}
}
3.4 授权异常回调#
-
未认证用户访问受限资源异常
@Component public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { @Autowired private ObjectMapper objectMapper; @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json"); ResultVO result = new ResultVO(ResultStatus.NO, "该资源需要登录访问", null); objectMapper.writeValue(httpServletResponse.getOutputStream(), result); } }
-
访问受限资源时权限不足异常
@Component public class MyAccessDeniedHandler implements AccessDeniedHandler { @Autowired private ObjectMapper objectMapper; @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setContentType("application/json"); ResultVO result = new ResultVO(ResultStatus.NO, "权限不足!", null); objectMapper.writeValue(httpServletResponse.getOutputStream(), result); } }
-
配置回调
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAuthenticationEntryPoint myAuthenticationEntryPoint; @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests().anyRequest().authenticated() .and() .exceptionHandling() .authenticationEntryPoint(myAuthenticationEntryPoint) .accessDeniedHandler(myAccessDeniedHandler); } }
3.5 认证与动态授权完整配置#
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailHandler loginFailHandler;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
private CustomSecurityMetadataSource customSecurityMetadataSource;
@Autowired
private CustomAccessDecisionManager customAccessDecisionManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(accessDecisionManager());
o.setSecurityMetadataSource(customSecurityMetadataSource);
return o;
}
})
.and()
.formLogin()
.loginProcessingUrl("/login").permitAll()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailHandler)
.and()
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/user/regist");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetail userDetail = userService.loadUserByUsername(username);
if (userDetail == null) {
throw new UsernameNotFoundException("用户不存在!");
}
return userDetail;
}
};
}
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
public AntPathMatcher antPathMatcher() {
return new AntPathMatcher();
}
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy(
"ROLE_admin > ROLE_student\n" + "ROLE_admin > ROLE_teacher"
);
return roleHierarchy;
}
@Bean
public AffirmativeBased accessDecisionManager() {
RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy());
return new AffirmativeBased(
Arrays.asList(roleHierarchyVoter)
);
}
}
四、验证码校验#
UsernamePasswordAuthenticationFilter
过滤器默认只能读取键值对类型的POST请求,下面对该过滤器功能进行扩展,实现以下两个功能:
- 支持从json格式的数据中读取username、password
- 支持验证码登录(这一步也可以通过重写DaoAuthenticationProvider实现)
4.1 导入依赖#
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>5.8.5</version>
</dependency>
4.2 验证码服务#
生成的验证码保存在redis中:
@Service
@Slf4j
public class CaptchaServiceImpl implements CaptchaService {
@Autowired
private RedisTemplate redisTemplate;
public void createCode(String username, HttpServletResponse response) throws IOException {
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(200, 100);
String code = captcha.getCode();
redisTemplate.boundHashOps("captcha").put(username, code);
captcha.write(response.getOutputStream());
}
@Override
public String getCode(String username) {
String captcha = (String) redisTemplate.boundHashOps("captcha").get(username);
return captcha;
}
}
4.3 验证码生成控制器#
前端用户点击获取验证码按钮,发送携带用户名的post请求,后端响应验证码图片
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
@Autowired
private CaptchaService captchaService;
@PostMapping("/create")
public void create(String username, HttpServletResponse response) throws IOException {
captchaService.createCode(username, response);
return;
}
}
4.4 扩展表单认证过滤器#
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private ObjectMapper objectMapper;
private CaptchaService captchaService;
public LoginFilter(ObjectMapper objectMapper, CaptchaService captchaService) {
super();
this.objectMapper = objectMapper;
this.captchaService = captchaService;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String contentType = request.getContentType();
if (contentType.contains(MediaType.APPLICATION_JSON_VALUE) || contentType.contains(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
Map<String, String> map = new HashMap<>();
try {
map = objectMapper.readValue(request.getInputStream(), Map.class);
} catch (IOException e) {
e.printStackTrace();
}
String captcha = map.get("captcha");
String username = map.get(getUsernameParameter());
String password = map.get(getPasswordParameter());
// 验证码校验
checkCode(username, captcha);
// 构建未认证token
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} else {
String captcha = request.getParameter("captcha");
String username = this.obtainUsername(request);
checkCode(username, captcha);
return super.attemptAuthentication(request, response);
}
}
private void checkCode(String username, String captcha) {
if (username == null || captcha == null || ! captcha.equalsIgnoreCase(captchaService.getCode(username))) {
throw new AuthenticationServiceException("验证码错误");
}
}
}
4.5 配置过滤器#
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailHandler loginFailHandler;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
private CustomSecurityMetadataSource customSecurityMetadataSource;
@Autowired
private CustomAccessDecisionManager customAccessDecisionManager;
@Autowired
private CaptchaService captchaService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(accessDecisionManager());
o.setSecurityMetadataSource(customSecurityMetadataSource);
return o;
}
})
.and()
.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler);
}
public LoginFilter loginFilter() {
LoginFilter loginFilter = new LoginFilterobjectMapper(), captchaService)
try {
// 设置认证管理器
// authenticationManagerBean方法是当前配置类的一个方法,负载构建认证管理器
loginFilter.setAuthenticationManager(authenticationManagerBean());
} catch (Exception e) {
e.printStackTrace();
}
loginFilter.setFilterProcessesUrl("/login");
loginFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
loginFilter.setAuthenticationFailureHandler(loginFailHandler);
return loginFilter;
}
}
addFilterAt
方法把自定义的过滤器放置在UsernamePasswordAuthenticationFilter
前面loginFilter
初始化需要设置认证管理器,可以通过WebSecurityConfigurerAdapter.authenticationManagerBean
方法获取一个认证管理器
4.6 自定义过滤器配置注意事项#
开发者自定义的过滤器不用使用@Bean
或@Componse
注解交由Spring容器管理,使用new
形式进行配置。
如果过滤器作为bean,则security设置的全局资源忽略配置会失效,因为Spring自动把自己管理的bean过滤器注册到安全调用链中
五、JWT认证#
为了更好的支持分布式会话,把token -> userDetails
作为键值对储存到redis中,并设置有效期;
用户每次请求携带token,只要redis中能查询到token就认为用户已登录(不用校验token是否合法),这样的优势是:
- 实现了分布式会话
- 因为token储存在redis,可以方便的实现token自动续期和用户退出功能
5.1 Redis读写服务#
@Service
@Slf4j
public class JwtServiceImpl implements JwtService {
@Autowired
private RedisTemplate redisTemplate;
public String createToken(String username, String key) {
byte[] bytes = key.getBytes();
String token = new JWT().setPayload("name", username).setKey(bytes).sign();
return token;
}
public void save(String token, UserDetail UserDetail, long time) {
redisTemplate.boundValueOps(token).set(UserDetail);
redisTemplate.boundValueOps(token).expire(time, TimeUnit.MINUTES);
}
public UserDetail get(String token) {
UserDetail userDetail = (UserDetail) redisTemplate.boundValueOps(token).get();
return userDetail;
}
public void expire(String token, long time) {
redisTemplate.boundValueOps(token).expire(time, TimeUnit.MINUTES);
}
}
5.2 重写认证成功回调方法#
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private JwtService jwtService;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
String token = jwtService.createToken(authentication.getName(), "password");
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json");
httpServletResponse.setHeader("token", token);
jwtService.save(token, (UserDetail) authentication.getPrincipal(), 30);
ResultVO result = new ResultVO(ResultStatus.OK, "登录成功", authentication);
objectMapper.writeValue(httpServletResponse.getOutputStream(), result);
}
}
- authentication.getPrincipal方法返回我们通过loadUserByUsername方法返回的UserDetails对象
- redis以json序列化数据时,
UserDetail
类实现了UserDetails
接口,里面存在getAuthorities方法,序列化时会认为存在Authorities属性(其实不存在),使用@JsonIgore
忽略此get方法;如果不进行配置,序列化不会报错,但反序列化时由于不存在set方法会导致反序列化失败 - 为避免跨域,我们把token储存到header中
5.3 自定义认证过滤器#
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private JwtService jwtService;
public JwtAuthenticationFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = httpServletRequest.getHeader("token");
if (token == null ) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
UserDetail userDetail = jwtService.get(token);
if (userDetail == null) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
// 如果能获取到userdeatil说明认证成功,为token延长30分钟,实现自动续期功能
// 封装成usernamePasswordAuthenticationToken,放入SecurityContext中
jwtService.expire(token, 30);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetail, null, userDetail.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
- security的过滤器链会从SecurityContext上下文中读取Authentication,如果Authentication为空则匿名过滤器会创建匿名token;此后,授权管理器读取Authentication同资源所需要权限进行比对,判断是否放行
- 只要token存在,我们认为认证成功,同时每次认证后为token进行续期,实现token自动续期
- 如果要实现登出功能,只要重写登出过滤器的回调方法,从redis中把token删除就可以令用户手中的token失效
5.4 配置自定义过滤器#
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailHandler loginFailHandler;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
private CustomSecurityMetadataSource customSecurityMetadataSource;
@Autowired
private CustomAccessDecisionManager customAccessDecisionManager;
@Autowired
private JwtService jwtService;
@Autowired
private CaptchaService captchaService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(accessDecisionManager());
o.setSecurityMetadataSource(customSecurityMetadataSource);
return o;
}
})
.and()
.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(jwtAuthenticationFilter(), LoginFilter.class)
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/user/regist", "/captcha/**");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetail userDetail = userService.loadUserByUsername(username);
if (userDetail == null) {
throw new UsernameNotFoundException("用户不存在!");
}
return userDetail;
}
};
}
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
public AntPathMatcher antPathMatcher() {
return new AntPathMatcher();
}
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy(
"ROLE_admin > ROLE_student\n" + "ROLE_admin > ROLE_teacher"
);
return roleHierarchy;
}
@Bean
public AffirmativeBased accessDecisionManager() {
RoleHierarchyVoter roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy());
return new AffirmativeBased(
Arrays.asList(roleHierarchyVoter)
);
}
public LoginFilter loginFilter() {
LoginFilter loginFilter = new LoginFilter(objectMapper(), captchaService);
try {
// 设置认证管理器
// authenticationManagerBean方法是当前配置类的一个方法,负载构建认证管理器
loginFilter.setAuthenticationManager(authenticationManagerBean());
} catch (Exception e) {
e.printStackTrace();
}
loginFilter.setFilterProcessesUrl("/login");
loginFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
loginFilter.setAuthenticationFailureHandler(loginFailHandler);
return loginFilter;
}
public JwtAuthenticationFilter jwtAuthenticationFilter() {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(jwtService);
return jwtAuthenticationFilter;
}
}
- 由于是前后端分离,sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)设置不生成sessionid
- 过滤器通过new进行装配,避免全局资源忽略配置失效
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析