From 190fb7054b138c0cd065fdc617a2150b9d77ba2c Mon Sep 17 00:00:00 2001 From: jalon2015 <1121263265@qq.com> Date: Mon, 6 Dec 2021 19:02:56 +0800 Subject: [PATCH 1/8] 1 --- .gitignore | 1 + README.md | 61 ---- demo-cache-redis/README.md | 70 ---- demo-email/README.md | 1 - demo-mq-rabbitmq/README.md | 13 - demo-orm-jpa/README.md | 24 -- demo-spring-security/README.md | 37 --- .../README.md | 208 ------------ .../demo-spring-security-basic-auth/README.md | 265 --------------- .../web/basic-auth/README.md | 24 -- .../README.md | 208 ------------ .../demo-spring-security-jpa/README.md | 3 - .../demo-spring-security-login-form/README.md | 314 ------------------ .../README.md | 250 -------------- .../demo-spring-security-logout/README.md | 168 ---------- .../README.md | 177 ---------- .../README.md | 154 --------- .../demo-spring-security-session/README.md | 151 --------- .../demo-spring-security-userinfo/README.md | 151 --------- demo-swagger3/README.md | 259 --------------- demo-task/README.md | 52 --- demo-upload/README.md | 94 ------ 22 files changed, 1 insertion(+), 2684 deletions(-) delete mode 100644 README.md delete mode 100644 demo-cache-redis/README.md delete mode 100644 demo-email/README.md delete mode 100644 demo-mq-rabbitmq/README.md delete mode 100644 demo-orm-jpa/README.md delete mode 100644 demo-spring-security/README.md delete mode 100644 demo-spring-security/demo-spring-security-auth-failure-handler/README.md delete mode 100644 demo-spring-security/demo-spring-security-basic-auth/README.md delete mode 100644 demo-spring-security/demo-spring-security-basic-auth/web/basic-auth/README.md delete mode 100644 demo-spring-security/demo-spring-security-block-brute-force-auth/README.md delete mode 100644 demo-spring-security/demo-spring-security-jpa/README.md delete mode 100644 demo-spring-security/demo-spring-security-login-form/README.md delete mode 100644 demo-spring-security/demo-spring-security-login-redirect/README.md delete mode 100644 demo-spring-security/demo-spring-security-logout/README.md delete mode 100644 demo-spring-security/demo-spring-security-manually-login/README.md delete mode 100644 demo-spring-security/demo-spring-security-remember-me/README.md delete mode 100644 demo-spring-security/demo-spring-security-session/README.md delete mode 100644 demo-spring-security/demo-spring-security-userinfo/README.md delete mode 100644 demo-swagger3/README.md delete mode 100644 demo-task/README.md delete mode 100644 demo-upload/README.md diff --git a/.gitignore b/.gitignore index 549e00a..8fe8312 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ HELP.md +README.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ diff --git a/README.md b/README.md deleted file mode 100644 index af5c11c..0000000 --- a/README.md +++ /dev/null @@ -1,61 +0,0 @@ -- [x] demo-helloworld(Helloworld示例) -- [ ] demo-properties(读取配置文件信息) -- [ ] demo-actuator(对SpringBoot的端点控制) -- [ ] demo-admin-client(对SpringBoot可视化管控-客户端) -- [ ] demo-admin-server(对SpringBoot可视化管控-服务端) -- [ ] demo-logback(集成logback日志) -- [ ] demo-log-aop(使用AOP拦截请求日志信息) -- [ ] demo-exception-handler(统一异常处理) -- [ ] demo-template-thymeleaf(使用模板引擎 - thymeleaf) -- [ ] demo-upload(上传,集成本地上传和七牛云上传) -- [ ] demo-orm-jpa(操作SQL关系型数据库 - JPA) -- [ ] demo-orm-mybatis(操作SQL关系型数据库 - mybatis) -- [ ] demo-orm-mybatis-mapper-page(操作SQL关系型数据库 - 集成mybatis通用Mapper,PageHelper) -- [ ] demo-orm-mybatis-plus(操作关系型数据库 - 集成mybatis-plus,Mapper操作,ActiveRecord操作) -- [ ] demo-cache-redis(使用redis进行缓存) -- [ ] demo-cache-ehcache(使用Ehcache进行缓存) -- [x] demo-email(集成邮件服务) -- [ ] demo-task(定时任务-Task实现) -- [ ] demo-task-quartz(定时任务 - Quartz实现) -- [ ] demo-task-xxl-job(定时任务 - XXL-JOB实现分布式调度) -- [x] demo-swagger(集成 Swagger 对 API 接口进行测试管理) -- [ ] demo-swagger-beauty(集成自定义且更加美观的 Swagger 对 API 接口进行测试管理) -- [ ] demo-rbac-security(实现基于RBAC的权限模型-Spring Security) -- [ ] demo-rbac-shiro(实现基于RBAC的权限模型-Shiro) -- [ ] demo-session(统一 Session 管理) -- [ ] demo-oauth(OAuth2 认证) -- [ ] demo-social(集成 JustAuth 实现第三方授权验证,实现 QQ、微信、GitHub、谷歌、小米等第三方登录) -- [ ] demo-zookeeper(使用 zookeeper 结合 AOP 实现分布式锁) -- [ ] demo-mq-rabbitmq(集成消息中间件 - RabbitMQ) -- [ ] demo-mq-rocketmq(集成消息中间件 - RocketMQ) -- [ ] demo-mq-kafka(集成消息中间件 - Kafka) -- [ ] demo-websocket(集成 websocket 服务) -- [ ] demo-websocket-socketio(集成 socketio 实现 websocket 服务) -- [ ] demo-ureport2 (集成 ureport2 实现自定义的复杂中国式报表引擎) -- [ ] demo-uflo(集成 uflo 实现流程控制引擎) -- [ ] demo-urule(集成 urule 实现规则引擎) -- [ ] demo-activiti(集成 Activiti 实现流程控制引擎) -- [ ] demo-async(Spring boot 实现异步调用) -- [ ] demo-dubbo(集成 dubbo) -- [ ] demo-war(打包成war包) -- [ ] demo-elasticsearch(集成 ElasticSearch) -- [ ] demo-mongodb(集成 MongoDb) -- [ ] demo-neo4j(集成 neo4j 图数据库) -- [ ] demo-docker(打包成 docker 镜像) -- [ ] demo-multi-datasource-jpa(集成JPA多数据源) -- [ ] demo-multi-datasource-mybatis(集成mybatis多数据源) -- [ ] demo-sharding-jdbc(集成 sharding-jdbc 实现分库分表) -- [ ] demo-tio(集成 tio) -- [ ] demo-grpc(集成grpc,配置tls/ssl) -- [ ] demo-codegen(集成 velocity 自动生成代码) -- [ ] demo-graylog(集成 gralog 日志管理) -- [ ] demo-sso(集成单点登录)参见 -- [ ] demo-ldap (集成 ldap)参见 -- [ ] demo-dynamic-datasource(动态添加数据源,切换数据源) -- [ ] demo-ratelimit-guava(单机限流保护API,集成 Guava 的 RateLimiter) -- [ ] demo-ratelimit-redis(分布式限流保护API,使用 Redis + lua 脚本实现) -- [ ] demo-https(集成 HTTPS) -- [ ] demo-elasticsearch-rest-high-level-client(集成 Elasticsearch 7.x 版本,使用官方 rest high level client操作 ES 数据) -- [ ] demo-springbatch(数据处理) -- [ ] demo-security-justauth(使用 JustAuth 登录 GitHub,使用 Security 管理登录状态) -- [ ] demo-flyway(集成 Flyway,项目启动时初始化数据库表结构,同时支持数据库脚本版本控制) \ No newline at end of file diff --git a/demo-cache-redis/README.md b/demo-cache-redis/README.md deleted file mode 100644 index 34e1b79..0000000 --- a/demo-cache-redis/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# 缓存:整合SpringDataRedis - -## 简介 - -SpringDataRedis简化了Redis的冗余代码和样板代码,为用户提供了低级和高级的抽象 - -## 示例 - -1. 添加依赖 spring-data-redis - -```xml - - - org.springframework.data - spring-data-redis - 2.4.5 - - -``` - -2. 配置连接器 Lettuce 连接器 - -```java -@Configuration -public class MyConfig { - @Bean - public LettuceConnectionFactory redisConnectionFactory(){ - RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration("ip", 6379); - configuration.setPassword("password"); - return new LettuceConnectionFactory(configuration); - } -} -``` - -3. 存取数据 - -```java -@RestController -public class DemoController { - - @Resource(name = "redisTemplate") - private ListOperations listOps; - - @GetMapping("/test") - public String test(){ - listOps.leftPush("name", "jalon"); - System.out.println(listOps.leftPop("name")); - return "success"; - } - -} -``` - -**知识点** - -- @Resource注解:类似@Autowired,区别如下 - - @Autowired根据类型注入 - - @Resource根据名称注入,一般用在setter方法上,符合面向对象思想(不直接操作属性) -- 例子介绍:上面例子中的@Resource调用过程如下 - - 先通过name = "redisTemplate"找到对应的bean - - 然后不是直接将该bean赋值给ListOperations,因为他俩类型不一致,也不存在继承关系 - - 而是将该bean赋值给ListOperations的构造函数(ListOperations本身没有构造函数,但是它的实现类DefaultListOperations有,所以这里是将bean赋值给它的实现类的构造函数) - - 简单一点,可以理解为,先注入redisTemplate,然后注入ListOperations -- @Autowired注解: - - 用在构造函数上时,可省略 -## 参考 - -- [SpringDataRedis官网](https://docs.spring.io/spring-data/redis/docs/2.4.5/reference/html/#redis) - -- [@Resource注解](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-resource-annotation) \ No newline at end of file diff --git a/demo-email/README.md b/demo-email/README.md deleted file mode 100644 index dd6d8d4..0000000 --- a/demo-email/README.md +++ /dev/null @@ -1 +0,0 @@ -参考链接:https://hutool.cn/docs/#/extra/%E9%82%AE%E4%BB%B6%E5%B7%A5%E5%85%B7-MailUtil diff --git a/demo-mq-rabbitmq/README.md b/demo-mq-rabbitmq/README.md deleted file mode 100644 index 8af2568..0000000 --- a/demo-mq-rabbitmq/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# 整合RabbitMQ -RabbitMQ是基于AMQP协议创建的,轻量级、高可用的消息中间件 -## 核心组件 -用脑图展示比较靠谱 -1. AmqpAdmin:抽象接口,用来定义交换机、队列,并绑定他们之间的关系,使得消息开发更加灵活 -2. RabbitAdmin:实现了AmqpAdmin -3. ConnectionFactory接口:创建消息连接,最常用的实现类是 PooledChannelConnectionFactory -4. AmqpTemplate: 消息模板,用来发送、接收消息等基础操作,属于高级别的抽象接口,类似JDBCTemplate -5. RabbitTemplate:AmqpTemplate的实现类 -6. MessageListener: 消息监听器,2.0开始有了注解@MessageListen,可以很方便的监听消息 -6. Container: -- 容器是消息监听器和队列之间的桥梁,要设置的属性包括ConnectionFactory,Queue, MessageListener -- 默认SimpleMessageListenerContainer,还有新出的DirectMessageListenerContainer,区别见[Choosing Container](https://docs.spring.io/spring-amqp/docs/current/reference/html/#choose-container) diff --git a/demo-orm-jpa/README.md b/demo-orm-jpa/README.md deleted file mode 100644 index 892a705..0000000 --- a/demo-orm-jpa/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# 持久层技术:Spring Data JPA - -> Spring Data JPA 对JPA进行了更加抽象的封装,减少了很多样板代码 - - - -### 知识点 - -- Spring Data JPA - -### 问题 - -- 问题1: - - 问题描述: Access to DialectResolutionInfo cannot be null when 'hibernate.dialect' not set - - 原因分析:从报错信息看,是说没有配置`hibernate.dialect`,但实际上yml中有配置; - - 解决办法:删除JavaConfig配置 - - 参考链接:[stackoverflow](https://stackoverflow.com/questions/26548505/org-hibernate-hibernateexception-access-to-dialectresolutioninfo-cannot-be-null) - -- 问题2: - - - 问题描述:No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call - - - 原因分析:删除和更新操作,需要加事务,用来保证数据一致性, - - 解决办法:在repository的删除和更新操作方法上,加注解`@Modifying`和`@Transactional` \ No newline at end of file diff --git a/demo-spring-security/README.md b/demo-spring-security/README.md deleted file mode 100644 index 4ce4392..0000000 --- a/demo-spring-security/README.md +++ /dev/null @@ -1,37 +0,0 @@ -## 简介 -SpringSecurity项目学习案例 -参考 -- [baeldung网站](https://www.baeldung.com/security-spring) -- [SpringSecurity官网](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-applications) -## 目录 -1. 认证机制:有多种 -- 基于用户名/密码的认证方式 - - From Login,表单登录认证 - - Basic Authentication,基本认证 - - Digest Authentication,数字认证(已废弃,不再使用这种认证方式,因为它的加密方式不安全,比如md5加密等;现在比较安全的加密方式有BCrypt等) -- 基于OAuth2的认证方式: - - - -#### 基础: -1. Spring Security注册流程 -2. SpringSecurity登录表单 -3. 基于接口认证(流行) -4. 登录时的异常处理 -5. 登出 -6. 登录之后重定向页面 -7. 记住我 -8. SpringSecurity认证提供器 -9. 手动管理认证的用户 -10. 额外的登录字段 -11. 自定义登录失败的处理器 -#### 核心: -1. Maven配置SpringSecurity -2. 在Spring Security中检索用户信息 -3. 介绍Spring Security的表达式 -4. 不加密、不过滤、允许所有的访问请求 -5. Session管理 -6. 介绍SpringSecurity方法级别的安全(前面介绍的都是请求级别,也就是URL级别) -7. Spring Security自动配置 -8. Spring Security5中的密码加密器 -9. 查找注册的Spring Security过滤器,比如前端vue,后端rest \ No newline at end of file diff --git a/demo-spring-security/demo-spring-security-auth-failure-handler/README.md b/demo-spring-security/demo-spring-security-auth-failure-handler/README.md deleted file mode 100644 index a9d406e..0000000 --- a/demo-spring-security/demo-spring-security-auth-failure-handler/README.md +++ /dev/null @@ -1,208 +0,0 @@ -## 简介 - -在我们之前演示的表单登录例子中,登录失败后默认会再次跳转到登录界面,没有任何提示; - -这里我们可以做一些小改动,在登录失败后增加一些提示信息,使其更加符合我们真实使用的场景; - -## 目录 - -1. 自定义的认证失败处理器 -2. 内置的认证失败处理器 -3. 配置异常处理器 -4. 实践 - -## 正文 - -### 1. 自定义的认证失败处理器 - -这里我们自己定义一个**认证失败处理器**,用来处理认证失败时的相关操作;这里我们返回一个时间戳和异常的提示信息 - -```java -public class CustomAuthenticationFailureHandler - implements AuthenticationFailureHandler { - - private ObjectMapper objectMapper = new ObjectMapper(); - - @Override - public void onAuthenticationFailure( - HttpServletRequest request, - HttpServletResponse response, - AuthenticationException exception) - throws IOException, ServletException { - - response.setStatus(HttpStatus.UNAUTHORIZED.value()); - Map data = new HashMap<>(); - data.put( - "timestamp", - Calendar.getInstance().getTime()); - data.put( - "exception", - exception.getMessage()); - - response.getOutputStream() - .println(objectMapper.writeValueAsString(data)); - } -} - -``` - -### 2. 内置的认证失败处理器 - -其实SpringSecurity内部也有提供几种认证失败的处理方式; - -1. `SimpleUrlAuthenticationFailureHandler `:这个就是默认的处理方式,即设定一个认证失败跳转的url;那么在认证失败时,就会跳转到特定的url; - - - 通过`failureUrl(url)`来设定跳转的url; - - - 如果没有设定,那么会重定向到默认的跳转页面:登录页面login并携带?error参数,比如`http://localhost:8090/login?error`; - - - 如果我们直接设定url为null,程序启动不了,因为设定的url不允许null,那源码中的null检测有啥用呢?其实是有办法设置null的,那就是通过重新构造一个空的`SimpleUrlAuthenticationFailureHandler`来实现,此时认证失败会返回401未授权的错误; - - - 不过前端的表现是重定向到login页面,因为401会被系统监测到,然后自动跳转到error页面; - - 但是因为没有`/error`的访问需要权限,所以会重定向到login页面; - - 如果配置一个`.antMatchers("/error").permitAll()`那么就会跳转到error页面; - - 并且通过F12可看到返回的错误码401; - - 下面是`SimpleUrlAuthenticationFailureHandler`的核心处理方法: - - ```java - @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) throws IOException, ServletException { - if (this.defaultFailureUrl == null) { - if (this.logger.isTraceEnabled()) { - this.logger.trace("Sending 401 Unauthorized error since no failure URL is set"); - } - else { - this.logger.debug("Sending 401 Unauthorized error"); - } - response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); - return; - } - saveException(request, exception); - if (this.forwardToDestination) { - this.logger.debug("Forwarding to " + this.defaultFailureUrl); - request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response); - } - else { - this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl); - } - } - ``` - -2. `ForwardAuthenticationFailureHandler`:这个处理器跟上面的类似,也是设定一个url,然后跳转; - - - 不同的是,他会把错误信息设置到request的SPRING_SECURITY_LAST_EXCEPTION属性中,然后进行**页面跳转**; - - 而上面的处理器是设置到response中,进行**页面的重定向**;不过上面的处理器如果有设置`forwardToDestination`属性,那么就跟`ForwardAuthenticationFailureHandler`一样了; - - 下面是`ForwardAuthenticationFailureHandler`的核心处理方法: - - ```java - @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) throws IOException, ServletException { - request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); - request.getRequestDispatcher(this.forwardUrl).forward(request, response); - } - ``` - -3. `ExceptionMappingAuthenticationFailureHandler `:这个处理器继承自默认的`SimpleUrlAuthenticationFailureHandler`,它的功能就比较丰富了,它可以根据异常的不同,跳转到不同的url; - - - 前提是要定义好map映射,其中map的key就是异常类的名称,map的value就是对应的异常要跳转的url; - - - 如果没有找到对应的url,就会回滚到`SimpleUrlAuthenticationFailureHandler`处理器进行处理; - -4. `DelegatingAuthenticationFailureHandler`:委托处理器,它自己不实现异常处理,而是根据异常的不同,委托给不同的处理器(就是上面的这些,还有其他的一些);这种方式会更加灵活; - -### 3. 配置异常处理器 - -配置的方式还是跟往常一样,先注入,再配置,如下所示: - -```java -@Configuration -@EnableWebSecurity -@Slf4j -public class SecurityConfiguration extends WebSecurityConfigurerAdapter { - - @Bean - public AuthenticationFailureHandler authenticationFailureHandler() { - return new CustomAuthenticationFailureHandler(); - } - - // 认证相关操作 - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - log.info("=== SecurityConfiguration.authenticate ==="); - // 数据没有持久化,只是保存在内存中 - auth.inMemoryAuthentication() - .withUser("javalover").password(passwordEncoder().encode("123456")).roles("USER") - .and() - .withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN"); - } - - // 授权相关操作 - @Override - protected void configure(HttpSecurity http) throws Exception { - log.info("=== SecurityConfiguration.authorize ==="); - http - // 关闭csrf,此时登出logout接收任何形式的请求;(默认开启,logout只接受post请求) - .csrf().disable() - .authorizeRequests() - // admin页面,只有admin角色可以访问 - .antMatchers("/admin").hasRole("ADMIN") - // home 页面,ADMIN 和 USER 都可以访问 - .antMatchers("/home").hasAnyRole("USER", "ADMIN") - // login 页面,所有用户都可以访问 - .antMatchers("/login").permitAll() - .anyRequest().authenticated() - .and() - // 自定义登录表单 - .formLogin().loginPage("/login") - // 登录成功跳转的页面,第二个参数true表示每次登录成功都是跳转到home,如果false则表示跳转到登录之前访问的页面 - .defaultSuccessUrl("/home", true) - // 失败跳转的页面(比如用户名/密码错误),这里还是跳转到login页面,只是给出错误提示 -// .failureUrl("/login?error=true") - .failureHandler(authenticationFailureHandler()) - .and() - // 登出 所有用户都可以访问 - .logout().permitAll(); - } - - // 定义一个密码加密器,这个BCrypt也是Spring默认的加密器 - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - -} - -``` - -可以看到,我们注释了之前的`.failureUrl("/login?error=true")`方法,改用` .failureHandler(authenticationFailureHandler())`方法; - -这样当认证失败时,会使用自定义的`CustomAuthenticationFailureHandler`处理器,此时会返回一个错误信息,包括时间戳和异常的提示信息; - -### 4. 实践 - -其他的登录和控制器代码就不贴了,都是跟之前的一样; - -下面我们就启动测试一下,访问`http://localhost:8090/login`,输入错误的用户名/密码,如下所示: - -![image-20211124115717289](https://i.loli.net/2021/11/24/CQPKOAveLDUWSwg.png) - -点击登录后,会按照预期的设定进行提示,如下所示: - -![image-20211124115745548](https://i.loli.net/2021/11/24/Ffk3EHvCaLmZp52.png) - -这里需要的话,可以在前端进行判断,然后弹框进行相应的提示 - -## 总结 - -这里介绍了多种认证失败的异常处理方式,包括自定义的,以及官方内置的; - -一般我们用默认的`SimpleUrlAuthenticationFailureHandler`就可以了; - -这里自定义的认证失败处理器,主要是为了了解认证失败的相关运行机制; - -[源码地址](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-auth-failure-handler) - diff --git a/demo-spring-security/demo-spring-security-basic-auth/README.md b/demo-spring-security/demo-spring-security-basic-auth/README.md deleted file mode 100644 index 7da0db5..0000000 --- a/demo-spring-security/demo-spring-security-basic-auth/README.md +++ /dev/null @@ -1,265 +0,0 @@ - - -## 简介 - -SpringSecurity的认证机制有多种,比如基于用户名/密码的认证,基于OAuth2.0的认证(OAuth已废弃)。。。 - -而基于用户名/密码的认证方式,又分多种,比如: - -- Form Login,表单登录认证(单体应用,比如SpringMVC) -- Basic Authentication,基本的http认证(前后端分离应用) -- 【已废弃】Digest Authentication,数字认证(已废弃,不再使用这种认证方式,因为它的加密方式不安全,比如md5加密等;现在比较安全的加密方式有BCrypt等) - -本节介绍的就是第二种:**基本认证的方式** - -## 目录 - -1. maven配置 -2. security配置 -3. controller控制器 -4. web界面 -5. 启动运行 - -## 正文 - -在开始之前,需要先了解两个词 - -- Authenticate认证:就是通过用户名/密码等方式,登入到系统,这个过程就是认证;类似于进入景区的大门 -- Authorize授权:就是登入到系统之后,校验用户是否有权限操作某个模块,这个过程就是授权;类似于进入景区后,各个收费区域,只有交了钱(有权限),才能进入指定区域; - -项目背景:Spring Boot + Vue - -项目结构如下:src/main目录就是后端接口,src/web目录就是前端界面 - -![image-20211112170900370](https://i.loli.net/2021/11/12/XirCJog4VSQWTcH.png) - -#### 1.maven配置 - -```xml - - - - spring-boot-demo - com.jalon - 0.0.1-SNAPSHOT - - 4.0.0 - - demo-spring-security - - - 8 - 8 - - - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-web - - - org.projectlombok - lombok - - - -``` - -#### 2. security配置 - -这里面主要包含两部分: - -- authenticate 认证配置:主要配置用户名,密码,角色(这里基于内存来保存,为了简化) -- authorize 授权配置:主要配置各个角色的权限,即可以访问哪些页面 - -```java -@Configuration -@EnableWebSecurity -public class CustomConfig extends WebSecurityConfigurerAdapter implements WebMvcConfigurer{ - - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**"); - } - - // 全局配置:用户名+密码,角色(基于内存) - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - auth.inMemoryAuthentication() - .withUser("javalover").password( passwordEncoder().encode("123456")) - .authorities("ROLE_USER"); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.cors().and() - .authorizeRequests() - .antMatchers("/login").permitAll() - .anyRequest().authenticated() - .and() - // 这里的httpBasic就是说明这里的认证不是通过登录表单,而是基于http形式的请求来认证 - .httpBasic(); - } - - // 这里的密码加密器需注入,不然会提示缺少默认的密码加密器 - @Bean - public PasswordEncoder passwordEncoder(){ - return new BCryptPasswordEncoder(); - } - -} -``` - -#### 3. controller控制器 - -控制器主要任务就是处理请求,这里我们只写了一个方法,用来测试认证和不认证的区别 - -```java -@RestController -public class UserController { - - @GetMapping("/home") - public String home(){ - return "home"; - } -} - -``` - -#### 4. 命令行请求测试 - -下面我们对上面的接口进行测试; - -先执行下面的请求命令:不传用户名/密码 - -```bash -curl -i http://localhost:8090/home -``` - -不出意料,会报401未授权的错误,如下所示: - -```bash -HTTP/1.1 401 -Vary: Origin -Vary: Access-Control-Request-Method -Vary: Access-Control-Request-Headers -Set-Cookie: JSESSIONID=37DBDC431DCA48BEBBEE1A65B82582C1; Path=/; HttpOnly -WWW-Authenticate: Basic realm="Realm" -X-Content-Type-Options: nosniff -X-XSS-Protection: 1; mode=block -Cache-Control: no-cache, no-store, max-age=0, must-revalidate -Pragma: no-cache -Expires: 0 -X-Frame-Options: DENY -Content-Type: application/json -Transfer-Encoding: chunked -Date: Fri, 12 Nov 2021 09:31:09 GMT - -{"timestamp":"2021-11-12T09:31:09.788+00:00","status":401,"error":"Unauthorized","message":"","path":"/home"} -``` - -接下来,我们传入配置中设置的用户名/密码(javalover/123456),如下所示: - -```bash -curl -i --user javalover:123456 http://localhost:8090/home -``` - -此时,返回200,而且返回结果为home,表示请求成功,如下所示: - -```bash -HTTP/1.1 200 -Vary: Origin -Vary: Access-Control-Request-Method -Vary: Access-Control-Request-Headers -Set-Cookie: JSESSIONID=1A81AF17E7EB380E16F0E8854DA59452; Path=/; HttpOnly -X-Content-Type-Options: nosniff -X-XSS-Protection: 1; mode=block -Cache-Control: no-cache, no-store, max-age=0, must-revalidate -Pragma: no-cache -Expires: 0 -X-Frame-Options: DENY -Content-Type: text/plain;charset=UTF-8 -Content-Length: 4 -Date: Fri, 12 Nov 2021 09:31:44 GMT - -home -``` - -如果我们没用curl进行请求,而是在浏览器,那么浏览器会自动弹出一个登录窗口,让我们填写用户名密码,如下所示: - -![image-20211112173718774](https://i.loli.net/2021/11/12/3hgtFJZUiR1AzBn.png)我们填入`javalover/123456`,就可以看到返回的数据了: - -![image-20211112173806301](https://i.loli.net/2021/11/12/TOrkU1wMLdqQbKS.png) - -#### 5. vue界面请求测试 - -刚才我们用curl测了请求,现在我们用vue来试下,核心文件如下:HelloWorld.vue - -```vue - - - -``` - -下面我们启动vue`yarn run serve`,访问主界面`http://localhost:8080/`,点击主页:可以看到打印了 success - -![image-20211112175126444](https://i.loli.net/2021/11/12/JjARF12CPwNdXHY.png) - - - -同理,如果我们把HelloWorld.vue中的用户名/密码配置拿掉,那么就会打印fail,报401错误: - -![image-20211112175337702](https://i.loli.net/2021/11/12/CO8Qm5NAVtP2bwI.png) - -## 总结 - -SpringSecurity的基本认证方式跟表单认证方式,后端的代码其实差不多,就是配置的地方不一样; - -这俩的核心都是通过用户名/密码的方式进行认证,只是适用的场景不同: - -- Form Login,表单登录认证,适用于单体应用 - -- Basic Authentication,基本的http认证,适用于前后端分离的应用 - - - -源码地址:[demo-spring-security-basic-auth](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-basic-auth) diff --git a/demo-spring-security/demo-spring-security-basic-auth/web/basic-auth/README.md b/demo-spring-security/demo-spring-security-basic-auth/web/basic-auth/README.md deleted file mode 100644 index 1b3ed18..0000000 --- a/demo-spring-security/demo-spring-security-basic-auth/web/basic-auth/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# basic-auth - -## Project setup -``` -yarn install -``` - -### Compiles and hot-reloads for development -``` -yarn serve -``` - -### Compiles and minifies for production -``` -yarn build -``` - -### Lints and fixes files -``` -yarn lint -``` - -### Customize configuration -See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/demo-spring-security/demo-spring-security-block-brute-force-auth/README.md b/demo-spring-security/demo-spring-security-block-brute-force-auth/README.md deleted file mode 100644 index a9d406e..0000000 --- a/demo-spring-security/demo-spring-security-block-brute-force-auth/README.md +++ /dev/null @@ -1,208 +0,0 @@ -## 简介 - -在我们之前演示的表单登录例子中,登录失败后默认会再次跳转到登录界面,没有任何提示; - -这里我们可以做一些小改动,在登录失败后增加一些提示信息,使其更加符合我们真实使用的场景; - -## 目录 - -1. 自定义的认证失败处理器 -2. 内置的认证失败处理器 -3. 配置异常处理器 -4. 实践 - -## 正文 - -### 1. 自定义的认证失败处理器 - -这里我们自己定义一个**认证失败处理器**,用来处理认证失败时的相关操作;这里我们返回一个时间戳和异常的提示信息 - -```java -public class CustomAuthenticationFailureHandler - implements AuthenticationFailureHandler { - - private ObjectMapper objectMapper = new ObjectMapper(); - - @Override - public void onAuthenticationFailure( - HttpServletRequest request, - HttpServletResponse response, - AuthenticationException exception) - throws IOException, ServletException { - - response.setStatus(HttpStatus.UNAUTHORIZED.value()); - Map data = new HashMap<>(); - data.put( - "timestamp", - Calendar.getInstance().getTime()); - data.put( - "exception", - exception.getMessage()); - - response.getOutputStream() - .println(objectMapper.writeValueAsString(data)); - } -} - -``` - -### 2. 内置的认证失败处理器 - -其实SpringSecurity内部也有提供几种认证失败的处理方式; - -1. `SimpleUrlAuthenticationFailureHandler `:这个就是默认的处理方式,即设定一个认证失败跳转的url;那么在认证失败时,就会跳转到特定的url; - - - 通过`failureUrl(url)`来设定跳转的url; - - - 如果没有设定,那么会重定向到默认的跳转页面:登录页面login并携带?error参数,比如`http://localhost:8090/login?error`; - - - 如果我们直接设定url为null,程序启动不了,因为设定的url不允许null,那源码中的null检测有啥用呢?其实是有办法设置null的,那就是通过重新构造一个空的`SimpleUrlAuthenticationFailureHandler`来实现,此时认证失败会返回401未授权的错误; - - - 不过前端的表现是重定向到login页面,因为401会被系统监测到,然后自动跳转到error页面; - - 但是因为没有`/error`的访问需要权限,所以会重定向到login页面; - - 如果配置一个`.antMatchers("/error").permitAll()`那么就会跳转到error页面; - - 并且通过F12可看到返回的错误码401; - - 下面是`SimpleUrlAuthenticationFailureHandler`的核心处理方法: - - ```java - @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) throws IOException, ServletException { - if (this.defaultFailureUrl == null) { - if (this.logger.isTraceEnabled()) { - this.logger.trace("Sending 401 Unauthorized error since no failure URL is set"); - } - else { - this.logger.debug("Sending 401 Unauthorized error"); - } - response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); - return; - } - saveException(request, exception); - if (this.forwardToDestination) { - this.logger.debug("Forwarding to " + this.defaultFailureUrl); - request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response); - } - else { - this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl); - } - } - ``` - -2. `ForwardAuthenticationFailureHandler`:这个处理器跟上面的类似,也是设定一个url,然后跳转; - - - 不同的是,他会把错误信息设置到request的SPRING_SECURITY_LAST_EXCEPTION属性中,然后进行**页面跳转**; - - 而上面的处理器是设置到response中,进行**页面的重定向**;不过上面的处理器如果有设置`forwardToDestination`属性,那么就跟`ForwardAuthenticationFailureHandler`一样了; - - 下面是`ForwardAuthenticationFailureHandler`的核心处理方法: - - ```java - @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) throws IOException, ServletException { - request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); - request.getRequestDispatcher(this.forwardUrl).forward(request, response); - } - ``` - -3. `ExceptionMappingAuthenticationFailureHandler `:这个处理器继承自默认的`SimpleUrlAuthenticationFailureHandler`,它的功能就比较丰富了,它可以根据异常的不同,跳转到不同的url; - - - 前提是要定义好map映射,其中map的key就是异常类的名称,map的value就是对应的异常要跳转的url; - - - 如果没有找到对应的url,就会回滚到`SimpleUrlAuthenticationFailureHandler`处理器进行处理; - -4. `DelegatingAuthenticationFailureHandler`:委托处理器,它自己不实现异常处理,而是根据异常的不同,委托给不同的处理器(就是上面的这些,还有其他的一些);这种方式会更加灵活; - -### 3. 配置异常处理器 - -配置的方式还是跟往常一样,先注入,再配置,如下所示: - -```java -@Configuration -@EnableWebSecurity -@Slf4j -public class SecurityConfiguration extends WebSecurityConfigurerAdapter { - - @Bean - public AuthenticationFailureHandler authenticationFailureHandler() { - return new CustomAuthenticationFailureHandler(); - } - - // 认证相关操作 - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - log.info("=== SecurityConfiguration.authenticate ==="); - // 数据没有持久化,只是保存在内存中 - auth.inMemoryAuthentication() - .withUser("javalover").password(passwordEncoder().encode("123456")).roles("USER") - .and() - .withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN"); - } - - // 授权相关操作 - @Override - protected void configure(HttpSecurity http) throws Exception { - log.info("=== SecurityConfiguration.authorize ==="); - http - // 关闭csrf,此时登出logout接收任何形式的请求;(默认开启,logout只接受post请求) - .csrf().disable() - .authorizeRequests() - // admin页面,只有admin角色可以访问 - .antMatchers("/admin").hasRole("ADMIN") - // home 页面,ADMIN 和 USER 都可以访问 - .antMatchers("/home").hasAnyRole("USER", "ADMIN") - // login 页面,所有用户都可以访问 - .antMatchers("/login").permitAll() - .anyRequest().authenticated() - .and() - // 自定义登录表单 - .formLogin().loginPage("/login") - // 登录成功跳转的页面,第二个参数true表示每次登录成功都是跳转到home,如果false则表示跳转到登录之前访问的页面 - .defaultSuccessUrl("/home", true) - // 失败跳转的页面(比如用户名/密码错误),这里还是跳转到login页面,只是给出错误提示 -// .failureUrl("/login?error=true") - .failureHandler(authenticationFailureHandler()) - .and() - // 登出 所有用户都可以访问 - .logout().permitAll(); - } - - // 定义一个密码加密器,这个BCrypt也是Spring默认的加密器 - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - -} - -``` - -可以看到,我们注释了之前的`.failureUrl("/login?error=true")`方法,改用` .failureHandler(authenticationFailureHandler())`方法; - -这样当认证失败时,会使用自定义的`CustomAuthenticationFailureHandler`处理器,此时会返回一个错误信息,包括时间戳和异常的提示信息; - -### 4. 实践 - -其他的登录和控制器代码就不贴了,都是跟之前的一样; - -下面我们就启动测试一下,访问`http://localhost:8090/login`,输入错误的用户名/密码,如下所示: - -![image-20211124115717289](https://i.loli.net/2021/11/24/CQPKOAveLDUWSwg.png) - -点击登录后,会按照预期的设定进行提示,如下所示: - -![image-20211124115745548](https://i.loli.net/2021/11/24/Ffk3EHvCaLmZp52.png) - -这里需要的话,可以在前端进行判断,然后弹框进行相应的提示 - -## 总结 - -这里介绍了多种认证失败的异常处理方式,包括自定义的,以及官方内置的; - -一般我们用默认的`SimpleUrlAuthenticationFailureHandler`就可以了; - -这里自定义的认证失败处理器,主要是为了了解认证失败的相关运行机制; - -[源码地址](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-auth-failure-handler) - diff --git a/demo-spring-security/demo-spring-security-jpa/README.md b/demo-spring-security/demo-spring-security-jpa/README.md deleted file mode 100644 index aadbba1..0000000 --- a/demo-spring-security/demo-spring-security-jpa/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## 简介 -基于JPA的SpringSecurity -这一版跟之前的唯一不同就是,使用了JPA,将数据持久化了 diff --git a/demo-spring-security/demo-spring-security-login-form/README.md b/demo-spring-security/demo-spring-security-login-form/README.md deleted file mode 100644 index b280551..0000000 --- a/demo-spring-security/demo-spring-security-login-form/README.md +++ /dev/null @@ -1,314 +0,0 @@ - - -## 简介 - -SpringSecurity的认证机制有多种,比如基于用户名/密码的认证,基于OAuth2.0的认证(OAuth已废弃)。。。 - -而基于用户名/密码的认证方式,又分多种,比如: - -- Form Login,表单登录认证(单体应用,比如SpringMVC) -- Basic Authentication,基本的http认证(前后端分离应用) -- 【已废弃】Digest Authentication,数字认证(已废弃,不再使用这种认证方式,因为它的加密方式不安全,比如md5加密等;现在比较安全的加密方式有BCrypt等) - -本节介绍的就是第一种:**表单登录的认证方式** - -## 目录 - -1. maven配置 -2. security配置 -3. controller控制器 -4. web界面 -5. 启动运行 - -## 正文 - -在开始之前,需要先了解两个词 - -- Authenticate认证:就是通过用户名/密码等方式,登入到系统,这个过程就是认证;类似于进入景区的大门 -- Authorize授权:就是登入到系统之后,校验用户是否有权限操作某个模块,这个过程就是授权;类似于进入景区后,各个收费区域,只有交了钱(有权限),才能进入指定区域; - -项目背景:Spring Boot + SpringMVC + Thymeleaf - -项目结构如下: - -![image-20210603141803128](https://i.loli.net/2021/06/03/FSsi8QHcnlxAyf3.png) - -#### 1.maven配置: - -```xml - - - - spring-boot-demo - com.jalon - 0.0.1-SNAPSHOT - - 4.0.0 - demo-spring-security-login-form - - 1.8 - 2.4.3 - 1.18.16 - 1.8 - 1.8 - - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-web - - - org.projectlombok - lombok - provided - - - -``` - -#### 2.security配置 - -这里面主要包含两部分: - -- authenticate 认证配置:主要配置用户名,密码,角色(这里基于内存来保存,为了简化) -- authorize 授权配置:主要配置各个角色的权限,即可以访问哪些页面 - -```java -@Configuration -@EnableWebSecurity -public class SecurityConfiguration extends WebSecurityConfigurerAdapter { - - // 认证相关操作 - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - // 数据没有持久化,只是保存在内存中 - auth.inMemoryAuthentication() - .withUser("javalover").password(passwordEncoder().encode("123456")).roles("USER") - .and() - .withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN"); - } - - // 授权相关操作 - @Override - protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests() - // admin页面,只有admin角色可以访问 - .antMatchers("/admin").hasRole("ADMIN") - // home 页面,ADMIN 和 USER 都可以访问 - .antMatchers("/home").hasAnyRole("USER", "ADMIN") - // login 页面,所有用户都可以访问 - .antMatchers("/login").permitAll() - .anyRequest().authenticated() - .and() - // 自定义登录表单 - .formLogin().loginPage("/login") - // 登录成功跳转的页面,第二个参数true表示每次登录成功都是跳转到home,如果false则表示跳转到登录之前访问的页面 - .defaultSuccessUrl("/home", true) - // 失败跳转的页面(比如用户名/密码错误),这里还是跳转到login页面,只是给出错误提示 - .failureUrl("/login?error=true") - .and() - .logout().permitAll() - .and() - // 权限不足时跳转的页面,即访问一个页面时没有对应的权限,会跳转到这个页面 - .exceptionHandling().accessDeniedPage("/accessDenied"); - } - - // 定义一个密码加密器,这个BCrypt也是Spring默认的加密器 - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - -} -``` - -#### 3. controller控制器 - -控制器主要任务就是处理请求,下面就是典型的MVC模式 - -```java -@Controller -@Slf4j -public class SecurityController { - - @RequestMapping("/login") - public String login(){ - log.info("=== login ==="); - return "login"; - } - - @RequestMapping("/home") - public String home(Model model){ - model.addAttribute("user", getUsername()); - model.addAttribute("role", getAuthority()); - return "home"; - } - - @RequestMapping("/admin") - public String admin(Model model){ - model.addAttribute("user", getUsername()); - model.addAttribute("role", getAuthority()); - return "admin"; - } - - // 权限不足 - @RequestMapping("/accessDenied") - public String accessDenied(Model model){ - model.addAttribute("user", getUsername()); - model.addAttribute("role", getAuthority()); - return "access_denied"; - } - - // 获取当前登录的用户名 - private String getUsername(){ - return SecurityContextHolder.getContext().getAuthentication().getName(); - } - - // 获取当前登录的用户角色:因为有可能一个用户有多个角色,所以需遍历 - private String getAuthority(){ - Collection authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities(); - ArrayList list = new ArrayList<>(); - for(GrantedAuthority authority: authorities){ - list.add(authority.getAuthority()); - } - log.info("=== authority:" + list); - return list.toString(); - } -} -``` - -#### 4. web界面 - -界面有4个: - -- login.html: 登录界面,所有人都可以访问 -- home.html: 主页面,普通用户和管理员可以访问 -- admin.html: 管理员页面,只有管理员可以访问 -- access_denied.html: 访问被拒绝页面,权限不足时会跳转到该页面;比如普通用户访问admin.html时 - -login.html - -```html - - - - - Spring Security - - - -
- Invalid username and password. -
-
- You have been logged out -
-
- - - -
- - -``` - - - -home.html - -```html - - - - - Spring Security Home - - - - 欢迎 - 你的权限是 - admin页面 - 退出 - - -``` - -admin.html - -```html - - - - - Spring Security Admin - - - - 欢迎 - 你的权限是 - 退出 - - -``` - -access_denied.html - -```html - - - - - Access Denied - - - - 没有权限访问页面 - 你的权限是 - 退出 - - -``` - -#### 5. 启动运行 - -访问 http://localhost:8088,会自动跳转到login界面,如下: - -![image-20210603143352317](https://i.loli.net/2021/06/03/1ryvFKGR4wTZ8WP.png) - -这里先用普通用户的身份来登录,javalover/123456,登陆后进入主页:可以看到,权限是普通用户 - -![image-20210603143453932](https://i.loli.net/2021/06/03/K24JhxYNAzC1o6L.png) - -这时点击`admin页面`就会提示权限不足,如下: - -![image-20210603143531292](https://i.loli.net/2021/06/03/3wEOuIRl2iQb8se.png) - -此时点击退出,又重新回到登录界面:并附有提示【已退出登录】 - -![image-20210603143631810](https://i.loli.net/2021/06/03/8HwOQKTMyz5hXiI.png) - -最后用管理账户登录,admin/123456,登录进入主页:可以看到,权限是管理员 - -![image-20210603143901708](https://i.loli.net/2021/06/03/Kvy1TUJPqn5OErc.png) - -这时点击`admin页面`,就会正常显示: - -![image-20210603144003914](https://i.loli.net/2021/06/03/acVsUEmMuFPpGAy.png) - -## 总结 - -SpringSecurity的表单登录认证,总的来说代码不是很多,因为很多功能SpringSecurity都是自带的(比如登录、登出、权限不足等),我们只需要根据自己的需求来修改一些配置就可以了 - - - -源码地址:[**demo-spring-security-login-form**](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-login-form) diff --git a/demo-spring-security/demo-spring-security-login-redirect/README.md b/demo-spring-security/demo-spring-security-login-redirect/README.md deleted file mode 100644 index 3fc9a8e..0000000 --- a/demo-spring-security/demo-spring-security-login-redirect/README.md +++ /dev/null @@ -1,250 +0,0 @@ -## 简介 - -通常一个后台管理系统,会包含不同的角色和权限; - -然后不同的角色,登录后会根据权限的不同,跳转到不同的界面; - -这里我们还是设定有两个角色:普通用户和管理员; - -- 普通用户:登录成功跳转到 home 页面; -- 管理员:登录成功跳转到 admin 页面; - -## 目录 - -- 基本配置 -- 配置用户和角色 -- 创建处理器类 -- 配置处理器 -- 修改控制器 -- 运行 - -## 正文 - -### 1. 基本配置 - -先配置一个默认的登录成功界面,如下所示: - -```java -@Configuration -@EnableWebSecurity -@Slf4j -public class SecSecurityConfig extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .authorizeRequests() - .formLogin() - .loginPage("/login") - // 登录成功跳转的页面,第二个参数true表示每次登录成功都是跳转到home,如果false则表示跳转到登录之前访问的页面 - .defaultSuccessUrl("/home.html", true) - // ... 其他配置 - - } -} -``` - -这样当用户登录成功后,会默认跳转到home.html界面; - -但是本节我们要做的就是修改这个地方,使得不同角色跳转不到不同的界面; - -下面开始进入主体 - -### 2. 配置用户和角色 - -这里我们用 全局配置: - -```java - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - // 数据没有持久化,只是保存在内存中 - auth.inMemoryAuthentication() - .withUser("javalover").password(passwordEncoder().encode("123456")).roles("USER") - .and() - .withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN"); - } -``` - -这里配置了两个用户: - -- 普通用户:javalover -- 管理员:admin - -### 3. 创建处理器类 - -第一步中的配置是:两个角色登录后,默认都是跳转到`home.html`界面; - -接下来就开始修改,使他们跳转到不同的界面; - -先定义一个处理器类,实现了`AuthenticationSuccessHandler`接口: - -```java -public class MySimpleUrlAuthenticationSuccessHandler - implements AuthenticationSuccessHandler { - - - private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); - - @Override - public void onAuthenticationSuccess(HttpServletRequest request, - HttpServletResponse response, Authentication authentication) - throws IOException { - - String targetUrl = determineTargetUrl(authentication); - - if (response.isCommitted()) { - logger.debug( - "Response has already been committed. Unable to redirect to " - + targetUrl); - return; - } - - redirectStrategy.sendRedirect(request, response, targetUrl); - } - -} - -``` - -覆写的`onAuthenticationSuccess`方法:登录成功会先到这个地方,然后我们就可以在这里控制下一步要跳转的界面(当然其他的一些操作也可以); - - - -`determineTargetUrl`方法:就是这篇文章的核心; - -它会根据不同的权限,获取到不同的跳转url,然后重定向; - -```java -protected String determineTargetUrl(final Authentication authentication) { - - Map roleTargetUrlMap = new HashMap<>(); - roleTargetUrlMap.put("ROLE_USER", "/home"); - roleTargetUrlMap.put("ROLE_ADMIN", "/admin"); - - final Collection authorities = authentication.getAuthorities(); - for (final GrantedAuthority grantedAuthority : authorities) { - String authorityName = grantedAuthority.getAuthority(); - if(roleTargetUrlMap.containsKey(authorityName)) { - return roleTargetUrlMap.get(authorityName); - } - } - - throw new IllegalStateException(); -} -``` - -### 4. 配置处理器 - -上面我们定义了一个处理器,用来控制登录成功后的跳转界面; - -这里我们将其配置到config类中; - -先在配置类中注入一个Bean:`AuthenticationSuccessHandler `,返回的是刚才创建的处理器类 - -```java -@Bean -public AuthenticationSuccessHandler myAuthenticationSuccessHandler(){ - return new MySimpleUrlAuthenticationSuccessHandler(); -} -``` - -然后替换掉开头设置的跳转url参数,如下所示: - -```java -@Override -protected void configure(final HttpSecurity http) throws Exception { - http - .authorizeRequests() - .formLogin() - .loginPage("/login") - .successHandler(myAuthenticationSuccessHandler()) - // ...其他配置 -} -``` - -### 5. 修改控制器 - -上面我们跳转home和admin,是通过控制器进行跳转的,下面我们配置一下控制器: - -```java -@Controller -@Slf4j -public class SecurityController { - - @RequestMapping("/login") - public String login(){ - log.info("=== login ==="); - return "login"; - } - - @RequestMapping("/home") - public String home(Model model){ - model.addAttribute("user", getUsername()); - model.addAttribute("role", getAuthority()); - return "home"; - } - - @RequestMapping("/admin") - public String admin(Model model){ - model.addAttribute("user", getUsername()); - model.addAttribute("role", getAuthority()); - return "admin"; - } - - @RequestMapping("/accessDenied") - public String accessDenied(Model model){ - model.addAttribute("user", getUsername()); - model.addAttribute("role", getAuthority()); - return "access_denied"; - } - - // 获取当前登录的用户名 - private String getUsername(){ - return SecurityContextHolder.getContext().getAuthentication().getName(); - } - - // 获取当前登录的用户角色 - private String getAuthority(){ - Collection authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities(); - ArrayList list = new ArrayList<>(); - for(GrantedAuthority authority: authorities){ - list.add(authority.getAuthority()); - } - log.info("=== authority:" + list); - return list.toString(); - } -} -``` - -### 6. 运行 - -接下来我们就可以启动程序,访问`http://localhost:8090/`进行测试了 - -> 前端代码就不贴了,就是三个界面:login,.html, home.html, admin.html。完整源码见文末 - -- 普通用户登录: - -![image-20211119123413681](https://i.loli.net/2021/11/19/Zt6Ds9gBxhOqH2A.png) - -跳转到home - -![image-20211119123425793](https://i.loli.net/2021/11/19/GDZEaugkNMBIK97.png) - -- 管理员登录: - -![image-20211119123533315](https://i.loli.net/2021/11/19/jWm1UARyIxfNpuL.png) - -跳转到admin - -![image-20211119123541475](https://i.loli.net/2021/11/19/AJGNECQVonU6mse.png) - -## - -## 总结 - -重定向的核心就是那个处理器中的`determineTargetUrl`方法,根据角色的不同,跳转到不同的界面; - - - -[源码地址](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-login-redirect) - diff --git a/demo-spring-security/demo-spring-security-logout/README.md b/demo-spring-security/demo-spring-security-logout/README.md deleted file mode 100644 index 7948f8d..0000000 --- a/demo-spring-security/demo-spring-security-logout/README.md +++ /dev/null @@ -1,168 +0,0 @@ - - -## 简介 - -前面我们介绍了[表单登录的入门案例](https://juejin.cn/post/7030306851762176007); - -本篇介绍下**登出**的入门案例,代码基于表单登录的案例进行演示; - -代码地址见文末 - -## 目录 - - - -## 正文 - -### 1. 基本配置 - -最基本的登出配置如下所示: - -```java - @Override - protected void configure(HttpSecurity http) throws Exception { - http - // 登出 所有用户都可以访问 - .logout().permitAll(); - } -``` - -这里默认的登出url为`/logout`,通过在url中访问`http://localhost:8090/logout`就可以登出了。 - -当然最方便的还是在界面中进行链接跳转,如下所示: - -```html - 退出 -``` - -### 2. 登出跳转 - -**logoutSuccessUrl配置**: - -登出跳转成功后的默认界面是根路径,比如`http://localhost:8090/`; - -下面我们可以进行简单的配置,配置成自己指定的界面,如下所示:一般推荐将登出成功后跳转的链接设置为登录界面(习惯) - -```java - @Override - protected void configure(HttpSecurity http) throws Exception { - http - // 登出 所有用户都可以访问 - .logout().permitAll() - .logoutSuccessUrl("/login"); - } -``` - -**logoutUrl配置:** - -登出跳转的默认url为`/logout`,比如`http://localhost:8090/logout`,如果登出成功,就跳转到上面配置的路径; - -配置如下所示: - -```java - @Override - protected void configure(HttpSecurity http) throws Exception { - http - // 登出 所有用户都可以访问 - .logout().permitAll() - .logoutUrl("/logout"); - } -``` - -### 3. 更新缓存 - -这里的缓存指的就是session和cookie; - -在登出之后,需要将session失效处理,并删除对应的cookie; - -对应的命令为:`invalidateHttpSession()` 和 `deleteCookies(...name)`; - -配置如下所示: - -> 其中删除的Cookies名称为`JSESSIONID`,这个就是前后端交互的一个凭证id,是在第一次前端请求后端时,后端返回的id;后续的请求后端会根据JSESSIONID来匹配对应的session - -```java - @Override - protected void configure(HttpSecurity http) throws Exception { - http - // 登出 所有用户都可以访问 - .logout().permitAll() - .logoutUrl("/logout") - .invalidateHttpSession(true) - .deleteCookies("JSESSIONID"); - } -``` - -### 4. 登出处理器 - -登出成功后,不仅可以设置特定的url,还可以执行一些自定义的操作; - -对应的命令为:`logoutSuccessHandler` - -比如我们需要记录登出时访问的最后一个界面,那么可以通过如下的代码来实现; - -先定义一个处理器:`CustomLogoutSuccessHandler.java` - -```java -public class CustomLogoutSuccessHandler extends - SimpleUrlLogoutSuccessHandler implements LogoutSuccessHandler { - - @Override - public void onLogoutSuccess( - HttpServletRequest request, - HttpServletResponse response, - Authentication authentication) - throws IOException, ServletException { - - String refererUrl = request.getHeader("Referer"); - System.out.println("Logout from: " + refererUrl); - - super.onLogoutSuccess(request, response, authentication); - } -} -``` - -然后在配置中注入该处理器,通过方法注入,如下所示: - -```java -public class SecurityConfiguration extends WebSecurityConfigurerAdapter { - - @Bean - public LogoutSuccessHandler logoutSuccessHandler(){ - return new CustomLogoutSuccessHandler(); - } - @Override - protected void configure(HttpSecurity http) throws Exception { - http - // 登出 所有用户都可以访问 - .logout() - .permitAll() - .logoutSuccessUrl("/login") - .logoutUrl("/logout") - .logoutSuccessHandler(logoutSuccessHandler()); - } - -} - -``` - -这样我们在登出时,就可以看到控制台打印下面的内容: - -```bash -Logout from: http://localhost:8090/home -``` - -## 总结 - -本篇介绍了登出的相关配置和处理; - -配置有: - -- logoutUrl(): 登出链接配置 -- logoutSuccessUrl(): 登出成功后的跳转链接 -- invalidateHttpSession: 失效session -- deleteCookies() : 删除对应cookie,多个cookieName逗号分隔 -- LogoutSuccessHandler:登出后执行的自定义操作 - -[源码地址](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-logout) - diff --git a/demo-spring-security/demo-spring-security-manually-login/README.md b/demo-spring-security/demo-spring-security-manually-login/README.md deleted file mode 100644 index 59435af..0000000 --- a/demo-spring-security/demo-spring-security-manually-login/README.md +++ /dev/null @@ -1,177 +0,0 @@ - - -## 简介 - -前面我们的SpringSecurity文章介绍的登录认证,不管是`form-login-auth`表单登录认证,还是`basic-auth`基本方式的认证,都是通过SpringSecurity系统自动进行的认证,我们并没用人工干预; - -比如表单登录认证,我们在表单中提交`login`的表单登录请求后,后台并没有自己写对应的处理器,而是由系统自动认证的; - -相应的,基本方式的认证也是直接把用户名密码写入header中,系统自动认证; - -那今天我们就来手动进行认证,以此熟悉下认证的过程 - -## 目录 - -## 正文 - -### 1. 安全配置 - -一如既往,还是简单配置一个用户,一个登录请求; - -不过这次的路径匹配多了一个自定义的manually-login:`.antMatchers("/manually-login").permitAll()`; - -这个就是我们在提交表单时要请求的路径,如果不开放,请求后会再次跳转到登录界面(因为没有权限); - -```java -@Configuration -@EnableWebSecurity -@Slf4j -public class SecurityConfiguration extends WebSecurityConfigurerAdapter { - - // 认证相关操作 - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - log.info("=== SecurityConfiguration.authenticate ==="); - // 数据没有持久化,只是保存在内存中 - auth.inMemoryAuthentication() - .withUser("javalover").password(passwordEncoder().encode("123456")).roles("USER"); - } - - // 授权相关操作 - @Override - protected void configure(HttpSecurity http) throws Exception { - log.info("=== SecurityConfiguration.authorize ==="); - http - // home 页面,ADMIN 和 USER 都可以访问 - .antMatchers("/home").hasAnyRole("USER", "ADMIN") - // login 页面,所有用户都可以访问 - .antMatchers("/manually-login").permitAll() - .antMatchers("/login").permitAll() - .anyRequest().authenticated() - .and() - // 自定义登录表单 - .formLogin().loginPage("/login") - // 登录成功跳转的页面,第二个参数true表示每次登录成功都是跳转到home,如果false则表示跳转到登录之前访问的页面 - .defaultSuccessUrl("/home", true) - // 失败跳转的页面(比如用户名/密码错误),这里还是跳转到login页面,只是给出错误提示 - .failureUrl("/login?error=true") - .and() - // 登出 所有用户都可以访问 - .logout().permitAll() - } - - // 定义一个密码加密器,这个BCrypt也是Spring默认的加密器 - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - -} - -``` - -### 2. 表单组件 - -登录表单如下所示:这里我们跳转的路径改为了`manually-login`,之前是`login`;这样登录请求时,请求登录认证会由我们自己进行处理; - -```html -
- - - -
-``` - -### 3. 控制器 - -这个控制器里有一个`manuallyLogin`就是负责接收上面的登录请求,如下所示: - -```java -@Controller -@Slf4j -public class SecurityController { - - @RequestMapping("/login") - public String login(){ - log.info("=== login ==="); - return "login"; - } - - @Autowired - AuthenticationManager authManager; - - @PostMapping(path="/manually-login", consumes={APPLICATION_FORM_URLENCODED_VALUE}) - public String manuallyLogin(HttpServletRequest request, String username, String password){ - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); - Authentication authentication = authManager.authenticate(authenticationToken); - SecurityContext context = SecurityContextHolder.getContext(); - context.setAuthentication(authentication); - HttpSession session = request.getSession(true); - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); - System.out.println("是否认证通过:"+context.getAuthentication()); - return "redirect:/home" ; - } - // ...省略其他的 - -} - -``` - -AuthenticationManager会在安全配置中配置,下面会介绍; - -这里先介绍下manuallyLogin方法中的认证步骤: - -1. 首先构建一个`UsernamePasswordAuthenticationToken`,根据用户名和密码(这里的用户名和密码就是通过登录表单传入的) - 1. `UsernamePasswordAuthenticationToken`类是`Authentication`的实现类; - 2. 主要实现的功能就是通过用户名/密码来认证用户,构造函数实现 - 3. 然后通过`isAuthenticated`判断是否认证成功 -2. 然后通过`AuthenticationManager`对上面的`token`进行认证;这一步是关键,如果用户名密码错误或者状态异常,都会在这里报错; -3. 认证通过后,将认证结果`Authentication`保存到`SecurityContext`上下文中(这个上下文主要工作就是负责认证信息的获取和设保存); - 1. 这里如果不保存,那么认证就没意义了,后续的访问还是会重定向到登录界面;因为后续的权限检测都是通过这个上下文检测的 - 2. 这里的`SecurityContext`是线程安全的,也就是说不同用户访问的是不同的`SecurityContext`上下文,互不影响 -4. 保存到上下文后,就是session的相关操作,这里先获取session; - 1. 获取的同时会生成JSESSIONID,如果getSession(false)则不会生成JSESSIONID; - 2. 将上下文保存到session中 -5. 通过`context.getAuthentication()`验证是否认证通过 -6. 最后重定向到home页面; - -### 4. AuthenticationManager配置 - -这个刚开始自己用方法注入了一个,但是死活不成功,报内部错误,大概意思就是用户状态异常; - -后来查了下,才知道原来`WebSecurityConfigurerAdapter`配置接口本身就有获取`AuthenticationManager`的方法,直接覆写然后注入@Bean就好了,如下所示: - -```java - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManagerBean(); - } -``` - -### 5. 实践 - -启动程序,访问`http://localhost:8090/login`跳转到登录界面,输入`javalover/123456`发送表单请求到`manually-login`,认证通过重定向到`home`页面 - -![image-20211123105928146](https://i.loli.net/2021/11/23/6zuweELWQJdpNnk.png) - -![image-20211123110142791](https://i.loli.net/2021/11/23/OW3pBoHXcDGIAQZ.png) - -后台打印的认证信息如下:Authenticated=true就说明认证通过,还有对应的权限信息 - -```bash -是否认证通过:UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=javalover, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]] -``` - -## 总结 - -本篇主要通过手动认证的方式,熟悉了一下认证的过程: - -- 先构造一个认证对象,通过用户名/密码; -- 再通过认证管理器进行认证请求,成功后返回认证信息(包括用户名、密码(不可见)、权限等信息),失败时会抛出各种异常; -- 接下来将认证信息保存到上下文; -- 最后将上下文保存到session中进行管理; - - - -[源码地址]() diff --git a/demo-spring-security/demo-spring-security-remember-me/README.md b/demo-spring-security/demo-spring-security-remember-me/README.md deleted file mode 100644 index e3632d9..0000000 --- a/demo-spring-security/demo-spring-security-remember-me/README.md +++ /dev/null @@ -1,154 +0,0 @@ -## 前言 - -前面介绍了基于SpringSecurity的表单登录例子; - -本篇介绍怎么给表单登录添加一个**记住我**的功能; - -有了这个功能,那么在token失效后,系统会自动获取最新token,而不用重新登录; - -这里需要注意一点:这里的token并不是普通的token,而是JSESSIONID;这个JSESSIONID会在前端第一次请求后端时返回,以后这个JSESSION就是前后台通讯的凭证 - -## 目录 - -## 正文 - -### 1. 安全配置 - -这里我们做一个最简单的配置,如下所示:添加一个rememberMe()方法 - -```java -@Configuration -@EnableWebSecurity -@Slf4j -public class SecurityConfiguration extends WebSecurityConfigurerAdapter { - - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - // 数据没有持久化,只是保存在内存中 - auth.inMemoryAuthentication() - .withUser("javalover").password(passwordEncoder().encode("123456")).roles("USER") - .and() - .withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN"); - } - // 授权相关操作 - @Override - protected void configure(HttpSecurity http) throws Exception { - log.info("=== SecurityConfiguration.authorize ==="); - http - // 登出 所有用户都可以访问 - .logout().permitAll() - .deleteCookies("JSESSIONID") - .and() - .rememberMe() - // ...省略其他配置 - ; - } - - -} - -``` - -可以看到,我们要做的只是提供一个rememberMe()方法,前台就可以在token失效后自动获取最新的token,而不用再重新输入用户名密码进行登录操作; - -### 2. 前端组件 - -这里我们前端也尽可能简化,在之前的表单登录基础上,只增加一个复选框 rememberMe,代码如下: - -```html -
- - - - - - - - - - - -
记住我:
- -
-``` - -### 3. 实践-不勾选rememberMe - -下面我们就可以基于上面的代码,进行一个简单的测试:看一下 勾选rememberMe和不勾选的差别 - -> 完整代码见文末地址 - -第一步:启动程序,界面如下所示:javalover/123456 - -![image-20211122152245002](https://i.loli.net/2021/11/22/HJfNM4xW7p2z6PV.png) - -这里我们先不勾选**记住我**,点击**登录**,跳转到如下的主页: - -![image-20211122152555706](https://i.loli.net/2021/11/22/yMzTQc1YvmiuC5W.png) - -第二步:**接下来是重点**,这里我们删除本地cookie中的JSESSIONID,如下所示:F12->应用程序->cookies->JSESSION->右键-删除 - -![image-20211122152731372](https://i.loli.net/2021/11/22/UJi4FWH9qo7kv6V.png) - -第三步:刷新页面,可以看到,自动跳转到登录页面,因为token失效了,前后端通讯的凭证没了: - -![image-20211122152901019](https://i.loli.net/2021/11/22/JazEfBAciKCy7xk.png) - -> 其实上面我们也可以不删除cookie,等着session失效(默认30分钟,可以在application.yml中配置:server.servlet.session.timeout=60,默认单位秒) -> -> 后端的session都失效了,那session产生的JSESSIONID肯定也无效了; - -### 4. 实践-勾选rememberMe - -下面我们勾选**记住我**,重复上面的步骤,会发现在删除cookie中的JSESSIONID时,看到多了一个remember-me; - -![image-20211122153751334](https://i.loli.net/2021/11/22/x6aBAdEGWgvVT1t.png) - -其实这里我们可以换个角度来理解:虽然删了JSESSIONID,但是因为还有一个remember-me,所以前后端的通讯还是没有断开; - -所以此时我们刷新页面,还是停留在主页,不会跳转到登录界面; - -**但是如果我们把remember-me也一起删掉,那么结果很明显,还是会跳到登陆界面**。 - -### 5. 更多配置 - -**失效时间:** - -上面我们配置的remember-me,默认的token失效时间是两周,下面我们可以配置的短一点,比如一天: - -```java -.logout().permitAll() - .deleteCookies("JSESSIONID") - .and() - .rememberMe() - .tokenValiditySeconds(86400) - .and() -``` - -> 失效时间:严格意义上来说,上面这个失效时间 应该是remember-me的失效时间; - -这样的话,如果超过一天后,你再去删除JSESSIONID或者session失效,那么刷新页面还是会跳转到登录界面; - -**加密的密钥:** - -前面我们在调试界面看到的remember-me cookie值,它的值是由:MD5(用户名+过期时间+密码+密钥)合成的; - -这里的密钥我们可以自己配置,如下所示: - -```java -.rememberMe() - .key("privateKey") - .tokenValiditySeconds(86400) -``` - -## 总结 - -上面介绍了**rememberMe**的相关知识,了解了其实**rememberMe**就是:用新的通讯凭证`remember-me`来管理旧的通讯凭证`JSESSIONID`; - -当`JSESSIONID`被删除或者`session`过期时,如果`rememberMe cookie`还没过期(默认两周),那么系统就可以自动登录 - -`remember-me`真实的过期时间可以在调试界面看到,如下所示: - -![image-20211122162553634](https://i.loli.net/2021/11/22/bQH12ZA3k8dy4nT.png) - diff --git a/demo-spring-security/demo-spring-security-session/README.md b/demo-spring-security/demo-spring-security-session/README.md deleted file mode 100644 index f591834..0000000 --- a/demo-spring-security/demo-spring-security-session/README.md +++ /dev/null @@ -1,151 +0,0 @@ - - -## 简介 - -前面我们介绍了基于SpringSecurity的两种认证方式:[表单认证](https://juejin.cn/post/7030306851762176007)和[基本认证](https://juejin.cn/post/7031077013393768484); - -本篇我们介绍下认证后如何获取用户的基本信息; - -这里的核心就是`SecurityContextHolder`类。 - -## 目录 - -1. 从SecurityContextHolder中获取 -2. 从控制器中获取 -3. 从自定义接口获取 - -## 正文 - -### 1. 从SecurityContextHolder中获取 - -代码如下所示: - -```java -Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); -if (!(authentication instanceof AnonymousAuthenticationToken)) { - String currentUserName = authentication.getName(); - return currentUserName; -}else{ - return ""; -} -``` - -这里我们加了一个校验,当获取的认证用户存在时我们才去访问; - -这种方法最大的好处就是方便,直接通过静态方法就可以获取; - -但是缺点也很明显,其中最大的缺点就是不方便测试; - -### 2. 从控制器中获取 - -通过控制器来获取用户信息,就很灵活了; - -- 我们可以通过Principal参数获取:这里的Principal其实就是一个实体类,用来代表用户,可以是个人,也可以是公司 - -```java -@GetMapping("/userinfo-principal") -public String userinfoByPrincipal(Principal principal) { - return principal.getName(); -} -``` - -- 也可以通过Authentication参数获取: - -```java -@GetMapping("/userinfo-authentication") -public String userinfoByAuthentication(Authentication authentication) { - return authentication.getName(); -} -``` - -上面我们主要获取了用户名,用户的其他信息也是可以获取的; - -这里我们可以试着获取用户的权限:通过 UserDetails 类来获取: - -```java -@GetMapping("/userinfo-authentication") -public String userinfoByAuthentication(Authentication authentication) { - UserDetails userDetails = (UserDetails) authentication.getPrincipal(); - System.out.println("User has authorities: " + userDetails.getAuthorities()); - return authentication.getName(); -} -``` - -> 需要注意的是, Principal 是不能转换成UserDetails的,因为他俩之间没关系; -> -> 而这里的authentication.getPrincipal()返回的是一个Object对象,在请求接口时,Object传入的是一个UserDetails对象,所以获取时可以通过UserDetails强转; - -通过debug我们可以看到,在访问`/userinfo-authentication`时,getPrincipal返回的实际上就是一个User对象(User实现了UserDetails),所以转换是没问题的 - -![image-20211116172359093](https://i.loli.net/2021/11/16/xogBnKGP5kcCeIJ.png) - -- 还可以通过HttpServletRequest获取:这个其实本质还是通过 Principal 获取 - -```java -@GetMapping("/userinfo-request") -public String userinfoByRequest(HttpServletRequest request) { - Principal principal = request.getUserPrincipal(); - return principal.getName(); -} -``` - -### 3. 自定义接口获取 - -前面我们体验了从控制器获取,这种方式有点局限,因为如果想在其他类中获取,就无能为力了; - -其实更灵活的方式是自己定义一个Bean,然后在需要的地方注入来获取; - -这里其实是对第一种方式的升级,将 SecurityContextHolder包装到一个Bean中,然后在需要的地方进行注入即可; - -接口类:**IAuthenticationFacade** - -```java -public interface IAuthenticationFacade { - Authentication getAuthentication(); -} -``` - -实现类:**AuthenticationFacade** - -```java -@Component -public class AuthenticationFacade implements IAuthenticationFacade { - - @Override - public Authentication getAuthentication() { - return SecurityContextHolder.getContext().getAuthentication(); - } -} -``` - -这样一来,我们就可以在需要的地方通过@Autowired注入IAuthenticationFacade即可: - -```java -@RestController -public class UserController { - - @Autowired - IAuthenticationFacade authenticationFacade; - - @GetMapping("/userinfo-custom-interface") - public String userinfoByCustomInterface() { - Authentication authentication = authenticationFacade.getAuthentication(); - return authentication.getName(); - } - -} -``` - -可以看到,这次我们没有用到任何的参数,只通过自定义的Bean来获取,这样不仅充分利用了Spring的依赖注入功能,还使得获取信息变得更加灵活。 - -## 总结 - -上面介绍了三种获取方式: - -1. 直接通过 SecurityContextHolder 的静态方法获取 -2. 从控制器中获取:通过各种参数,比如Principal、Authentication、HttpServletRequest -3. 从自定义的Bean中获取:该方法是对方法1的升级,通过将 SecurityContextHolder包装到 Bean 中,使得获取信息更加灵活 - - - -[源码地址](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-userinfo) diff --git a/demo-spring-security/demo-spring-security-userinfo/README.md b/demo-spring-security/demo-spring-security-userinfo/README.md deleted file mode 100644 index f591834..0000000 --- a/demo-spring-security/demo-spring-security-userinfo/README.md +++ /dev/null @@ -1,151 +0,0 @@ - - -## 简介 - -前面我们介绍了基于SpringSecurity的两种认证方式:[表单认证](https://juejin.cn/post/7030306851762176007)和[基本认证](https://juejin.cn/post/7031077013393768484); - -本篇我们介绍下认证后如何获取用户的基本信息; - -这里的核心就是`SecurityContextHolder`类。 - -## 目录 - -1. 从SecurityContextHolder中获取 -2. 从控制器中获取 -3. 从自定义接口获取 - -## 正文 - -### 1. 从SecurityContextHolder中获取 - -代码如下所示: - -```java -Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); -if (!(authentication instanceof AnonymousAuthenticationToken)) { - String currentUserName = authentication.getName(); - return currentUserName; -}else{ - return ""; -} -``` - -这里我们加了一个校验,当获取的认证用户存在时我们才去访问; - -这种方法最大的好处就是方便,直接通过静态方法就可以获取; - -但是缺点也很明显,其中最大的缺点就是不方便测试; - -### 2. 从控制器中获取 - -通过控制器来获取用户信息,就很灵活了; - -- 我们可以通过Principal参数获取:这里的Principal其实就是一个实体类,用来代表用户,可以是个人,也可以是公司 - -```java -@GetMapping("/userinfo-principal") -public String userinfoByPrincipal(Principal principal) { - return principal.getName(); -} -``` - -- 也可以通过Authentication参数获取: - -```java -@GetMapping("/userinfo-authentication") -public String userinfoByAuthentication(Authentication authentication) { - return authentication.getName(); -} -``` - -上面我们主要获取了用户名,用户的其他信息也是可以获取的; - -这里我们可以试着获取用户的权限:通过 UserDetails 类来获取: - -```java -@GetMapping("/userinfo-authentication") -public String userinfoByAuthentication(Authentication authentication) { - UserDetails userDetails = (UserDetails) authentication.getPrincipal(); - System.out.println("User has authorities: " + userDetails.getAuthorities()); - return authentication.getName(); -} -``` - -> 需要注意的是, Principal 是不能转换成UserDetails的,因为他俩之间没关系; -> -> 而这里的authentication.getPrincipal()返回的是一个Object对象,在请求接口时,Object传入的是一个UserDetails对象,所以获取时可以通过UserDetails强转; - -通过debug我们可以看到,在访问`/userinfo-authentication`时,getPrincipal返回的实际上就是一个User对象(User实现了UserDetails),所以转换是没问题的 - -![image-20211116172359093](https://i.loli.net/2021/11/16/xogBnKGP5kcCeIJ.png) - -- 还可以通过HttpServletRequest获取:这个其实本质还是通过 Principal 获取 - -```java -@GetMapping("/userinfo-request") -public String userinfoByRequest(HttpServletRequest request) { - Principal principal = request.getUserPrincipal(); - return principal.getName(); -} -``` - -### 3. 自定义接口获取 - -前面我们体验了从控制器获取,这种方式有点局限,因为如果想在其他类中获取,就无能为力了; - -其实更灵活的方式是自己定义一个Bean,然后在需要的地方注入来获取; - -这里其实是对第一种方式的升级,将 SecurityContextHolder包装到一个Bean中,然后在需要的地方进行注入即可; - -接口类:**IAuthenticationFacade** - -```java -public interface IAuthenticationFacade { - Authentication getAuthentication(); -} -``` - -实现类:**AuthenticationFacade** - -```java -@Component -public class AuthenticationFacade implements IAuthenticationFacade { - - @Override - public Authentication getAuthentication() { - return SecurityContextHolder.getContext().getAuthentication(); - } -} -``` - -这样一来,我们就可以在需要的地方通过@Autowired注入IAuthenticationFacade即可: - -```java -@RestController -public class UserController { - - @Autowired - IAuthenticationFacade authenticationFacade; - - @GetMapping("/userinfo-custom-interface") - public String userinfoByCustomInterface() { - Authentication authentication = authenticationFacade.getAuthentication(); - return authentication.getName(); - } - -} -``` - -可以看到,这次我们没有用到任何的参数,只通过自定义的Bean来获取,这样不仅充分利用了Spring的依赖注入功能,还使得获取信息变得更加灵活。 - -## 总结 - -上面介绍了三种获取方式: - -1. 直接通过 SecurityContextHolder 的静态方法获取 -2. 从控制器中获取:通过各种参数,比如Principal、Authentication、HttpServletRequest -3. 从自定义的Bean中获取:该方法是对方法1的升级,通过将 SecurityContextHolder包装到 Bean 中,使得获取信息更加灵活 - - - -[源码地址](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-userinfo) diff --git a/demo-swagger3/README.md b/demo-swagger3/README.md deleted file mode 100644 index fefb0ed..0000000 --- a/demo-swagger3/README.md +++ /dev/null @@ -1,259 +0,0 @@ -## 目录 - -- 前言:什么是Swagger -- 起步:(只需简单的3步) - - 加载依赖 - - 添加注解@EnableOpenApi - - 启动SpringBoot,访问Swagger后台界面 -- 配置:基于Java的配置 -- 注解:Swagger2 和 Swagger3做对比 -- 源码: -- 问题:踩坑记录(后面再整理) - -## 前言 - -**什么是Swagger:** - -​ Swagger 是最流行的 API 开发工具,它遵循 OpenAPI Specification(OpenAPI 规范,也简称 OAS)。 - -​ 它最方便的地方就在于,API文档可以和服务端保持同步,即服务端更新一个接口,前端的API文档就可以实时更新,而且可以在线测试。 - -​ 这样一来,Swagger就大大降低了前后端的沟通障碍,不用因为一个接口调不通而争论不休 - -> 之前用的看云文档,不过这种第三方的都需要手动维护,还是不太方便 - -## 起步 - -1. 加载依赖 - -```xml - - io.springfox - springfox-boot-starter - 3.0.0 - -``` - -2. 添加@EnableOpenApi注解 - -```java -@EnableOpenApi -@SpringBootApplication -public class SwaggerApplication { - public static void main(String[] args) { - SpringApplication.run(SwaggerApplication.class, args); - } -} -``` - -3. 启动项目,访问"http://localhost:8080/swagger-ui/index.html" - -![image-20210729112424407](D:\StudyData\github-project\tangyuanxueJava\【文章】\【SpringBoot】\知识点\后台接口文档管理Swagger3\主页.png) - -这样一个简单的Swagger后台接口文档就搭建完成了; - -下面我们说下配置和注解 - -## 配置 - -可以看到,上面那个界面中,默认显示了一个`basic-error-controller`接口分组,但是我们并没有写; - -通过在项目中查找我们发现,SpringBoot内部确实有这样一个控制器类,如下所示: - -![image-20210729113119350](D:\StudyData\github-project\tangyuanxueJava\【文章】\【SpringBoot】\知识点\后台接口文档管理Swagger3\BasicErrorController.png) - -这说明Swagger默认的配置,会自动把@Controller控制器类添加到接口文档中 - -下面我们就自己配置一下,如下所示: - -```java -import io.swagger.annotations.ApiOperation; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import springfox.documentation.builders.ApiInfoBuilder; -import springfox.documentation.builders.PathSelectors; -import springfox.documentation.builders.RequestHandlerSelectors; -import springfox.documentation.oas.annotations.EnableOpenApi; -import springfox.documentation.service.ApiInfo; -import springfox.documentation.service.Contact; -import springfox.documentation.spi.DocumentationType; -import springfox.documentation.spring.web.plugins.Docket; - -@Configuration -public class SwaggerConfig { - - @Bean - public Docket createRestApi() { - // 配置OAS 3.0协议 - return new Docket(DocumentationType.OAS_30) - .apiInfo(apiInfo()) - .select() - // 查找有@Tag注解的类,并生成一个对应的分组;类下面的所有http请求方法,都会生成对应的API接口 - // 通过这个配置,就可以将那些没有添加@Tag注解的控制器类排除掉 - .apis(RequestHandlerSelectors.withClassAnnotation(Tag.class)) - .paths(PathSelectors.any()) - .build(); - } - - private ApiInfo apiInfo() { - return new ApiInfoBuilder() - .title("GPS Doc") - .description("GPS Doc文档") - .termsOfServiceUrl("http://www.javalover.com") - .contact(new Contact("javalover", "http://www.javalover.cn", "1121263265@qq.com")) - .version("2.0.0") - .build(); - } - -} -``` - -这样上面那个`basic-error-controller`就看不见了 - -## 注解 - -我们先看下Swagger2中的注解,如下所示: - -- @Api:用在控制器类上,表示对类的说明 - - tags="说明该类的作用,可以在UI界面上看到的说明信息的一个好用注解" - - value="该参数没什么意义,在UI界面上也看到,所以不需要配置" - -- @ApiOperation:用在请求的方法上,说明方法的用途、作用 - - value="说明方法的用途、作用" - - notes="方法的备注说明" - -- @ApiImplicitParams:用在请求的方法上,表示一组参数说明 - - @ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面(标注一个指定的参数,详细概括参数的各个方面,例如:参数名是什么?参数意义,是否必填等) - - name:属性值为方法参数名 - - value:参数意义的汉字说明、解释 - - required:参数是否必须传 - - paramType:参数放在哪个地方 - - header --> 请求参数的获取:@RequestHeader - - query --> 请求参数的获取:@RequestParam - - path(用于restful接口)--> 请求参数的获取:@PathVariable - - dataType:参数类型,默认String,其它值dataType="Integer" - - defaultValue:参数的默认值 - -- @ApiResponses:用在请求的方法上,表示一组响应 - - @ApiResponse:用在@ApiResponses中,一般用于表达一个错误的响应信息 - - code:状态码数字,例如400 - - message:信息,例如"请求参数没填好" - - response:抛出异常的类 - -- @ApiModel:用于响应类上(POJO实体类),描述一个返回响应数据的信息(描述POJO类请求或响应的实体说明) - (这种一般用在post接口的时候,使用@RequestBody接收JSON格式的数据的场景,请求参数无法使用@ApiImplicitParam注解进行描述的时候) - - @ApiModelProperty:用在POJO属性上,描述响应类的属性说明 -- @ApiIgnore:使用该注解忽略这个某个API或者参数; - -上面这些是Swagger2的注解,下面我们看下Swagger3和它的简单对比 - -![Swagger3注解](https://i.loli.net/2021/07/29/s62vJN5XLKdugER.png) - -接下来我们就用Swagger3的注解来写一个接口看下效果(其中穿插了Swagger2的注解) - -- 控制器UserController.java - -```java -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiImplicitParam; -import io.swagger.annotations.ApiImplicitParams; -import io.swagger.annotations.ApiOperation; -import io.swagger.v3.oas.annotations.Hidden; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.Parameters; -import io.swagger.v3.oas.annotations.enums.ParameterIn; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.web.bind.annotation.*; -import springfox.documentation.annotations.ApiIgnore; - -@Tag(name = "user-controller", description = "用户接口") -@RestController -public class UserController { - - // 忽略这个api - @Operation(hidden = true) - @GetMapping("/hello") - public String hello(){ - return "hello"; - } - - @Operation(summary = "用户接口 - 获取用户详情") - @GetMapping("/user/detail") - // 这里的@Parameter也可以不加,Swagger会自动识别到这个name参数 - // 但是加@Parameter注解可以增加一些描述等有用的信息 - public User getUser(@Parameter(in = ParameterIn.QUERY, name = "name", description = "用户名") String name){ - User user = new User(); - user.setUsername(name); - user.setPassword("123"); - return user; - } - - @Operation(summary = "用户接口 - 添加用户") - @PostMapping("/user/add") - // 这里的user会被Swagger自动识别 - public User addUser(@RequestBody User user){ - System.out.println("添加用户"); - return user; - } - -} - -``` - -实体类User.java: - -```java - -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Schema -@Data -public class User { - - @Schema(name = "username", description = "用户名", example = "javalover") - private String username; - - @Schema(name = "password", description = "密码", example = "123456") - private String password; - - // 隐藏这个属性,这样接口文档的请求参数中就看不到这个属性 - @Schema(hidden = true) - private String email; - -} - -``` - -启动后运行界面如下: - -- 首页展示: - -![image-20210729132629924](D:\StudyData\github-project\tangyuanxueJava\【文章】\【SpringBoot】\知识点\后台接口文档管理Swagger3\首页展示.png) - -- /user/add接口展示: - -![image-20210729132730799](D:\StudyData\github-project\tangyuanxueJava\【文章】\【SpringBoot】\知识点\后台接口文档管理Swagger3\user-add接口.png) - -- /user/detail接口展示 - - ![image-20210729132849933](D:\StudyData\github-project\tangyuanxueJava\【文章】\【SpringBoot】\知识点\后台接口文档管理Swagger3\user-detail接口.png) - -## 问题 - -目前只是简单地体验了下,其实里面还是有很多坑,等后面有空再整理解决,下面列举几个: - -- @Paramters参数无效 -- @ApiImplicitParamter的body属性无效 -- @Tag的name属性:如果name属性不是当前类名的小写连字符格式,则会被识别为一个单独的接口分组 -- 等等 - - - -**最近整理了一份面试资料《Java面试题-校招版》附答案,无密码无水印,感兴趣的可以关注公众号回复“面试”领取。** - diff --git a/demo-task/README.md b/demo-task/README.md deleted file mode 100644 index 829e262..0000000 --- a/demo-task/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# 定时任务:Spring自带的TaskScheduler接口 - -## 简介 - -Spring自带的`TaskScheduler`主要用来执行一些定时任务,比如每天的23点执行一次任务(固定时间点)、每隔10分钟执行一次任务等(固定频率) - -## 示例 - -- 首先写一个定时任务 - - `MyTask.java` - - ```java - @Component - public class MyTask { - - @Scheduled(cron = "*/5 * * * * *") - public void task1(){ - System.out.println("this is task1"); - } - } - - ``` - -- 然后在主程序中添加注解`@EnableScheduling` - - ```java - @SpringBootApplication - @EnableScheduling - public class TaskApplication { - public static void main(String[] args) { - SpringApplication.run(TaskApplication.class, args); - } - } - - ``` - -- 最后启动程序,就可以看到控制台的任务执行情况,每隔5s打印一次 - -## 知识点 - -- 注解`@Scheduled`:设置定时任务,支持cron表达式、fixedRate固定频率触发等; -- 注解`@EnableScheduling `:开启定时任务,可以加在主类或者配置类中 -- 配置线程池大小`spring.task.scheduling.pool.size=20`:默认是1 - -## 参考 - -- [@Scheduled](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling-annotation-support-scheduled)注解 - -- [cron表达式](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling-cron-expression) - -- [@EnableScheduling](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling-annotation-support)注解 \ No newline at end of file diff --git a/demo-upload/README.md b/demo-upload/README.md deleted file mode 100644 index 221a187..0000000 --- a/demo-upload/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# 上传文件 - -## 简介 - -前端用vue写个简单界面,用来选择文件,进行上传 - -后端用Spring Boot写个接口,用来接收文件 - -> PS:注意跨域问题 - -## 示例 - -#### 前端: - -**文件上传页面** `Home.vue` - -```vue - - - +``` + +下面我们启动vue`yarn run serve`,访问主界面`http://localhost:8080/`,点击主页:可以看到打印了 success + +![image-20211112175126444](https://i.loli.net/2021/11/12/JjARF12CPwNdXHY.png) + + + +同理,如果我们把HelloWorld.vue中的用户名/密码配置拿掉,那么就会打印fail,报401错误: + +![image-20211112175337702](https://i.loli.net/2021/11/12/CO8Qm5NAVtP2bwI.png) + +## 总结 + +SpringSecurity的基本认证方式跟表单认证方式,后端的代码其实差不多,就是配置的地方不一样; + +这俩的核心都是通过用户名/密码的方式进行认证,只是适用的场景不同: + +- Form Login,表单登录认证,适用于单体应用 + +- Basic Authentication,基本的http认证,适用于前后端分离的应用 + + + +源码地址:[demo-spring-security-basic-auth](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-basic-auth) diff --git a/demo-spring-security/demo-spring-security-basic-auth/web/basic-auth/README.md b/demo-spring-security/demo-spring-security-basic-auth/web/basic-auth/README.md new file mode 100644 index 0000000..1b3ed18 --- /dev/null +++ b/demo-spring-security/demo-spring-security-basic-auth/web/basic-auth/README.md @@ -0,0 +1,24 @@ +# basic-auth + +## Project setup +``` +yarn install +``` + +### Compiles and hot-reloads for development +``` +yarn serve +``` + +### Compiles and minifies for production +``` +yarn build +``` + +### Lints and fixes files +``` +yarn lint +``` + +### Customize configuration +See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/demo-spring-security/demo-spring-security-block-brute-force-auth/README.md b/demo-spring-security/demo-spring-security-block-brute-force-auth/README.md new file mode 100644 index 0000000..e69de29 diff --git a/demo-spring-security/demo-spring-security-jpa/README.md b/demo-spring-security/demo-spring-security-jpa/README.md new file mode 100644 index 0000000..aadbba1 --- /dev/null +++ b/demo-spring-security/demo-spring-security-jpa/README.md @@ -0,0 +1,3 @@ +## 简介 +基于JPA的SpringSecurity +这一版跟之前的唯一不同就是,使用了JPA,将数据持久化了 diff --git a/demo-spring-security/demo-spring-security-login-form/README.md b/demo-spring-security/demo-spring-security-login-form/README.md new file mode 100644 index 0000000..b280551 --- /dev/null +++ b/demo-spring-security/demo-spring-security-login-form/README.md @@ -0,0 +1,314 @@ + + +## 简介 + +SpringSecurity的认证机制有多种,比如基于用户名/密码的认证,基于OAuth2.0的认证(OAuth已废弃)。。。 + +而基于用户名/密码的认证方式,又分多种,比如: + +- Form Login,表单登录认证(单体应用,比如SpringMVC) +- Basic Authentication,基本的http认证(前后端分离应用) +- 【已废弃】Digest Authentication,数字认证(已废弃,不再使用这种认证方式,因为它的加密方式不安全,比如md5加密等;现在比较安全的加密方式有BCrypt等) + +本节介绍的就是第一种:**表单登录的认证方式** + +## 目录 + +1. maven配置 +2. security配置 +3. controller控制器 +4. web界面 +5. 启动运行 + +## 正文 + +在开始之前,需要先了解两个词 + +- Authenticate认证:就是通过用户名/密码等方式,登入到系统,这个过程就是认证;类似于进入景区的大门 +- Authorize授权:就是登入到系统之后,校验用户是否有权限操作某个模块,这个过程就是授权;类似于进入景区后,各个收费区域,只有交了钱(有权限),才能进入指定区域; + +项目背景:Spring Boot + SpringMVC + Thymeleaf + +项目结构如下: + +![image-20210603141803128](https://i.loli.net/2021/06/03/FSsi8QHcnlxAyf3.png) + +#### 1.maven配置: + +```xml + + + + spring-boot-demo + com.jalon + 0.0.1-SNAPSHOT + + 4.0.0 + demo-spring-security-login-form + + 1.8 + 2.4.3 + 1.18.16 + 1.8 + 1.8 + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + provided + + + +``` + +#### 2.security配置 + +这里面主要包含两部分: + +- authenticate 认证配置:主要配置用户名,密码,角色(这里基于内存来保存,为了简化) +- authorize 授权配置:主要配置各个角色的权限,即可以访问哪些页面 + +```java +@Configuration +@EnableWebSecurity +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + // 认证相关操作 + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + // 数据没有持久化,只是保存在内存中 + auth.inMemoryAuthentication() + .withUser("javalover").password(passwordEncoder().encode("123456")).roles("USER") + .and() + .withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN"); + } + + // 授权相关操作 + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + // admin页面,只有admin角色可以访问 + .antMatchers("/admin").hasRole("ADMIN") + // home 页面,ADMIN 和 USER 都可以访问 + .antMatchers("/home").hasAnyRole("USER", "ADMIN") + // login 页面,所有用户都可以访问 + .antMatchers("/login").permitAll() + .anyRequest().authenticated() + .and() + // 自定义登录表单 + .formLogin().loginPage("/login") + // 登录成功跳转的页面,第二个参数true表示每次登录成功都是跳转到home,如果false则表示跳转到登录之前访问的页面 + .defaultSuccessUrl("/home", true) + // 失败跳转的页面(比如用户名/密码错误),这里还是跳转到login页面,只是给出错误提示 + .failureUrl("/login?error=true") + .and() + .logout().permitAll() + .and() + // 权限不足时跳转的页面,即访问一个页面时没有对应的权限,会跳转到这个页面 + .exceptionHandling().accessDeniedPage("/accessDenied"); + } + + // 定义一个密码加密器,这个BCrypt也是Spring默认的加密器 + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} +``` + +#### 3. controller控制器 + +控制器主要任务就是处理请求,下面就是典型的MVC模式 + +```java +@Controller +@Slf4j +public class SecurityController { + + @RequestMapping("/login") + public String login(){ + log.info("=== login ==="); + return "login"; + } + + @RequestMapping("/home") + public String home(Model model){ + model.addAttribute("user", getUsername()); + model.addAttribute("role", getAuthority()); + return "home"; + } + + @RequestMapping("/admin") + public String admin(Model model){ + model.addAttribute("user", getUsername()); + model.addAttribute("role", getAuthority()); + return "admin"; + } + + // 权限不足 + @RequestMapping("/accessDenied") + public String accessDenied(Model model){ + model.addAttribute("user", getUsername()); + model.addAttribute("role", getAuthority()); + return "access_denied"; + } + + // 获取当前登录的用户名 + private String getUsername(){ + return SecurityContextHolder.getContext().getAuthentication().getName(); + } + + // 获取当前登录的用户角色:因为有可能一个用户有多个角色,所以需遍历 + private String getAuthority(){ + Collection authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities(); + ArrayList list = new ArrayList<>(); + for(GrantedAuthority authority: authorities){ + list.add(authority.getAuthority()); + } + log.info("=== authority:" + list); + return list.toString(); + } +} +``` + +#### 4. web界面 + +界面有4个: + +- login.html: 登录界面,所有人都可以访问 +- home.html: 主页面,普通用户和管理员可以访问 +- admin.html: 管理员页面,只有管理员可以访问 +- access_denied.html: 访问被拒绝页面,权限不足时会跳转到该页面;比如普通用户访问admin.html时 + +login.html + +```html + + + + + Spring Security + + + +
+ Invalid username and password. +
+
+ You have been logged out +
+
+ + + +
+ + +``` + + + +home.html + +```html + + + + + Spring Security Home + + + + 欢迎 + 你的权限是 + admin页面 + 退出 + + +``` + +admin.html + +```html + + + + + Spring Security Admin + + + + 欢迎 + 你的权限是 + 退出 + + +``` + +access_denied.html + +```html + + + + + Access Denied + + + + 没有权限访问页面 + 你的权限是 + 退出 + + +``` + +#### 5. 启动运行 + +访问 http://localhost:8088,会自动跳转到login界面,如下: + +![image-20210603143352317](https://i.loli.net/2021/06/03/1ryvFKGR4wTZ8WP.png) + +这里先用普通用户的身份来登录,javalover/123456,登陆后进入主页:可以看到,权限是普通用户 + +![image-20210603143453932](https://i.loli.net/2021/06/03/K24JhxYNAzC1o6L.png) + +这时点击`admin页面`就会提示权限不足,如下: + +![image-20210603143531292](https://i.loli.net/2021/06/03/3wEOuIRl2iQb8se.png) + +此时点击退出,又重新回到登录界面:并附有提示【已退出登录】 + +![image-20210603143631810](https://i.loli.net/2021/06/03/8HwOQKTMyz5hXiI.png) + +最后用管理账户登录,admin/123456,登录进入主页:可以看到,权限是管理员 + +![image-20210603143901708](https://i.loli.net/2021/06/03/Kvy1TUJPqn5OErc.png) + +这时点击`admin页面`,就会正常显示: + +![image-20210603144003914](https://i.loli.net/2021/06/03/acVsUEmMuFPpGAy.png) + +## 总结 + +SpringSecurity的表单登录认证,总的来说代码不是很多,因为很多功能SpringSecurity都是自带的(比如登录、登出、权限不足等),我们只需要根据自己的需求来修改一些配置就可以了 + + + +源码地址:[**demo-spring-security-login-form**](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-login-form) diff --git a/demo-spring-security/demo-spring-security-login-redirect/README.md b/demo-spring-security/demo-spring-security-login-redirect/README.md new file mode 100644 index 0000000..3fc9a8e --- /dev/null +++ b/demo-spring-security/demo-spring-security-login-redirect/README.md @@ -0,0 +1,250 @@ +## 简介 + +通常一个后台管理系统,会包含不同的角色和权限; + +然后不同的角色,登录后会根据权限的不同,跳转到不同的界面; + +这里我们还是设定有两个角色:普通用户和管理员; + +- 普通用户:登录成功跳转到 home 页面; +- 管理员:登录成功跳转到 admin 页面; + +## 目录 + +- 基本配置 +- 配置用户和角色 +- 创建处理器类 +- 配置处理器 +- 修改控制器 +- 运行 + +## 正文 + +### 1. 基本配置 + +先配置一个默认的登录成功界面,如下所示: + +```java +@Configuration +@EnableWebSecurity +@Slf4j +public class SecSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .formLogin() + .loginPage("/login") + // 登录成功跳转的页面,第二个参数true表示每次登录成功都是跳转到home,如果false则表示跳转到登录之前访问的页面 + .defaultSuccessUrl("/home.html", true) + // ... 其他配置 + + } +} +``` + +这样当用户登录成功后,会默认跳转到home.html界面; + +但是本节我们要做的就是修改这个地方,使得不同角色跳转不到不同的界面; + +下面开始进入主体 + +### 2. 配置用户和角色 + +这里我们用 全局配置: + +```java + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + // 数据没有持久化,只是保存在内存中 + auth.inMemoryAuthentication() + .withUser("javalover").password(passwordEncoder().encode("123456")).roles("USER") + .and() + .withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN"); + } +``` + +这里配置了两个用户: + +- 普通用户:javalover +- 管理员:admin + +### 3. 创建处理器类 + +第一步中的配置是:两个角色登录后,默认都是跳转到`home.html`界面; + +接下来就开始修改,使他们跳转到不同的界面; + +先定义一个处理器类,实现了`AuthenticationSuccessHandler`接口: + +```java +public class MySimpleUrlAuthenticationSuccessHandler + implements AuthenticationSuccessHandler { + + + private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, Authentication authentication) + throws IOException { + + String targetUrl = determineTargetUrl(authentication); + + if (response.isCommitted()) { + logger.debug( + "Response has already been committed. Unable to redirect to " + + targetUrl); + return; + } + + redirectStrategy.sendRedirect(request, response, targetUrl); + } + +} + +``` + +覆写的`onAuthenticationSuccess`方法:登录成功会先到这个地方,然后我们就可以在这里控制下一步要跳转的界面(当然其他的一些操作也可以); + + + +`determineTargetUrl`方法:就是这篇文章的核心; + +它会根据不同的权限,获取到不同的跳转url,然后重定向; + +```java +protected String determineTargetUrl(final Authentication authentication) { + + Map roleTargetUrlMap = new HashMap<>(); + roleTargetUrlMap.put("ROLE_USER", "/home"); + roleTargetUrlMap.put("ROLE_ADMIN", "/admin"); + + final Collection authorities = authentication.getAuthorities(); + for (final GrantedAuthority grantedAuthority : authorities) { + String authorityName = grantedAuthority.getAuthority(); + if(roleTargetUrlMap.containsKey(authorityName)) { + return roleTargetUrlMap.get(authorityName); + } + } + + throw new IllegalStateException(); +} +``` + +### 4. 配置处理器 + +上面我们定义了一个处理器,用来控制登录成功后的跳转界面; + +这里我们将其配置到config类中; + +先在配置类中注入一个Bean:`AuthenticationSuccessHandler `,返回的是刚才创建的处理器类 + +```java +@Bean +public AuthenticationSuccessHandler myAuthenticationSuccessHandler(){ + return new MySimpleUrlAuthenticationSuccessHandler(); +} +``` + +然后替换掉开头设置的跳转url参数,如下所示: + +```java +@Override +protected void configure(final HttpSecurity http) throws Exception { + http + .authorizeRequests() + .formLogin() + .loginPage("/login") + .successHandler(myAuthenticationSuccessHandler()) + // ...其他配置 +} +``` + +### 5. 修改控制器 + +上面我们跳转home和admin,是通过控制器进行跳转的,下面我们配置一下控制器: + +```java +@Controller +@Slf4j +public class SecurityController { + + @RequestMapping("/login") + public String login(){ + log.info("=== login ==="); + return "login"; + } + + @RequestMapping("/home") + public String home(Model model){ + model.addAttribute("user", getUsername()); + model.addAttribute("role", getAuthority()); + return "home"; + } + + @RequestMapping("/admin") + public String admin(Model model){ + model.addAttribute("user", getUsername()); + model.addAttribute("role", getAuthority()); + return "admin"; + } + + @RequestMapping("/accessDenied") + public String accessDenied(Model model){ + model.addAttribute("user", getUsername()); + model.addAttribute("role", getAuthority()); + return "access_denied"; + } + + // 获取当前登录的用户名 + private String getUsername(){ + return SecurityContextHolder.getContext().getAuthentication().getName(); + } + + // 获取当前登录的用户角色 + private String getAuthority(){ + Collection authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities(); + ArrayList list = new ArrayList<>(); + for(GrantedAuthority authority: authorities){ + list.add(authority.getAuthority()); + } + log.info("=== authority:" + list); + return list.toString(); + } +} +``` + +### 6. 运行 + +接下来我们就可以启动程序,访问`http://localhost:8090/`进行测试了 + +> 前端代码就不贴了,就是三个界面:login,.html, home.html, admin.html。完整源码见文末 + +- 普通用户登录: + +![image-20211119123413681](https://i.loli.net/2021/11/19/Zt6Ds9gBxhOqH2A.png) + +跳转到home + +![image-20211119123425793](https://i.loli.net/2021/11/19/GDZEaugkNMBIK97.png) + +- 管理员登录: + +![image-20211119123533315](https://i.loli.net/2021/11/19/jWm1UARyIxfNpuL.png) + +跳转到admin + +![image-20211119123541475](https://i.loli.net/2021/11/19/AJGNECQVonU6mse.png) + +## + +## 总结 + +重定向的核心就是那个处理器中的`determineTargetUrl`方法,根据角色的不同,跳转到不同的界面; + + + +[源码地址](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-login-redirect) + diff --git a/demo-spring-security/demo-spring-security-logout/README.md b/demo-spring-security/demo-spring-security-logout/README.md new file mode 100644 index 0000000..7948f8d --- /dev/null +++ b/demo-spring-security/demo-spring-security-logout/README.md @@ -0,0 +1,168 @@ + + +## 简介 + +前面我们介绍了[表单登录的入门案例](https://juejin.cn/post/7030306851762176007); + +本篇介绍下**登出**的入门案例,代码基于表单登录的案例进行演示; + +代码地址见文末 + +## 目录 + + + +## 正文 + +### 1. 基本配置 + +最基本的登出配置如下所示: + +```java + @Override + protected void configure(HttpSecurity http) throws Exception { + http + // 登出 所有用户都可以访问 + .logout().permitAll(); + } +``` + +这里默认的登出url为`/logout`,通过在url中访问`http://localhost:8090/logout`就可以登出了。 + +当然最方便的还是在界面中进行链接跳转,如下所示: + +```html + 退出 +``` + +### 2. 登出跳转 + +**logoutSuccessUrl配置**: + +登出跳转成功后的默认界面是根路径,比如`http://localhost:8090/`; + +下面我们可以进行简单的配置,配置成自己指定的界面,如下所示:一般推荐将登出成功后跳转的链接设置为登录界面(习惯) + +```java + @Override + protected void configure(HttpSecurity http) throws Exception { + http + // 登出 所有用户都可以访问 + .logout().permitAll() + .logoutSuccessUrl("/login"); + } +``` + +**logoutUrl配置:** + +登出跳转的默认url为`/logout`,比如`http://localhost:8090/logout`,如果登出成功,就跳转到上面配置的路径; + +配置如下所示: + +```java + @Override + protected void configure(HttpSecurity http) throws Exception { + http + // 登出 所有用户都可以访问 + .logout().permitAll() + .logoutUrl("/logout"); + } +``` + +### 3. 更新缓存 + +这里的缓存指的就是session和cookie; + +在登出之后,需要将session失效处理,并删除对应的cookie; + +对应的命令为:`invalidateHttpSession()` 和 `deleteCookies(...name)`; + +配置如下所示: + +> 其中删除的Cookies名称为`JSESSIONID`,这个就是前后端交互的一个凭证id,是在第一次前端请求后端时,后端返回的id;后续的请求后端会根据JSESSIONID来匹配对应的session + +```java + @Override + protected void configure(HttpSecurity http) throws Exception { + http + // 登出 所有用户都可以访问 + .logout().permitAll() + .logoutUrl("/logout") + .invalidateHttpSession(true) + .deleteCookies("JSESSIONID"); + } +``` + +### 4. 登出处理器 + +登出成功后,不仅可以设置特定的url,还可以执行一些自定义的操作; + +对应的命令为:`logoutSuccessHandler` + +比如我们需要记录登出时访问的最后一个界面,那么可以通过如下的代码来实现; + +先定义一个处理器:`CustomLogoutSuccessHandler.java` + +```java +public class CustomLogoutSuccessHandler extends + SimpleUrlLogoutSuccessHandler implements LogoutSuccessHandler { + + @Override + public void onLogoutSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) + throws IOException, ServletException { + + String refererUrl = request.getHeader("Referer"); + System.out.println("Logout from: " + refererUrl); + + super.onLogoutSuccess(request, response, authentication); + } +} +``` + +然后在配置中注入该处理器,通过方法注入,如下所示: + +```java +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + @Bean + public LogoutSuccessHandler logoutSuccessHandler(){ + return new CustomLogoutSuccessHandler(); + } + @Override + protected void configure(HttpSecurity http) throws Exception { + http + // 登出 所有用户都可以访问 + .logout() + .permitAll() + .logoutSuccessUrl("/login") + .logoutUrl("/logout") + .logoutSuccessHandler(logoutSuccessHandler()); + } + +} + +``` + +这样我们在登出时,就可以看到控制台打印下面的内容: + +```bash +Logout from: http://localhost:8090/home +``` + +## 总结 + +本篇介绍了登出的相关配置和处理; + +配置有: + +- logoutUrl(): 登出链接配置 +- logoutSuccessUrl(): 登出成功后的跳转链接 +- invalidateHttpSession: 失效session +- deleteCookies() : 删除对应cookie,多个cookieName逗号分隔 +- LogoutSuccessHandler:登出后执行的自定义操作 + +[源码地址](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-logout) + diff --git a/demo-spring-security/demo-spring-security-manually-login/README.md b/demo-spring-security/demo-spring-security-manually-login/README.md new file mode 100644 index 0000000..59435af --- /dev/null +++ b/demo-spring-security/demo-spring-security-manually-login/README.md @@ -0,0 +1,177 @@ + + +## 简介 + +前面我们的SpringSecurity文章介绍的登录认证,不管是`form-login-auth`表单登录认证,还是`basic-auth`基本方式的认证,都是通过SpringSecurity系统自动进行的认证,我们并没用人工干预; + +比如表单登录认证,我们在表单中提交`login`的表单登录请求后,后台并没有自己写对应的处理器,而是由系统自动认证的; + +相应的,基本方式的认证也是直接把用户名密码写入header中,系统自动认证; + +那今天我们就来手动进行认证,以此熟悉下认证的过程 + +## 目录 + +## 正文 + +### 1. 安全配置 + +一如既往,还是简单配置一个用户,一个登录请求; + +不过这次的路径匹配多了一个自定义的manually-login:`.antMatchers("/manually-login").permitAll()`; + +这个就是我们在提交表单时要请求的路径,如果不开放,请求后会再次跳转到登录界面(因为没有权限); + +```java +@Configuration +@EnableWebSecurity +@Slf4j +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + // 认证相关操作 + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + log.info("=== SecurityConfiguration.authenticate ==="); + // 数据没有持久化,只是保存在内存中 + auth.inMemoryAuthentication() + .withUser("javalover").password(passwordEncoder().encode("123456")).roles("USER"); + } + + // 授权相关操作 + @Override + protected void configure(HttpSecurity http) throws Exception { + log.info("=== SecurityConfiguration.authorize ==="); + http + // home 页面,ADMIN 和 USER 都可以访问 + .antMatchers("/home").hasAnyRole("USER", "ADMIN") + // login 页面,所有用户都可以访问 + .antMatchers("/manually-login").permitAll() + .antMatchers("/login").permitAll() + .anyRequest().authenticated() + .and() + // 自定义登录表单 + .formLogin().loginPage("/login") + // 登录成功跳转的页面,第二个参数true表示每次登录成功都是跳转到home,如果false则表示跳转到登录之前访问的页面 + .defaultSuccessUrl("/home", true) + // 失败跳转的页面(比如用户名/密码错误),这里还是跳转到login页面,只是给出错误提示 + .failureUrl("/login?error=true") + .and() + // 登出 所有用户都可以访问 + .logout().permitAll() + } + + // 定义一个密码加密器,这个BCrypt也是Spring默认的加密器 + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} + +``` + +### 2. 表单组件 + +登录表单如下所示:这里我们跳转的路径改为了`manually-login`,之前是`login`;这样登录请求时,请求登录认证会由我们自己进行处理; + +```html +
+ + + +
+``` + +### 3. 控制器 + +这个控制器里有一个`manuallyLogin`就是负责接收上面的登录请求,如下所示: + +```java +@Controller +@Slf4j +public class SecurityController { + + @RequestMapping("/login") + public String login(){ + log.info("=== login ==="); + return "login"; + } + + @Autowired + AuthenticationManager authManager; + + @PostMapping(path="/manually-login", consumes={APPLICATION_FORM_URLENCODED_VALUE}) + public String manuallyLogin(HttpServletRequest request, String username, String password){ + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); + Authentication authentication = authManager.authenticate(authenticationToken); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(authentication); + HttpSession session = request.getSession(true); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); + System.out.println("是否认证通过:"+context.getAuthentication()); + return "redirect:/home" ; + } + // ...省略其他的 + +} + +``` + +AuthenticationManager会在安全配置中配置,下面会介绍; + +这里先介绍下manuallyLogin方法中的认证步骤: + +1. 首先构建一个`UsernamePasswordAuthenticationToken`,根据用户名和密码(这里的用户名和密码就是通过登录表单传入的) + 1. `UsernamePasswordAuthenticationToken`类是`Authentication`的实现类; + 2. 主要实现的功能就是通过用户名/密码来认证用户,构造函数实现 + 3. 然后通过`isAuthenticated`判断是否认证成功 +2. 然后通过`AuthenticationManager`对上面的`token`进行认证;这一步是关键,如果用户名密码错误或者状态异常,都会在这里报错; +3. 认证通过后,将认证结果`Authentication`保存到`SecurityContext`上下文中(这个上下文主要工作就是负责认证信息的获取和设保存); + 1. 这里如果不保存,那么认证就没意义了,后续的访问还是会重定向到登录界面;因为后续的权限检测都是通过这个上下文检测的 + 2. 这里的`SecurityContext`是线程安全的,也就是说不同用户访问的是不同的`SecurityContext`上下文,互不影响 +4. 保存到上下文后,就是session的相关操作,这里先获取session; + 1. 获取的同时会生成JSESSIONID,如果getSession(false)则不会生成JSESSIONID; + 2. 将上下文保存到session中 +5. 通过`context.getAuthentication()`验证是否认证通过 +6. 最后重定向到home页面; + +### 4. AuthenticationManager配置 + +这个刚开始自己用方法注入了一个,但是死活不成功,报内部错误,大概意思就是用户状态异常; + +后来查了下,才知道原来`WebSecurityConfigurerAdapter`配置接口本身就有获取`AuthenticationManager`的方法,直接覆写然后注入@Bean就好了,如下所示: + +```java + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } +``` + +### 5. 实践 + +启动程序,访问`http://localhost:8090/login`跳转到登录界面,输入`javalover/123456`发送表单请求到`manually-login`,认证通过重定向到`home`页面 + +![image-20211123105928146](https://i.loli.net/2021/11/23/6zuweELWQJdpNnk.png) + +![image-20211123110142791](https://i.loli.net/2021/11/23/OW3pBoHXcDGIAQZ.png) + +后台打印的认证信息如下:Authenticated=true就说明认证通过,还有对应的权限信息 + +```bash +是否认证通过:UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=javalover, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]] +``` + +## 总结 + +本篇主要通过手动认证的方式,熟悉了一下认证的过程: + +- 先构造一个认证对象,通过用户名/密码; +- 再通过认证管理器进行认证请求,成功后返回认证信息(包括用户名、密码(不可见)、权限等信息),失败时会抛出各种异常; +- 接下来将认证信息保存到上下文; +- 最后将上下文保存到session中进行管理; + + + +[源码地址]() diff --git a/demo-spring-security/demo-spring-security-remember-me/README.md b/demo-spring-security/demo-spring-security-remember-me/README.md new file mode 100644 index 0000000..e3632d9 --- /dev/null +++ b/demo-spring-security/demo-spring-security-remember-me/README.md @@ -0,0 +1,154 @@ +## 前言 + +前面介绍了基于SpringSecurity的表单登录例子; + +本篇介绍怎么给表单登录添加一个**记住我**的功能; + +有了这个功能,那么在token失效后,系统会自动获取最新token,而不用重新登录; + +这里需要注意一点:这里的token并不是普通的token,而是JSESSIONID;这个JSESSIONID会在前端第一次请求后端时返回,以后这个JSESSION就是前后台通讯的凭证 + +## 目录 + +## 正文 + +### 1. 安全配置 + +这里我们做一个最简单的配置,如下所示:添加一个rememberMe()方法 + +```java +@Configuration +@EnableWebSecurity +@Slf4j +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + // 数据没有持久化,只是保存在内存中 + auth.inMemoryAuthentication() + .withUser("javalover").password(passwordEncoder().encode("123456")).roles("USER") + .and() + .withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN"); + } + // 授权相关操作 + @Override + protected void configure(HttpSecurity http) throws Exception { + log.info("=== SecurityConfiguration.authorize ==="); + http + // 登出 所有用户都可以访问 + .logout().permitAll() + .deleteCookies("JSESSIONID") + .and() + .rememberMe() + // ...省略其他配置 + ; + } + + +} + +``` + +可以看到,我们要做的只是提供一个rememberMe()方法,前台就可以在token失效后自动获取最新的token,而不用再重新输入用户名密码进行登录操作; + +### 2. 前端组件 + +这里我们前端也尽可能简化,在之前的表单登录基础上,只增加一个复选框 rememberMe,代码如下: + +```html +
+ + + + + + + + + + + +
记住我:
+ +
+``` + +### 3. 实践-不勾选rememberMe + +下面我们就可以基于上面的代码,进行一个简单的测试:看一下 勾选rememberMe和不勾选的差别 + +> 完整代码见文末地址 + +第一步:启动程序,界面如下所示:javalover/123456 + +![image-20211122152245002](https://i.loli.net/2021/11/22/HJfNM4xW7p2z6PV.png) + +这里我们先不勾选**记住我**,点击**登录**,跳转到如下的主页: + +![image-20211122152555706](https://i.loli.net/2021/11/22/yMzTQc1YvmiuC5W.png) + +第二步:**接下来是重点**,这里我们删除本地cookie中的JSESSIONID,如下所示:F12->应用程序->cookies->JSESSION->右键-删除 + +![image-20211122152731372](https://i.loli.net/2021/11/22/UJi4FWH9qo7kv6V.png) + +第三步:刷新页面,可以看到,自动跳转到登录页面,因为token失效了,前后端通讯的凭证没了: + +![image-20211122152901019](https://i.loli.net/2021/11/22/JazEfBAciKCy7xk.png) + +> 其实上面我们也可以不删除cookie,等着session失效(默认30分钟,可以在application.yml中配置:server.servlet.session.timeout=60,默认单位秒) +> +> 后端的session都失效了,那session产生的JSESSIONID肯定也无效了; + +### 4. 实践-勾选rememberMe + +下面我们勾选**记住我**,重复上面的步骤,会发现在删除cookie中的JSESSIONID时,看到多了一个remember-me; + +![image-20211122153751334](https://i.loli.net/2021/11/22/x6aBAdEGWgvVT1t.png) + +其实这里我们可以换个角度来理解:虽然删了JSESSIONID,但是因为还有一个remember-me,所以前后端的通讯还是没有断开; + +所以此时我们刷新页面,还是停留在主页,不会跳转到登录界面; + +**但是如果我们把remember-me也一起删掉,那么结果很明显,还是会跳到登陆界面**。 + +### 5. 更多配置 + +**失效时间:** + +上面我们配置的remember-me,默认的token失效时间是两周,下面我们可以配置的短一点,比如一天: + +```java +.logout().permitAll() + .deleteCookies("JSESSIONID") + .and() + .rememberMe() + .tokenValiditySeconds(86400) + .and() +``` + +> 失效时间:严格意义上来说,上面这个失效时间 应该是remember-me的失效时间; + +这样的话,如果超过一天后,你再去删除JSESSIONID或者session失效,那么刷新页面还是会跳转到登录界面; + +**加密的密钥:** + +前面我们在调试界面看到的remember-me cookie值,它的值是由:MD5(用户名+过期时间+密码+密钥)合成的; + +这里的密钥我们可以自己配置,如下所示: + +```java +.rememberMe() + .key("privateKey") + .tokenValiditySeconds(86400) +``` + +## 总结 + +上面介绍了**rememberMe**的相关知识,了解了其实**rememberMe**就是:用新的通讯凭证`remember-me`来管理旧的通讯凭证`JSESSIONID`; + +当`JSESSIONID`被删除或者`session`过期时,如果`rememberMe cookie`还没过期(默认两周),那么系统就可以自动登录 + +`remember-me`真实的过期时间可以在调试界面看到,如下所示: + +![image-20211122162553634](https://i.loli.net/2021/11/22/bQH12ZA3k8dy4nT.png) + diff --git a/demo-spring-security/demo-spring-security-session/README.md b/demo-spring-security/demo-spring-security-session/README.md new file mode 100644 index 0000000..f591834 --- /dev/null +++ b/demo-spring-security/demo-spring-security-session/README.md @@ -0,0 +1,151 @@ + + +## 简介 + +前面我们介绍了基于SpringSecurity的两种认证方式:[表单认证](https://juejin.cn/post/7030306851762176007)和[基本认证](https://juejin.cn/post/7031077013393768484); + +本篇我们介绍下认证后如何获取用户的基本信息; + +这里的核心就是`SecurityContextHolder`类。 + +## 目录 + +1. 从SecurityContextHolder中获取 +2. 从控制器中获取 +3. 从自定义接口获取 + +## 正文 + +### 1. 从SecurityContextHolder中获取 + +代码如下所示: + +```java +Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); +if (!(authentication instanceof AnonymousAuthenticationToken)) { + String currentUserName = authentication.getName(); + return currentUserName; +}else{ + return ""; +} +``` + +这里我们加了一个校验,当获取的认证用户存在时我们才去访问; + +这种方法最大的好处就是方便,直接通过静态方法就可以获取; + +但是缺点也很明显,其中最大的缺点就是不方便测试; + +### 2. 从控制器中获取 + +通过控制器来获取用户信息,就很灵活了; + +- 我们可以通过Principal参数获取:这里的Principal其实就是一个实体类,用来代表用户,可以是个人,也可以是公司 + +```java +@GetMapping("/userinfo-principal") +public String userinfoByPrincipal(Principal principal) { + return principal.getName(); +} +``` + +- 也可以通过Authentication参数获取: + +```java +@GetMapping("/userinfo-authentication") +public String userinfoByAuthentication(Authentication authentication) { + return authentication.getName(); +} +``` + +上面我们主要获取了用户名,用户的其他信息也是可以获取的; + +这里我们可以试着获取用户的权限:通过 UserDetails 类来获取: + +```java +@GetMapping("/userinfo-authentication") +public String userinfoByAuthentication(Authentication authentication) { + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + System.out.println("User has authorities: " + userDetails.getAuthorities()); + return authentication.getName(); +} +``` + +> 需要注意的是, Principal 是不能转换成UserDetails的,因为他俩之间没关系; +> +> 而这里的authentication.getPrincipal()返回的是一个Object对象,在请求接口时,Object传入的是一个UserDetails对象,所以获取时可以通过UserDetails强转; + +通过debug我们可以看到,在访问`/userinfo-authentication`时,getPrincipal返回的实际上就是一个User对象(User实现了UserDetails),所以转换是没问题的 + +![image-20211116172359093](https://i.loli.net/2021/11/16/xogBnKGP5kcCeIJ.png) + +- 还可以通过HttpServletRequest获取:这个其实本质还是通过 Principal 获取 + +```java +@GetMapping("/userinfo-request") +public String userinfoByRequest(HttpServletRequest request) { + Principal principal = request.getUserPrincipal(); + return principal.getName(); +} +``` + +### 3. 自定义接口获取 + +前面我们体验了从控制器获取,这种方式有点局限,因为如果想在其他类中获取,就无能为力了; + +其实更灵活的方式是自己定义一个Bean,然后在需要的地方注入来获取; + +这里其实是对第一种方式的升级,将 SecurityContextHolder包装到一个Bean中,然后在需要的地方进行注入即可; + +接口类:**IAuthenticationFacade** + +```java +public interface IAuthenticationFacade { + Authentication getAuthentication(); +} +``` + +实现类:**AuthenticationFacade** + +```java +@Component +public class AuthenticationFacade implements IAuthenticationFacade { + + @Override + public Authentication getAuthentication() { + return SecurityContextHolder.getContext().getAuthentication(); + } +} +``` + +这样一来,我们就可以在需要的地方通过@Autowired注入IAuthenticationFacade即可: + +```java +@RestController +public class UserController { + + @Autowired + IAuthenticationFacade authenticationFacade; + + @GetMapping("/userinfo-custom-interface") + public String userinfoByCustomInterface() { + Authentication authentication = authenticationFacade.getAuthentication(); + return authentication.getName(); + } + +} +``` + +可以看到,这次我们没有用到任何的参数,只通过自定义的Bean来获取,这样不仅充分利用了Spring的依赖注入功能,还使得获取信息变得更加灵活。 + +## 总结 + +上面介绍了三种获取方式: + +1. 直接通过 SecurityContextHolder 的静态方法获取 +2. 从控制器中获取:通过各种参数,比如Principal、Authentication、HttpServletRequest +3. 从自定义的Bean中获取:该方法是对方法1的升级,通过将 SecurityContextHolder包装到 Bean 中,使得获取信息更加灵活 + + + +[源码地址](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-userinfo) diff --git a/demo-spring-security/demo-spring-security-userinfo/README.md b/demo-spring-security/demo-spring-security-userinfo/README.md new file mode 100644 index 0000000..f591834 --- /dev/null +++ b/demo-spring-security/demo-spring-security-userinfo/README.md @@ -0,0 +1,151 @@ + + +## 简介 + +前面我们介绍了基于SpringSecurity的两种认证方式:[表单认证](https://juejin.cn/post/7030306851762176007)和[基本认证](https://juejin.cn/post/7031077013393768484); + +本篇我们介绍下认证后如何获取用户的基本信息; + +这里的核心就是`SecurityContextHolder`类。 + +## 目录 + +1. 从SecurityContextHolder中获取 +2. 从控制器中获取 +3. 从自定义接口获取 + +## 正文 + +### 1. 从SecurityContextHolder中获取 + +代码如下所示: + +```java +Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); +if (!(authentication instanceof AnonymousAuthenticationToken)) { + String currentUserName = authentication.getName(); + return currentUserName; +}else{ + return ""; +} +``` + +这里我们加了一个校验,当获取的认证用户存在时我们才去访问; + +这种方法最大的好处就是方便,直接通过静态方法就可以获取; + +但是缺点也很明显,其中最大的缺点就是不方便测试; + +### 2. 从控制器中获取 + +通过控制器来获取用户信息,就很灵活了; + +- 我们可以通过Principal参数获取:这里的Principal其实就是一个实体类,用来代表用户,可以是个人,也可以是公司 + +```java +@GetMapping("/userinfo-principal") +public String userinfoByPrincipal(Principal principal) { + return principal.getName(); +} +``` + +- 也可以通过Authentication参数获取: + +```java +@GetMapping("/userinfo-authentication") +public String userinfoByAuthentication(Authentication authentication) { + return authentication.getName(); +} +``` + +上面我们主要获取了用户名,用户的其他信息也是可以获取的; + +这里我们可以试着获取用户的权限:通过 UserDetails 类来获取: + +```java +@GetMapping("/userinfo-authentication") +public String userinfoByAuthentication(Authentication authentication) { + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + System.out.println("User has authorities: " + userDetails.getAuthorities()); + return authentication.getName(); +} +``` + +> 需要注意的是, Principal 是不能转换成UserDetails的,因为他俩之间没关系; +> +> 而这里的authentication.getPrincipal()返回的是一个Object对象,在请求接口时,Object传入的是一个UserDetails对象,所以获取时可以通过UserDetails强转; + +通过debug我们可以看到,在访问`/userinfo-authentication`时,getPrincipal返回的实际上就是一个User对象(User实现了UserDetails),所以转换是没问题的 + +![image-20211116172359093](https://i.loli.net/2021/11/16/xogBnKGP5kcCeIJ.png) + +- 还可以通过HttpServletRequest获取:这个其实本质还是通过 Principal 获取 + +```java +@GetMapping("/userinfo-request") +public String userinfoByRequest(HttpServletRequest request) { + Principal principal = request.getUserPrincipal(); + return principal.getName(); +} +``` + +### 3. 自定义接口获取 + +前面我们体验了从控制器获取,这种方式有点局限,因为如果想在其他类中获取,就无能为力了; + +其实更灵活的方式是自己定义一个Bean,然后在需要的地方注入来获取; + +这里其实是对第一种方式的升级,将 SecurityContextHolder包装到一个Bean中,然后在需要的地方进行注入即可; + +接口类:**IAuthenticationFacade** + +```java +public interface IAuthenticationFacade { + Authentication getAuthentication(); +} +``` + +实现类:**AuthenticationFacade** + +```java +@Component +public class AuthenticationFacade implements IAuthenticationFacade { + + @Override + public Authentication getAuthentication() { + return SecurityContextHolder.getContext().getAuthentication(); + } +} +``` + +这样一来,我们就可以在需要的地方通过@Autowired注入IAuthenticationFacade即可: + +```java +@RestController +public class UserController { + + @Autowired + IAuthenticationFacade authenticationFacade; + + @GetMapping("/userinfo-custom-interface") + public String userinfoByCustomInterface() { + Authentication authentication = authenticationFacade.getAuthentication(); + return authentication.getName(); + } + +} +``` + +可以看到,这次我们没有用到任何的参数,只通过自定义的Bean来获取,这样不仅充分利用了Spring的依赖注入功能,还使得获取信息变得更加灵活。 + +## 总结 + +上面介绍了三种获取方式: + +1. 直接通过 SecurityContextHolder 的静态方法获取 +2. 从控制器中获取:通过各种参数,比如Principal、Authentication、HttpServletRequest +3. 从自定义的Bean中获取:该方法是对方法1的升级,通过将 SecurityContextHolder包装到 Bean 中,使得获取信息更加灵活 + + + +[源码地址](https://github.com/Jalon2015/spring-boot-demo/tree/master/demo-spring-security/demo-spring-security-userinfo) diff --git a/demo-swagger3/README.md b/demo-swagger3/README.md new file mode 100644 index 0000000..fefb0ed --- /dev/null +++ b/demo-swagger3/README.md @@ -0,0 +1,259 @@ +## 目录 + +- 前言:什么是Swagger +- 起步:(只需简单的3步) + - 加载依赖 + - 添加注解@EnableOpenApi + - 启动SpringBoot,访问Swagger后台界面 +- 配置:基于Java的配置 +- 注解:Swagger2 和 Swagger3做对比 +- 源码: +- 问题:踩坑记录(后面再整理) + +## 前言 + +**什么是Swagger:** + +​ Swagger 是最流行的 API 开发工具,它遵循 OpenAPI Specification(OpenAPI 规范,也简称 OAS)。 + +​ 它最方便的地方就在于,API文档可以和服务端保持同步,即服务端更新一个接口,前端的API文档就可以实时更新,而且可以在线测试。 + +​ 这样一来,Swagger就大大降低了前后端的沟通障碍,不用因为一个接口调不通而争论不休 + +> 之前用的看云文档,不过这种第三方的都需要手动维护,还是不太方便 + +## 起步 + +1. 加载依赖 + +```xml + + io.springfox + springfox-boot-starter + 3.0.0 + +``` + +2. 添加@EnableOpenApi注解 + +```java +@EnableOpenApi +@SpringBootApplication +public class SwaggerApplication { + public static void main(String[] args) { + SpringApplication.run(SwaggerApplication.class, args); + } +} +``` + +3. 启动项目,访问"http://localhost:8080/swagger-ui/index.html" + +![image-20210729112424407](D:\StudyData\github-project\tangyuanxueJava\【文章】\【SpringBoot】\知识点\后台接口文档管理Swagger3\主页.png) + +这样一个简单的Swagger后台接口文档就搭建完成了; + +下面我们说下配置和注解 + +## 配置 + +可以看到,上面那个界面中,默认显示了一个`basic-error-controller`接口分组,但是我们并没有写; + +通过在项目中查找我们发现,SpringBoot内部确实有这样一个控制器类,如下所示: + +![image-20210729113119350](D:\StudyData\github-project\tangyuanxueJava\【文章】\【SpringBoot】\知识点\后台接口文档管理Swagger3\BasicErrorController.png) + +这说明Swagger默认的配置,会自动把@Controller控制器类添加到接口文档中 + +下面我们就自己配置一下,如下所示: + +```java +import io.swagger.annotations.ApiOperation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.oas.annotations.EnableOpenApi; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; + +@Configuration +public class SwaggerConfig { + + @Bean + public Docket createRestApi() { + // 配置OAS 3.0协议 + return new Docket(DocumentationType.OAS_30) + .apiInfo(apiInfo()) + .select() + // 查找有@Tag注解的类,并生成一个对应的分组;类下面的所有http请求方法,都会生成对应的API接口 + // 通过这个配置,就可以将那些没有添加@Tag注解的控制器类排除掉 + .apis(RequestHandlerSelectors.withClassAnnotation(Tag.class)) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("GPS Doc") + .description("GPS Doc文档") + .termsOfServiceUrl("http://www.javalover.com") + .contact(new Contact("javalover", "http://www.javalover.cn", "1121263265@qq.com")) + .version("2.0.0") + .build(); + } + +} +``` + +这样上面那个`basic-error-controller`就看不见了 + +## 注解 + +我们先看下Swagger2中的注解,如下所示: + +- @Api:用在控制器类上,表示对类的说明 + - tags="说明该类的作用,可以在UI界面上看到的说明信息的一个好用注解" + - value="该参数没什么意义,在UI界面上也看到,所以不需要配置" + +- @ApiOperation:用在请求的方法上,说明方法的用途、作用 + - value="说明方法的用途、作用" + - notes="方法的备注说明" + +- @ApiImplicitParams:用在请求的方法上,表示一组参数说明 + - @ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面(标注一个指定的参数,详细概括参数的各个方面,例如:参数名是什么?参数意义,是否必填等) + - name:属性值为方法参数名 + - value:参数意义的汉字说明、解释 + - required:参数是否必须传 + - paramType:参数放在哪个地方 + - header --> 请求参数的获取:@RequestHeader + - query --> 请求参数的获取:@RequestParam + - path(用于restful接口)--> 请求参数的获取:@PathVariable + - dataType:参数类型,默认String,其它值dataType="Integer" + - defaultValue:参数的默认值 + +- @ApiResponses:用在请求的方法上,表示一组响应 + - @ApiResponse:用在@ApiResponses中,一般用于表达一个错误的响应信息 + - code:状态码数字,例如400 + - message:信息,例如"请求参数没填好" + - response:抛出异常的类 + +- @ApiModel:用于响应类上(POJO实体类),描述一个返回响应数据的信息(描述POJO类请求或响应的实体说明) + (这种一般用在post接口的时候,使用@RequestBody接收JSON格式的数据的场景,请求参数无法使用@ApiImplicitParam注解进行描述的时候) + - @ApiModelProperty:用在POJO属性上,描述响应类的属性说明 +- @ApiIgnore:使用该注解忽略这个某个API或者参数; + +上面这些是Swagger2的注解,下面我们看下Swagger3和它的简单对比 + +![Swagger3注解](https://i.loli.net/2021/07/29/s62vJN5XLKdugER.png) + +接下来我们就用Swagger3的注解来写一个接口看下效果(其中穿插了Swagger2的注解) + +- 控制器UserController.java + +```java +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiImplicitParams; +import io.swagger.annotations.ApiOperation; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.*; +import springfox.documentation.annotations.ApiIgnore; + +@Tag(name = "user-controller", description = "用户接口") +@RestController +public class UserController { + + // 忽略这个api + @Operation(hidden = true) + @GetMapping("/hello") + public String hello(){ + return "hello"; + } + + @Operation(summary = "用户接口 - 获取用户详情") + @GetMapping("/user/detail") + // 这里的@Parameter也可以不加,Swagger会自动识别到这个name参数 + // 但是加@Parameter注解可以增加一些描述等有用的信息 + public User getUser(@Parameter(in = ParameterIn.QUERY, name = "name", description = "用户名") String name){ + User user = new User(); + user.setUsername(name); + user.setPassword("123"); + return user; + } + + @Operation(summary = "用户接口 - 添加用户") + @PostMapping("/user/add") + // 这里的user会被Swagger自动识别 + public User addUser(@RequestBody User user){ + System.out.println("添加用户"); + return user; + } + +} + +``` + +实体类User.java: + +```java + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema +@Data +public class User { + + @Schema(name = "username", description = "用户名", example = "javalover") + private String username; + + @Schema(name = "password", description = "密码", example = "123456") + private String password; + + // 隐藏这个属性,这样接口文档的请求参数中就看不到这个属性 + @Schema(hidden = true) + private String email; + +} + +``` + +启动后运行界面如下: + +- 首页展示: + +![image-20210729132629924](D:\StudyData\github-project\tangyuanxueJava\【文章】\【SpringBoot】\知识点\后台接口文档管理Swagger3\首页展示.png) + +- /user/add接口展示: + +![image-20210729132730799](D:\StudyData\github-project\tangyuanxueJava\【文章】\【SpringBoot】\知识点\后台接口文档管理Swagger3\user-add接口.png) + +- /user/detail接口展示 + + ![image-20210729132849933](D:\StudyData\github-project\tangyuanxueJava\【文章】\【SpringBoot】\知识点\后台接口文档管理Swagger3\user-detail接口.png) + +## 问题 + +目前只是简单地体验了下,其实里面还是有很多坑,等后面有空再整理解决,下面列举几个: + +- @Paramters参数无效 +- @ApiImplicitParamter的body属性无效 +- @Tag的name属性:如果name属性不是当前类名的小写连字符格式,则会被识别为一个单独的接口分组 +- 等等 + + + +**最近整理了一份面试资料《Java面试题-校招版》附答案,无密码无水印,感兴趣的可以关注公众号回复“面试”领取。** + diff --git a/demo-task/README.md b/demo-task/README.md new file mode 100644 index 0000000..829e262 --- /dev/null +++ b/demo-task/README.md @@ -0,0 +1,52 @@ +# 定时任务:Spring自带的TaskScheduler接口 + +## 简介 + +Spring自带的`TaskScheduler`主要用来执行一些定时任务,比如每天的23点执行一次任务(固定时间点)、每隔10分钟执行一次任务等(固定频率) + +## 示例 + +- 首先写一个定时任务 + + `MyTask.java` + + ```java + @Component + public class MyTask { + + @Scheduled(cron = "*/5 * * * * *") + public void task1(){ + System.out.println("this is task1"); + } + } + + ``` + +- 然后在主程序中添加注解`@EnableScheduling` + + ```java + @SpringBootApplication + @EnableScheduling + public class TaskApplication { + public static void main(String[] args) { + SpringApplication.run(TaskApplication.class, args); + } + } + + ``` + +- 最后启动程序,就可以看到控制台的任务执行情况,每隔5s打印一次 + +## 知识点 + +- 注解`@Scheduled`:设置定时任务,支持cron表达式、fixedRate固定频率触发等; +- 注解`@EnableScheduling `:开启定时任务,可以加在主类或者配置类中 +- 配置线程池大小`spring.task.scheduling.pool.size=20`:默认是1 + +## 参考 + +- [@Scheduled](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling-annotation-support-scheduled)注解 + +- [cron表达式](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling-cron-expression) + +- [@EnableScheduling](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling-annotation-support)注解 \ No newline at end of file diff --git a/demo-upload/README.md b/demo-upload/README.md new file mode 100644 index 0000000..221a187 --- /dev/null +++ b/demo-upload/README.md @@ -0,0 +1,94 @@ +# 上传文件 + +## 简介 + +前端用vue写个简单界面,用来选择文件,进行上传 + +后端用Spring Boot写个接口,用来接收文件 + +> PS:注意跨域问题 + +## 示例 + +#### 前端: + +**文件上传页面** `Home.vue` + +```vue + + +