鉴权/认证框架Spring Security和Apache Shiro比较

220 篇文章 7 订阅
213 篇文章 3 订阅

参考:

打开IDEA => File => New => Project,选择选择 Spring Initializr

如果IDEA自动下载的依赖中断了,或者网络问题,可以手动:

mvn -U idea:idea

新建Spring Boot项目之后,依赖Spring Security。
在application.properties文件中输入账号密码:

spring.security.user.name=cqq
spring.security.user.password=pass

并创建一个Controller,

package com.cqq.csrf1.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {


    @PostMapping("/transfer")
    public void transferMoney(String name, Integer money) {
        System.out.println("[*] name = " + name);
        System.out.println("[*] 转账 ");
        System.out.println("[*] money = " + money);
    }


    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

启动这个Application,打开应用根目录,会重定向到/login,
在这里插入图片描述
这就是Spring Security自带的登录界面:
在这里插入图片描述

登录的时候发现会自动带上一个_csrf的字段,看起来是个csrf token:
在这里插入图片描述
当去掉这个字段的时候,即便账号密码是正确的,也不会登录成功,
在这里插入图片描述
通过查看前端代码,知道这个_csrf字段是预埋在网页里的。
在这里插入图片描述

先不登录,试一下我们写的这个/transfer接口:
在没有_csrf值的情况下:
在这里插入图片描述
发现直接重定向到登录页面。
那就老老实实登录吧,
使用正确的用户名密码登录成功之后,会返回有效的JSESSIONID,用作登录凭证:
在这里插入图片描述
于是带着这个Cookie再请求/transfer接口,
在这里插入图片描述

结果响应403。相比这就是Spring Security的CSRF防御了。

回头看看Spring Boot启动的信息:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.1)

2021-01-13 15:45:37.465  INFO 47736 --- [           main] com.cqq.csrf1.Csrf1Application           : Starting Csrf1Application using Java 1.8.0_172 on cqq with PID 47736 (C:\Users\Administrator\Downloads\csrf-1\target\classes started by Administrator in C:\Users\Administrator\Downloads\csrf-1)
2021-01-13 15:45:37.467  INFO 47736 --- [           main] com.cqq.csrf1.Csrf1Application           : No active profile set, falling back to default profiles: default
2021-01-13 15:45:38.016  INFO 47736 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-01-13 15:45:38.022  INFO 47736 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-01-13 15:45:38.022  INFO 47736 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.41]
2021-01-13 15:45:38.077  INFO 47736 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-01-13 15:45:38.077  INFO 47736 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 578 ms
2021-01-13 15:45:38.184  INFO 47736 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-01-13 15:45:38.351  INFO 47736 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@66ba7e45, org.springframework.security.web.context.SecurityContextPersistenceFilter@2b214b94, org.springframework.security.web.header.HeaderWriterFilter@13047d7d, org.springframework.security.web.csrf.CsrfFilter@a64e035, org.springframework.security.web.authentication.logout.LogoutFilter@985696, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4ae263bf, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@215a34b4, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@70e02081, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@6be25526, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@49601f82, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@250b236d, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@9d200de, org.springframework.security.web.session.SessionManagementFilter@65bb9029, org.springframework.security.web.access.ExceptionTranslationFilter@782168b7, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@65ae095c]
2021-01-13 15:45:38.396  INFO 47736 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-01-13 15:45:38.403  INFO 47736 --- [           main] com.cqq.csrf1.Csrf1Application           : Started Csrf1Application in 1.223 seconds (JVM running for 3.875)
2021-01-13 15:45:48.429  INFO 47736 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-01-13 15:45:48.430  INFO 47736 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2021-01-13 15:45:48.431  INFO 47736 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms

看到这样一行:

Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@66ba7e45, 
org.springframework.security.web.context.SecurityContextPersistenceFilter@2b214b94, 
org.springframework.security.web.header.HeaderWriterFilter@13047d7d, 
org.springframework.security.web.csrf.CsrfFilter@a64e035, 
org.springframework.security.web.authentication.logout.LogoutFilter@985696, 
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@4ae263bf, 
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@215a34b4, 
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@70e02081, 
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@6be25526, 
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@49601f82, 
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@250b236d, 
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@9d200de, 
org.springframework.security.web.session.SessionManagementFilter@65bb9029, 
org.springframework.security.web.access.ExceptionTranslationFilter@782168b7, 
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@65ae095c]

看到了CsrfFilter,想必这个就是默认用于防御CSRF攻击的Filter了吧。
这个类在spring-security-web包下面:
在这里插入图片描述
发现访问这两个接口都是默认需要登录凭据的:
在这里插入图片描述

在/hello接口下断点。不带Cookie情况下断不下来,直接响应302。为了弄清楚Spring Security是如何判断然后响应302的,我先带着有效的Cookie访问,成功想/hello接口断下来。

GET /hello HTTP/1.1
Host: cqq.com:8080
Cookie: JSESSIONID=64460ED9A74B51260CB77738D73B8AC7
Accept: text/html
Connection: close


然后寻找不带Cookie时可能经过的调用栈。
经测试,不带Cookie时发现org.springframework.security.web.authentication.AnonymousAuthenticationFilter的doFilter方法可以断下来。
在这里插入图片描述
在这里插入图片描述

在这里给我们设定了Anonymous用户,
然后最后到达这个FilterChain的最后一个Filter,

准确地说,它是一个Intercepter:

org.springframework.security.web.access.intercept.FilterSecurityInterceptor

不过它也实现了Filter接口,算是一种特殊的Filter,难怪还是给它放到了FilterChain里面:

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter

在这里插入图片描述
这里把请求、响应、Chain封装成一个FilterInvocation对象

在这里插入图片描述

在beforeInvocation方法中,

this.attemptAuthorization(object, attributes, authenticated);

在这里插入图片描述

看方法名应该是对这个Anonymous的用户对象进行Authorization(鉴权),看它是否有权限访问要访问的这个url。
然后通过调用:

this.accessDecisionManager.decide(authenticated, object, attributes);

继续判断,然后这里判断没有这个访问权限,然后抛出了异常,这里被捕获之后,进行可能的日志等操作之后,继续抛出。
在这里插入图片描述
然后被上一个Filter:org.springframework.security.web.access.ExceptionTranslationFilter#doFilter的catch捕获,
在这里插入图片描述

然后继续处理,发现这里异常是AccessDeniedException
在这里插入图片描述
继续跟进handleAccessDeniedException方法:
在这里插入图片描述
继续跟进sendStartAuthentication方法:

在commece(开始)方法里,找到应该进行认证的url是/login
在这里插入图片描述
构造这个重定向的url:
在这里插入图片描述
构造出完整了重定向url之后,就进行重定向:
在这里插入图片描述
在这里插入图片描述
至于javax.servlet.http.HttpServletResponse如何调用sendRedirect方法进行重定向就不继续跟进了。

整理一下:
判断没有有效的JSESSIONID之后,就通过AnonymousAuthenticationFilter将这个用户强行设置为匿名用户(AnonymousUser),并且已经认证(Authenticated)。封装成一个FilterInvocation对象,交给accessDecisionManager去decide,然后对比/hello接口需要的用户权限(认证用户的权限)和这个请求所带的用户权限(AnonymousUser权限),判断为权限不足,于是交给ExceptionTranslationFilter捕获,然后对这个异常进行处理,即重定向。

注意,

  • 继承了org.springframework.web.filter.OncePerRequestFilter的Filter,比如CsrfFilter,调用的是doFilterInternal方法。

  • 继承了org.springframework.web.filter.GenericFilterBean的Filter,比如LogoutFilter,调用的是doFilter

注意到不管是doFilterInternal还是doFilter
其方法接收的参数都是这三个:

(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)

request很好理解,因为Filter就是对请求进行"filter"(过滤),拿到请求的body,header之类的;
response,因为有时候也会写入到response对象中,等待整个FilterChain结束之后统一写入到响应中;
filterChain有一个变量表示当前在进行哪个Filter,有数字来进行定位。

CsrfFilter

在这里插入图片描述
在这里插入图片描述
因为我们用的是GET方法,在这个Filter设置的白名单中,不用进行CSRF攻击检查。

LogoutFilter

在LogoutFilter中,我以为会对请求的url与系统的退出url(/logou)进行匹配,然而只是比较了一下方法就返回了fasle(//TODO)
在这里插入图片描述

UsernamePasswordAuthenticationFilter

这个Filter也跟LogoutFilter一样,还没到匹配url(/login)的地方,直接判断方法就返回false了。
继续下一个Filter。

附录

不同请求头导致的不同响应:

带有Accept头时,会302重定向:
在这里插入图片描述

在这里插入图片描述
不带时会401,
带上X-Requested-With: XMLHttpRequest时,
在这里插入图片描述

默认Spring Security进行了CSRF防御,想要禁用这个防御机制,需要新建一个类SecurityConfig,继承自WebSecurityConfigurerAdapter

package com.cqq.csrf1.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf()
                .disable();
    }
}

然后再调用/transfer接口,即可成功调用:
在这里插入图片描述
在这里插入图片描述

前后端分离的项目

不是将 _csrf 放在 Model 中返回前端了,而是放在 Cookie 中返回前端

由于直接拿到Cookie才能拿到CSRF token,攻击者不能拿到受害者的Cookie,所以能防止CSRF攻击。

需要再写一个类继承自WebSecurityConfigurerAdapter

package com.cqq.csrf1.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

@Configuration
public class SecurityConfig2 extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/js/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .successHandler((req,resp,authentication)->{
                    resp.getWriter().write("success");
                })
                .permitAll()
                .and()
                .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}

前端html页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="js/jquery.min.js"></script>
    <script src="js/jquery.cookie.js"></script>
</head>
<body>
<div>
    <input type="text" id="username">
    <input type="password" id="password">
    <input type="button" value="登录" id="loginBtn">
</div>
<script>
    $("#loginBtn").click(function () {
        let _csrf = $.cookie('XSRF-TOKEN');
        $.post('/login.html',{username:$("#username").val(),password:$("#password").val(),_csrf:_csrf},function (data) {
            alert(data);
        })
    })
</script>
</body>
</html>

这里设置withHttpOnlyFalse,即设置HttpOnly属性为False,即允许前端js操作Cookie。

然后把之前的类SecurityConfig注释掉,重新启动项目。
抓包发现:
提交的_csrf值确实与Cookie中的XSRF-TOKEN一致。
在这里插入图片描述
当修改这个值时,就不能请求成功。
在这里插入图片描述

不使用Spring Security框架

新建一各Spring Boot项目,只依赖Spring Web,不选择Spring Security,
在这里插入图片描述

没有Spring Security的各种Filter,直接就进入了Controller的逻辑,

使用Shiro进行鉴权和认证

安装Shiro官方教程中与Spring Boot集成的教程:
https://shiro.apache.org/spring-boot.html
依然只依赖Spring Web启动IDEA,然后在pom.xml里加上Shiro跟Spring Boot适配的依赖:

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.7.0</version>
        </dependency>

从它的pom.xml文件里可以看出,引入他的同时也引入了

- spring-boot-starter
- spring-boot-starter-web
- shiro-spring-boot-starter

等。
在配置文件application.properties中配置:

shiro.loginUrl = /login.html

# Let Shiro Manage the sessions
shiro.userNativeSessionManager = true

# disable URL session rewriting
shiro.sessionManager.sessionIdUrlRewritingEnabled = false

然后从shiro官方给的spring-boot-web示例:
https://github.com/apache/shiro/blob/master/samples/spring-boot-web
中引入静态代码html等到templates目录。
在这里插入图片描述
输出这样的错误。

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.1)

2021-01-14 15:08:27.420  INFO 64796 --- [           main] com.cqq.csrf3.Csrf3Application           : Starting Csrf3Application using Java 1.8.0_172 on cqq with PID 64796 (C:\Users\Administrator\Downloads\csrf-3\target\classes started by Administrator in C:\Users\Administrator\Downloads\csrf-3)
2021-01-14 15:08:27.421  INFO 64796 --- [           main] com.cqq.csrf3.Csrf3Application           : No active profile set, falling back to default profiles: default
2021-01-14 15:08:27.901  INFO 64796 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.apache.shiro.spring.boot.autoconfigure.ShiroBeanAutoConfiguration' of type [org.apache.shiro.spring.boot.autoconfigure.ShiroBeanAutoConfiguration$$EnhancerBySpringCGLIB$$e650c5b8] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-01-14 15:08:27.910  INFO 64796 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration' of type [org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration$$EnhancerBySpringCGLIB$$e8e4045d] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-01-14 15:08:27.918  INFO 64796 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'eventBus' of type [org.apache.shiro.event.support.DefaultEventBus] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-01-14 15:08:27.949  INFO 64796 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration' of type [org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration$$EnhancerBySpringCGLIB$$aed2622] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-01-14 15:08:27.951  INFO 64796 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.apache.shiro.spring.boot.autoconfigure.ShiroAutoConfiguration' of type [org.apache.shiro.spring.boot.autoconfigure.ShiroAutoConfiguration$$EnhancerBySpringCGLIB$$ac506788] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-01-14 15:08:27.956  WARN 64796 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'shiroEventBusAwareBeanPostProcessor' defined in class path resource [org/apache/shiro/spring/boot/autoconfigure/ShiroBeanAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.apache.shiro.spring.ShiroEventBusBeanPostProcessor]: Factory method 'shiroEventBusAwareBeanPostProcessor' threw exception; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'eventBus' defined in class path resource [org/apache/shiro/spring/boot/autoconfigure/ShiroBeanAutoConfiguration.class]: Initialization of bean failed; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'authorizationAttributeSourceAdvisor' defined in class path resource [org/apache/shiro/spring/boot/autoconfigure/ShiroAnnotationProcessorAutoConfiguration.class]: Unsatisfied dependency expressed through method 'authorizationAttributeSourceAdvisor' parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityManager' defined in class path resource [org/apache/shiro/spring/config/web/autoconfigure/ShiroWebAutoConfiguration.class]: Unsatisfied dependency expressed through method 'securityManager' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'missingRealm' defined in class path resource [org/apache/shiro/spring/boot/autoconfigure/ShiroAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.apache.shiro.realm.Realm]: Factory method 'missingRealm' threw exception; nested exception is org.apache.shiro.spring.boot.autoconfigure.exception.NoRealmBeanConfiguredException
2021-01-14 15:08:27.963  INFO 64796 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2021-01-14 15:08:27.977 ERROR 64796 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

No bean of type 'org.apache.shiro.realm.Realm' found.

Action:

Please create bean of type 'Realm' or add a shiro.ini in the root classpath (src/main/resources/shiro.ini) or in the META-INF folder (src/main/resources/META-INF/shiro.ini).

简单来说应该就是没有配置org.apache.shiro.realm.Realm这个bean.
解决方案是可以在以下文件中配置:

- src/main/resources/shiro.ini
- src/main/resources/META-INF/shiro.ini

通过查看shiro官方文档,发现不仅可以通过ini配置文件,也可以通过Shiro的API来配置:

Realm realm = //instantiate or acquire a Realm instance.  We'll discuss Realms later.
SecurityManager securityManager = new DefaultSecurityManager(realm);

//Make the SecurityManager instance available to the entire application via static memory: 
SecurityUtils.setSecurityManager(securityManager);

参考Shiro的官方示例:
https://github.com/apache/shiro/blob/master/samples/spring-boot-web/src/main/java/org/apache/shiro/samples/WebApp.java

@SpringBootApplication所在的类加上这段代码也可以完成Realm的设置:

    @Bean
    public Realm realm() {
        TextConfigurationRealm realm = new TextConfigurationRealm();
        realm.setUserDefinitions("joe.coder=password,user\n" +
                "jill.coder=password,admin");

        realm.setRoleDefinitions("admin=read,write\n" +
                "user=read");
        realm.setCachingEnabled(true);
        return realm;
    }

然后发现如果不在application.properties中设置:

shiro.loginUrl = /login.html

则访问/会被无限重定向到login.jsp,而我并没有配这个文件。导致失败。
加上这个配置之后就能正确地重定向到登录页面/login.html了:

然后发现配置之后,默认Shiro就启动了rememberMe的功能:
在这里插入图片描述
然后测试一下这个功能:

# If enabled Shiro will manage the HTTP sessions instead of the container. ref: https://shiro.apache.org/spring-boot.html
shiro.userNativeSessionManager = true

安装官方的说明,这个设置为true之后,就会让Shiro来管理session,而不是容器(这里是Tomcat)
在这里插入图片描述
发现Shiro管理Session的话,它的JSESSION的形式是:

JSESSIONID=51c54adb-1eb3-4ad3-970c-0494d4e412a2

与容器(Tomcat)的SESSION管理的格式不一样:

JSESSIONID=79F667E5FAACC540ED6FB69D6EDC5825

而且用Tomcat管理session的时候,重定向的时候url后面会带着jsessionid。

自定义RememberMeManager:
在shiro.ini中配置:

[main]
...
rememberMeManager = com.my.impl.RememberMeManager
securityManager.rememberMeManager = $rememberMeManager

参考:https://shiro.apache.org/web.html#Web-Custom%7B%7BRememberMeManager%7D%7D

Cookie中SESSIONID的名字、rememberMe的名字都是可以自定义的:

# Cookie中SESSION的名字可以自定义
shiro.sessionManager.cookie.name = shiro_SESSIONID

# Cookie中rememberMe的名字可以自定义
shiro.rememberMeManager.cookie.name = remember-shiro-me

修改完之后重新启动,就变成这样了。
在这里插入图片描述
然后依然会无限重定向:
在这里插入图片描述
即便我们已经在templates里已经有了login.html。
后来思考可能这个templates目录里是给Spring控制的,而可能static目录下是不需要Spring来路由的,于是将login.html放到static目录下,果然可以访问了:
在这里插入图片描述
但是static目录貌似不是这样用的,而是存放一些js和css之类的文件的。
于是删除static目录下的login.html,再参考官方示例,给加一个LoginController,对/login.html这个url进行mapping:

package com.cqq.csrf3.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;


@Controller
public class LoginController {

    @RequestMapping("/login.html")
    public String loginTemplate() {
        return "login";
    }

}

这里的方法名loginTemplate无所谓,方便说明即可。反正有注解对url进行mapping。

Spring Boot配置path之后报404的某个原因

参考:
https://www.cnblogs.com/FondWang/p/12951064.html

在Class上注解了@RestController之后,必须在这个类上面加上@RequestMapping给它加一个path映射,否则出现404.

javax.servlet.ServletException: Circular view path [login]: would dispatch back to the current handler URL [/login] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)

详情:

	at org.springframework.web.servlet.view.InternalResourceView.prepareForRendering(InternalResourceView.java:210) ~[spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.view.InternalResourceView.renderMergedOutputModel(InternalResourceView.java:148) ~[spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:316) ~[spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1394) ~[spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1139) ~[spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1078) ~[spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:961) ~[spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.2.jar:5.3.2]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.2.jar:5.3.2]

参考:

  • https://www.cnblogs.com/guochunyang2004/p/8629112.html
  • https://www.baeldung.com/spring-circular-view-path-error

Consider defining a bean named ‘authenticator’ in your configuration.

@Bean public SecurityManager securityManager() {……}
改为默认的即可:
@Bean public DefaultWebSecurityManager securityManager() {……}

参考:

  • https://github.com/ityouknow/spring-boot-examples/issues/56

配置权限

写一个类,进行@Configuration注解:

@Configuration
public class ShiroConfig

拿到logger对象,进行日志处理:

private final Logger logger = LoggerFactory.getLogger(this.getClass());

剩下的默认:

    @Bean
    MyRealm myRealm() {
        return new MyRealm();
    }

    // 参考: https://github.com/ityouknow/spring-boot-examples/issues/56
    @Bean
    DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(myRealm());
        return manager;
    }

然后就是进行权限配置:

    @Bean("shiroFilterFactoryBean")
    ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager")SecurityManager securityManager) {
        logger.info("启动shiroFilter--时间是:" + new Date());
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        bean.setLoginUrl("/login");      // 登录的
        bean.setSuccessUrl("/hello");    // 登录成功之后跳转的
        bean.setUnauthorizedUrl("/unauth");

        Map<String, String> map = new LinkedHashMap<>();
        map.put("/static/**", "anon");
        map.put("/css/**", "anon");
        map.put("/js/**", "anon");
        map.put("/login", "anon");
        map.put("/doLogin", "anon");
        // 以上接口都是未授权可以访问,剩下的默认就是需要授权访问了
        map.put("/**", "authc");
        bean.setFilterChainDefinitionMap(map);
        return bean;
    }

这里设置用于登录的url和登录成功之后跳转的url。
然后设置一些不需要登录就可以访问的url,比如静态资源,和登录/注册url等。
其他的url就是默认需要认证才能访问的。
下面就是在没有将/css/加入到anon的结果,css资源也要认证才能访问。
在这里插入图片描述

Spring Boot无需配置 web.xml,但在其他Java项目中,web.xml是一个非常重要的文件,用来配置Servlet、Filter、Listener等。

参考:

  • https://s31k31.github.io/2020/04/26/JavaSpringBootCodeAudit-2-SpringBoot/

配置MyBatis的Java接口和Mapper.xml文件。
步骤:
1、在application.proterties文件中指定xml文件的位置:

mybatis.mapper-locations=classpath:mapper/*Mapper.xml

一般都是resources目录下的mapper目录。
在这里插入图片描述
2、在dao或者mapper目录下新建一个Mapper,比如UserMapper.java文件

import org.apache.ibatis.annotations.Mapper;
// 注意这里要写这个注解,否则
@Mapper
public interface UserMapper {


User selectByLoginNameAndPasswd(@Param("username") String username, @Param("password") String password);

}

定义方法名,接收参数名、参数类型、以及返回对象。
这里的User类在entity目录下定义。
3、在UserMapper.xml中实现SQL语句,

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cqq.csrf3.dao.UserMapper">

    <resultMap id="BaseResultMap" type="com.cqq.csrf3.entity.User">
        <id column="user_id" jdbcType="BIGINT" property="userId"/>
        <result column="login_name" jdbcType="VARCHAR" property="loginName"/>
        <result column="password_md5" jdbcType="VARCHAR" property="passwordMd5"/>
    </resultMap>

    <select id="selectByLoginNameAndPasswd" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from user
        where login_name = #{loginName} and password_md5 = #{passwordMD5}
    </select>

</mapper>

在mapper标签里写语句,定义返回类型resultMap,指定namespace为
这个xml匹配的Mapper接口类,这里是com.cqq.csrf3.dao.UserMapper
如果namespace出错,会报这个错Invalid bound statement (not found)
参考:https://blog.csdn.net/qq_35981283/article/details/78590090

4、在Controller中doLogin方法调用service的login方法,在service的Impl中实现具体的login方法,在Impl中调用Mapper接口的查询方法selectByLoginNameAndPasswd

// LoginController.java

@Controller
public class LoginController {

    @Resource
    private UserService userService;

    // 只允许POST,但是并不抛出错误
    @RequestMapping(value = "/doLogin", method = RequestMethod.POST)
    @ResponseBody
    public Result doLogin(@RequestParam("username") String username, @RequestParam("password") String password, HttpSession httpSession) {
    String loginResult = userService.login(username, MD5Util.MD5Encode(password, "UTF-8"), httpSession);
    }
}

// UserService.java

import org.springframework.stereotype.Service;

@Service
public interface UserService {
	String login(String loginName, String passwordMD5, HttpSession httpSession);
}

// UserServiceImpl.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public String login(String loginName, String passwordMD5, HttpSession httpSession) {
        User user = userMapper.selectByLoginNameAndPasswd(loginName, passwordMD5);
        ...
    }

最终在
UserServiceImpl 这里调用UserMapper 的实现(xml中实现)执行sql语句。

注意xml中引用的变量名要与Mapper接口中的变量名一致:

    <select id="selectByLoginNameAndPasswd" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from mall_user
        where login_name = #{loginName} and password_md5 = #{passwordMD5}
    </select>

UserMapper.java

import org.apache.ibatis.annotations.Param;

User selectByLoginNameAndPasswd(@Param("loginName") String username, @Param("passwordMD5") String password);

或者如果这里不写@Param,直接写

User selectByLoginNameAndPasswd(String loginName, String passwordMD5);

会默认使用这个局部变量名与xml中引用的变量名保持一致。

在/doLogin这个映射的方法中,返回类型是Result类型,实现了序列化接口,

public class Result<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    private int resultCode;
    private String message;
    private T data;
}

进行@ResponseBody注解,表示返回的是一个序列化的对象
在这里插入图片描述
若不进行这个注解,则会报错:

org.thymeleaf.exceptions.TemplateInputException: Error resolving template [doLogin], template might not exist or might not be accessible by any of the configured Template Resolvers

另外注意到这里返回的中文是乱码。原因是Response的默认content-type是application/json;charset=ISO-8859-1

解决方法是在@Mapping注解加上produces = "application/json;charset=utf-8"

    @RequestMapping(value = "/doLogin", method = RequestMethod.POST, produces = "application/json;charset=utf-8")
    @ResponseBody

但是这种方法只是在某个方法上设置content-type。其他的全局方法参考以下
参考:

  • https://www.cnblogs.com/uqing/p/10163094.html
  • https://blog.csdn.net/zknxx/article/details/52423608

Shiro的Filter

Shiro中默认的Filter有以下,org\apache\shiro\web\filter\mgt\DefaultFilter

import org.apache.shiro.web.filter.InvalidRequestFilter;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.filter.authc.UserFilter;
import org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
import org.apache.shiro.web.filter.authz.PortFilter;
import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;
import org.apache.shiro.web.filter.authz.SslFilter;
import org.apache.shiro.web.filter.session.NoSessionCreationFilter;

public enum DefaultFilter {
    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    authcBearer(BearerHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class),
    invalidRequest(InvalidRequestFilter.class);
    ...
    

主要在org.apache.shiro.web.filter包下面。
在FilterChain中会被调用preHandle方法。
具体的流程是:

org.apache.shiro.web.servlet.OncePerRequestFilter#doFilter

在这个方法中执行:

this.doFilterInternal(request, response, filterChain);

而由于这个类OncePerRequestFilter是一个抽象类,其具体的实现方法由其子类完成:
org.apache.shiro.web.servlet.AdviceFilter就继承了它,并实现了这个方法,在其doFilterInternal方法中,

            boolean continueChain = this.preHandle(request, response);

            if (continueChain) {
                this.executeChain(request, response, chain);
            }

            this.postHandle(request, response);

通过调用preHandle方法,通过其返回值进行判断是否继续调用FilterChain中的Filter。
再继续PathMatchingFilter继承了AdviceFilter,继续调用preHandle方法.
然后下面调用的是AccessControlFilter和InvalidRequestFilter,这次不是调用preHandle方法了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值