springsecurity和apach shiro 都是目前常用的为企业应用系统提供安全访问控制方案的框架,其中shiro相对于Spring Security 更加轻量级,配置容易,功能也相对简单。适用于传统SSM 项目。而spring security比shiro功能上要多一点,上手较难,但它和spring框架无缝对接,比较适用于springboot项目。
首先需要在idea中创建一个springboot项目:
spring boot版本默认选择2.2.6.勾选上spring web,mysqldriver,springsecurity,mybatis 依赖。
点击next后等待idea下载项目依赖完毕。
完成后发现idea自动给我们下载了很多springsecurit的相关依赖,springsecurity版本为5.2.2 查阅官方文档可知,我们仅需要添加
spring-boot-starter-security 这一个依赖即可在项目中使用springsecurity
从如下官方文档截图中可知,在springboot中使用springSecurity只要在pom文件中添加spring-boot-starter-security即可,springboot 会为我们自动生成一些springsecutiry的默认配置:
springboot为我们做了以下默认配置:
1.创建并注册了一个servlet过滤器:springSecurityFilterChain,这个过滤器负责了我们程序中的所有安全相关的功能,包括程序的url保护,提交的用户名密码验证,重定向到登录表单等等
2.所有和该应用程序的交互都需要一个被认证过的用户。创建了一个userDetailService类的 bean对象,其中用户名为“user”,随机生成的密码会在程序启动时输出到控制台上,通过该用户名及密码才能登录应用。
3.为程序自动生成了一个默认的登录表单页面,会用BCrypt方式为密码提供存储加密,并且提供了用户退出登录功能。
4.提供了针对CRSF攻击(跨站请求伪造)和sessionFixation(会话固定攻击)的防护
5.对请求头的一些集成安全措施:包括HTTP严格安全传输功能(告诉浏览器只能通过HTTPS访问当前资源, 禁止HTTP方式。),响应头content-type类型集成安全性配置(X-Content-Type-Options ),缓存控制(应用可以重新配置该项以便缓存静态资源),Xss(跨站脚本攻击)防护,响应头iframe点击劫持攻击的集成安全性配置。
6,实现了一些servletApi (HttpServletRequest) 的方法,包括:
getRemoteUser, getUserPrincipal, isUserInRole, login,logOut
仅靠springboot的默认配置肯定不够,我们需要新建 WebSecurityConfig 配置类,继承WebSecurityConfigAdapter类,WebSecurityConfigAdapter是springSecurity提供的默认配置适配器类,并重写三个configure方法。之后我们对 springsecurity 的个性化配置基本都会在这几个方法中进行
配置用户未认证请求处理方案
这时候我们直接启动项目访问localhost:8080/,可以看到访问路径直接跳转到了springSecurity默认提供的登陆表单页面:
这是由于第一次访问时session中没有记录登录信息,于是请求被拦截并默认跳转到登录页面,重定向url为/login。在前后端分离项目中,若用户没有登录,则应该返回json信息给前端以提示用户登录,而不是直接跳转,因此需要屏蔽 springSecurity 的这一默认行为。为实现该功能我们必须先了解框架内部默认跳转行为的实现机制。
看一下控制台的输出日志:
可以看到 “/” 请求路径似乎经过了15 个框架内部提供的过滤器,最后在 AffirmativeBased 类中抛出了 AccessDeniedException 异常:
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
然后在 ExceptionTranslationFilter 过滤器中捕获异常并处理:
else if (exception instanceof AccessDeniedException) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
logger.debug(
"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
exception);
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
logger.debug(
"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
exception);
accessDeniedHandler.handle(request, response,
(AccessDeniedException) exception);
}
}
看其中的 sendStartAuthentication 方法的逻辑:
protected void sendStartAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContextHolder.getContext().setAuthentication(null);
requestCache.saveRequest(request, response);
logger.debug("Calling Authentication entry point.");
authenticationEntryPoint.commence(request, response, reason);
}
最后一步调用了authenticationEntryPoint 类的commence方法,这是一个接口,debug一下,看看具体调用了哪个实现类:
发现是调用了 DelegatingAuthenticationEntryPoint 类的 commence 方法,
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
for (RequestMatcher requestMatcher : entryPoints.keySet()) {
if (logger.isDebugEnabled()) {
logger.debug("Trying to match using " + requestMatcher);
}
if (requestMatcher.matches(request)) {
AuthenticationEntryPoint entryPoint = entryPoints.get(requestMatcher);
if (logger.isDebugEnabled()) {
logger.debug("Match found! Executing " + entryPoint);
}
entryPoint.commence(request, response, authException);
return;
}
}
if (logger.isDebugEnabled()) {
logger.debug("No match found. Using default entry point " + defaultEntryPoint);
}
// No EntryPoint matched, use defaultEntryPoint
defaultEntryPoint.commence(request, response, authException);
}
接着debug,发现最终调用了 LoginUrlAuthenticationEntryPoint 的commence方法,接着调试:
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
String redirectUrl = null;
if (useForward) {
if (forceHttps && "http".equals(request.getScheme())) {
// First redirect the current request to HTTPS.
// When that request is received, the forward to the login page will be
// used.
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}
if (redirectUrl == null) {
String loginForm = determineUrlToUseForThisRequest(request, response,
authException);
if (logger.isDebugEnabled()) {
logger.debug("Server side forward to: " + loginForm);
}
RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
dispatcher.forward(request, response);
return;
}
}
else {
// redirect to login page. Use https if forceHttps true
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
}
redirectStrategy.sendRedirect(request, response, redirectUrl);
}
到这一步准备跳转至 “/login" ,基于以上分析,我们发现要想改变框架的默认跳转行为,首先我们要实现自己的 AuthenticationEntryPoint 类 并重写 commence 方法。其次,要让框架使用我们自定义的 AuthenticationEntryPoint 实现类。
我们接着分析:
首先还是回到 ExceptionTranslationFilter.sendStartAuthentication 方法中,之前调试的时候发现此时的 authenticationEntryPoint 实现类是 DelegatingAuthenticationEntryPoint ,我们要将这个类换成自定义的AuthenticationEntryPoint 实现类,而authenticationEntryPoint 是 ExceptionTranslationFilter 的一个属性,看一下它的构造方法:
public ExceptionTranslationFilter(AuthenticationEntryPoint authenticationEntryPoint,
RequestCache requestCache) {
Assert.notNull(authenticationEntryPoint,
"authenticationEntryPoint cannot be null");
Assert.notNull(requestCache, "requestCache cannot be null");
this.authenticationEntryPoint = authenticationEntryPoint;
this.requestCache = requestCache;
}
也就是说在 ExceptionTranslationFilter 被创建时 会设置其 authenticationEntryPoint 属性,看一下此时的调用堆栈信息
找到 ExceptionHandlingConfigurer 的 configure方法:
@Override
public void configure(H http) {
AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(
entryPoint, getRequestCache(http));
AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
http.addFilter(exceptionTranslationFilter);
}
在这个方法中实例化了一个 ExceptionTranslationFilter 对象 并传入一个 AuthenticationEntryPoint 对象 ,而这个对象是由getAuthenticationEntryPoint 方法 返回的:
AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
if (entryPoint == null) {
entryPoint = createDefaultEntryPoint(http);
}
return entryPoint;
}
这个方法首先判断 entryPoint 属性是否为空,不为空则返回,为空则创建一个默认的 entryPoint
private AccessDeniedHandler createDefaultDeniedHandler(H http) {
if (this.defaultDeniedHandlerMappings.isEmpty()) {
return new AccessDeniedHandlerImpl();
}
if (this.defaultDeniedHandlerMappings.size() == 1) {
return this.defaultDeniedHandlerMappings.values().iterator().next();
}
return new RequestMatcherDelegatingAccessDeniedHandler(
this.defaultDeniedHandlerMappings,
new AccessDeniedHandlerImpl());
}
因此我们可以自定义一个 entryPoint 并用来设置 ExceptionHandlingConfigurer 对象的 entryPoint 属性,同样的,还是在 ExceptionHandlingConfigurer 的构造方法上打断点,看看它的调用栈:
找到 HttpSecurity 的 exceptionHandling 方法
public ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling() throws Exception {
return getOrApply(new ExceptionHandlingConfigurer<>());
}
private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(
C configurer) throws Exception {
C existingConfig = (C) getConfigurer(configurer.getClass());
if (existingConfig != null) {
return existingConfig;
}
return apply(configurer);
}
首先调用 getConfigurer 方法 获取一个 ExceptionHandlingConfigurer ,若不存在则调用apply 方法 创建一个,getConfigurer 和apply 方法的源码就不再深入分析了,总的来说,这个方法 只在第一次调用的时候才会创建一个 ExceptionHandlingConfigurer ,而默认情况下 ExceptionHandlingConfigurer 的 entryPoint 属性为空,只能通过调用它的 authenticationEntryPoint 方法来设置:
public ExceptionHandlingConfigurer<H> authenticationEntryPoint(
AuthenticationEntryPoint authenticationEntryPoint) {
this.authenticationEntryPoint = authenticationEntryPoint;
return this;
}
因此我们可以在 WebSecurityConfig 配置类中的 configurer(HttpSecurity http) 方法中调用httpSecurity 的 exceptionHandling 方法,获取 ExceptionHandlingConfigurer 对象,然后调用它的 authenticationEntryPoint 方法设置自定义的 entryPoint,具体配置如下:
//自定义用户未登录时异常处理类
@Autowired
MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests().antMatchers("/login").permitAll();
http.authorizeRequests().anyRequest().authenticated();
http.formLogin().and().httpBasic();
http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint); //用户未登录的异常处理逻辑
http.csrf().disable();
}
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint
{
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException
{
response.setContentType("text/json;charset=utf-8");
PrintWriter printWriter = response.getWriter();
printWriter.write("用户未登陆!");
printWriter.flush();
printWriter.close();
}
}
重启应用访问 localhost:8080/ ,这次就不会再跳转到登录页面了
这样前端项目拿到json信息后可以再做页面跳转等相关操作。
按照如上的配置,显然在用户登录成功之前,自定义的 AuthenticationEntryPoint 会拦截所有的请求,这就会导致用户正常的登录请求也被拦截,因此我们需要单独配置正常登录请求不被拦截。
首先先我们要找出框架默认的登录请求路径。我们先把刚才的自定义 entryPoint 配置注释掉,重启项目访问localhost:8080/login, 此时跳转至默认的登录页面。之前说到,springSecurity默认创建了一个用户user用于登录,登录密码会在项目启动时打印到控制台。
输入用户名user,密码, 点击登录,看一下控制台输出:
可以看到,默认的登录请求路径为 “/login” 并且应该是个 Post 请求 .
接下类还是把自定义entryPoint 配置 加上,使用 PostMan 发送Post请求至 localhost:8080/login
和预想的一样,请求被拦截了,看一下控制台输出:
控制台输出和之前分析的基本类似,是在 AffirmativeBased 类中抛出了异常然后被 ExceptionTranslationFilter 捕获最终 跳转到 entryPoint 中。看一下 AffirmativeBased 的 代码 ,只有 vote 方法的返回值为-1 才会抛出异常,接着debug,进入vote 方法:
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
public int vote(Authentication authentication, FilterInvocation fi,
Collection<ConfigAttribute> attributes) {
assert authentication != null;
assert fi != null;
assert attributes != null;
WebExpressionConfigAttribute weca = findConfigAttribute(attributes);
if (weca == null) {
return ACCESS_ABSTAIN;
}
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
fi);
ctx = weca.postProcess(ctx, fi);
return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
: ACCESS_DENIED;
}
最后会调用 ExpressionUtils 类的 evaluateAsBoolean 方法,
在这里调用了 Spel 表达式的 getValue 方法 获取表达式的值,表达式 exp 为 “authenticated”
ctx 为standardEvaluationContext 类对象,其中的rootObject 为 WebExpressionRoot 对象,按照spel表达式的用法,这里会调用 WebExpressionRoot 对象的 isAuthenticated 方法
public final boolean isAnonymous() {
return trustResolver.isAnonymous(authentication);
}
public final boolean isAuthenticated() {
return !isAnonymous();
}
isAuthenticated() 方法其实就是调用了 isAnonymous 方法,只不过对其结果进行取反,接着看 isAnonymous 方法:
public boolean isAnonymous(Authentication authentication) {
if ((anonymousClass == null) || (authentication == null)) {
return false;
}
return anonymousClass.isAssignableFrom(authentication.getClass());
}
这个方法的大意是,如果 autnenticatin 对象类是 anonymousClass 的子类,则返回true,相应的 isAuthenticated 方法会返回false,最终导致 vote 方法返回-1。 而 anonymousClass 为 AnonymousAuthenticationToken。
因此 expression 的值是决定请求是否被拦截的关键,
在 SecurityExpressionRoot 中 有一个 permitAll 属性 其值始终为true。这个属性的作用我们之后再分析。
我们从 vote 方法开始按照调用堆栈,往上找 expression 的来源:
public int vote(Authentication authentication, FilterInvocation fi,
Collection<ConfigAttribute> attributes) {
assert authentication != null;
assert fi != null;
assert attributes != null;
WebExpressionConfigAttribute weca = findConfigAttribute(attributes);
if (weca == null) {
return ACCESS_ABSTAIN;
}
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
fi);
ctx = weca.postProcess(ctx, fi);
return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
: ACCESS_DENIED;
}
在vote 方法中 WebExpressionConfigAttribute 变量 通过 findConfigAttribute 方法获取。
private WebExpressionConfigAttribute findConfigAttribute(
Collection<ConfigAttribute> attributes) {
for (ConfigAttribute attribute : attributes) {
if (attribute instanceof WebExpressionConfigAttribute) {
return (WebExpressionConfigAttribute) attribute;
}
}
return null;
}
在 findConfigAttribute 方法 中 遍历 attributes 变量,如果 当前遍历的 ConfigAttribute 类型是 WebExpressionConfigAttribute 则强制类型转换后返回。而expression 是通过 调用 WebExpressionConfigAttribute 的 .getAuthorizeExpression() 获取的,因此 expression 是 WebExpressionConfigAttribute 类的 authorizeExpression 属性值。
接着往上找 ConfigAttribute 集合 的来源:
在 AbstractSecurityInterceptor 类 的 beforeInvocation 方法中 调用了 DefaultFilterInvocationSecurityMetadataSource 类的 getAttributes 方法获取 attributes ,进入这个方法:
public Collection<ConfigAttribute> getAttributes(Object object) {
final HttpServletRequest request = ((FilterInvocation) object).getRequest();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
.entrySet()) {
if (entry.getKey().matches(request)) {
return entry.getValue();
}
}
return null;
}
requesretMap 是 Map<RequestMatcher, Collection> 类型,
遍历 requestMap 调用 RequestMatcher的matches 方法,匹配 request 对象
匹配成功则 返回value 值即 ConfigAttribute集合。
当前 requestMap 中只有唯一的 key AnyRequestMacher 对象,其 matches 方法始终返回 true ,可以匹配所有请求路径,因此直接返回其value 值 WebExpressionConfigAttribute 对象。
requestMap 是 DefaultFilterInvocationSecurityMetadataSource 类的属性。在其构造方法中 会将 requestMap 注入进来,在构造方法打断点,重启应用查看调用堆栈,找到 requestMap 的初始化方法:
在 ExpressionUrlAuthorizationConfigurer 类的 createMetadataSource 方法中,调用 REGISTRY.createRequestMap() 方法,该方法其实是遍历了 REGISTRY 的 urlMapping 属性来创建 requestMap,而此时 urlMapping 属性已经有值了,REGISTRY 会在 ExpressionUrlAuthorizationConfigurer 的构造方法中进行注入,同样的套路 在构造方法打断点,看调用堆栈找到 REGISTRY 的 urlMapping 属性的赋值时机:
WebSecurityConfigurerAdapter 是我们自定义 配置类的父类。在它的 configure(HttpSecurity http) 方法中 调用了httpSecurity 的 authorizeRequests() 方法,这个方法初始化了 ExpressionUrlAuthorizationConfigurer
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests()
throws Exception {
ApplicationContext context = getContext();
return getOrApply(new ExpressionUrlAuthorizationConfigurer<>(context))
.getRegistry();
}
类似于之前分析过的,getOrApply 方法只在第一次调用的时候会初始化 ExpressionUrlAuthorizationConfigurer 对象,之后调用时仅仅获取已存在的实例,获取到之后在返回 ExpressionUrlAuthorizationConfigurer 对象的 REGISTRY 属性。但此时 REGISTRY 中的 urlMapping 属性为空,说明 urlMapping 属性是在之后的处理中被初始化的。
因此接着看 anyRequest() 方法:
public C anyRequest() {
Assert.state(!this.anyRequestConfigured, "Can't configure anyRequest after itself");
C configurer = requestMatchers(ANY_REQUEST);
this.anyRequestConfigured = true;
return configurer;
}
public C requestMatchers(RequestMatcher... requestMatchers) {
Assert.state(!this.anyRequestConfigured, "Can't configure requestMatchers after anyRequest");
return chainRequestMatchers(Arrays.asList(requestMatchers));
}
protected final C chainRequestMatchers(List<RequestMatcher> requestMatchers) {
this.unmappedMatchers = requestMatchers;
return chainRequestMatchersInternal(requestMatchers);
}
@Override
protected final AuthorizedUrl chainRequestMatchersInternal(
List<RequestMatcher> requestMatchers) {
return new AuthorizedUrl(requestMatchers);
}
这个方法的作用主要是传入了一个 AnyRequestMatcher 类的实例,将其放入一个 list 中,然后构造一个 AuthorizedUrl 对象,并将这个 list 赋给 AuthorizedUrl 的 requestMatchers 属性 然后返回 AuthorizedUrl 对象。
AuthorizedUrl 类是 ExpressionUrlAuthorizationConfigurer 的内部类。它的具体源码就不再深入分析了
接着来看 authenticated() 方法:
public ExpressionInterceptUrlRegistry authenticated() {
return access(authenticated);
}
access 方法的参数 authenticated 是一个String 类型的常量。
总算是找到了 “authenticated” 的来源,access 方法传入了“authenticated” 字符串,
public ExpressionInterceptUrlRegistry access(String attribute) {
if (not) {
attribute = "!" + attribute;
}
interceptUrl(requestMatchers, SecurityConfig.createList(attribute));
return ExpressionUrlAuthorizationConfigurer.this.REGISTRY;
}
这里的 createList 方法是将 attributeauthen(即”authenticated“字符串)封装为一个 SecurityConfig 对象,SecurityConfig 是 ConfigAttribute 的实现类然后放入一个 list 中返回。
public static List<ConfigAttribute> createList(String... attributeNames) {
Assert.notNull(attributeNames, "You must supply an array of attribute names");
List<ConfigAttribute> attributes = new ArrayList<>(
attributeNames.length);
for (String attribute : attributeNames) {
attributes.add(new SecurityConfig(attribute.trim()));
}
return attributes;
}
再看 interceptUrl 方法:
private void interceptUrl(Iterable<? extends RequestMatcher> requestMatchers,
Collection<ConfigAttribute> configAttributes) {
for (RequestMatcher requestMatcher : requestMatchers) {
REGISTRY.addMapping(new AbstractConfigAttributeRequestMatcherRegistry.UrlMapping(
requestMatcher, configAttributes));
}
}
这里的UrlMapping 方法 将 requestMatcher 和 之前 createList 方法封装好的 ConfigAttribute 集合对象 一起封装为一个 UrlMapping 对象。
static final class UrlMapping {
private RequestMatcher requestMatcher;
private Collection<ConfigAttribute> configAttrs;
UrlMapping(RequestMatcher requestMatcher, Collection<ConfigAttribute> configAttrs) {
this.requestMatcher = requestMatcher;
this.configAttrs = configAttrs;
}
这里的 requestMatcher 集合 就是 之前 调用 authenticated() 方法 封装好的 AuthorizedUrl 对象 的 requestMatchers 属性。
然后调用 addMapping 方法将 UrlMapping 对象添加到 REGISTRY 的 urlMappings 属性中(该属性类型为 UrlMapping 对象集合)。
final void addMapping(UrlMapping urlMapping) {
this.unmappedMatchers = null;
this.urlMappings.add(urlMapping);
}
至此我们发现,WebSecurityConfigurerAdapter 的 configure(HttpSecurity http) 方法主要就是 初始化了 ExpressionUrlAuthorizationConfigurer,并且 在其 REGISTRY 属性的 urlMappings 属性中填入 UrlMapping 对象(RequestMatcher 和 ConfigAttribute 集合 的对应关系)。
WebSecurityConfigurerAdapter 中默认配置如下:
http.authorizeRequests().anyRequest().authenticated()
anyRequest()方法 生成了一个AnyRequestMacher 对象 拦截所有请求。
authenticated() 方法 传入 “authenticated” 字符串生成 ConfigAttribute 代表 所有请求都需要经过认证,未经过验证的请求都会被拦截。
因此,我们可以多次调用 httpSecurity 的 authorizeRequests() 方法 对不同请求设置不同的拦截和认证机制。
看一下和 anyRequest() 在同一个类中的 antMatchers(String… antPatterns) 方法。这个方法的实现和 anyRequest() 类似,区别在于可以传入多个请求路径字符串,对每一个 请求路径都生成一个对应的 AntPathRequestMatcher。
public C antMatchers(String... antPatterns) {
Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
}
public static List<RequestMatcher> antMatchers(String... antPatterns) {
return antMatchers(null, antPatterns);
}
public static List<RequestMatcher> antMatchers(HttpMethod httpMethod,
String... antPatterns) {
String method = httpMethod == null ? null : httpMethod.toString();
List<RequestMatcher> matchers = new ArrayList<>();
for (String pattern : antPatterns) {
matchers.add(new AntPathRequestMatcher(pattern, method));
}
return matchers;
}
接着再找到和 authenticated() 在同一类中的 permitAll() 方法. 该方法的实现和 authenticated() 类似,区别在于用于生成 ConfigAttribute 的 字符串 是 “permitAll”。
permitAll 表示 无需认证。和 authenticated 正好相反。
而在 SecurityExpressionRoot 中 正好有一个 permitAll 属性,
spel 的 getValue 方法会返回 permitAll 属性的值,而这个属性值始终为 true,这样就能保证请求不被拦截。因此我们可以在 configure(HttpSecurity http) 中 增加如下配置:
http.authorizeRequests().antMatchers("/login").permitAll();
保证 “/login" 不被拦截。
这里需要注意,这段配置必须放在 WebSecurityConfigurerAdapter 类 configure(HttpSecurity http) 方法中的默认配置 http.authorizeRequests().anyRequest().authenticated() 之前,否则会报错。
因为,在之前分析过的 DefaultFilterInvocationSecurityMetadataSource类 的 getAttributes 方法中,会遍历 requestMap,(requestMap 是通过遍历REGISTRY 的 urlMapping 属性得到的)。
只要请求和 requestMap 中的key(即 RequestMatcher)匹配,就直接返回 value值(ConfigAttribute集合),如果将 默认配置放在前面,那么无论什么请求都能匹配 AnyRequestMatcher,这样的话后面的 RequestMatcher 配置就不会生效。框架内部为了防止发生这种错误,如果将默认配置放在后面,在项目启动过程中会直接抛出异常。
配置完成后启动项目,访问localhost:8080/login ,此时就不会再出现未登录提示了